本文章是由機器翻譯。

C#

在您的 Roslyn 分析器中加入程式碼修正

AlexTurner

如果您遵循的步驟在我以前的文章,"使用 Roslyn 到寫活代碼分析器為您 API"(msdn.microsoft.com/magazine/dn879356),你寫顯示即時錯誤不正確正則運算式 (RegEx) 模式字串分析器。每個不正確模式在編輯器中,獲取紅色的波浪線,就像你會看到編譯器錯誤,和那些亂七八糟的文字出現活的當您鍵入您的代碼。這其中電源 C# 和Visual Basic編輯經驗的Visual Studio2015年預覽新.NET 編譯器平臺 ("羅斯林") 的 Api,使成為可能。

你可以做更多嗎?如果你有知識,看到不只是有什麼不對,但也如何修復它,你可以建議通過新的Visual Studio燈泡的相關代碼修復。此代碼修復程式將允許開發人員使用您的分析器不只是在他的代碼中查找錯誤 — — 他立刻可以還清理它。

在這篇文章,我將向你展示如何將代碼修補程式提供程式添加到您提供的修補程式的 RegEx 診斷分析儀在每條曲線都正則運算式。此修復程式將作為一個專案列入燈泡功能表,讓使用者預覽此修復程式並將其應用於她的代碼會自動添加。

撿回你停下來的地方

若要開始,請確保您已經遵循與上一篇文章中的步驟。在那篇文章,我將向您展示如何編寫第一半您的分析器,生成每個不正確正則運算式模式字串根據診斷的花體字。那篇文章你穿過:

  • 安裝Visual Studio2015年預覽、 SDK 和相關的羅斯林 VSIX 包。
  • 創建一個新的診斷代碼修復專案。
  • 將代碼添加到 DiagnosticAnalyzer.cs 來執行不正確正則運算式模式檢測。

如果你想要快速趕上,查閱圖 1,­其中列出最終的代碼為 DiagnosticAnalyzer.cs。

圖 1 DiagnosticAnalyzer.cs 的完整代碼

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace RegexAnalyzer
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class RegexAnalyzerAnalyzer : DiagnosticAnalyzer
  {
    public const string DiagnosticId = "Regex";
    internal const string Title = "Regex error parsing string argument";
    internal const string MessageFormat = "Regex error {0}";
    internal const string Category = "Syntax";
    internal static DiagnosticDescriptor Rule =
      new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
      Category, DiagnosticSeverity.Error, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor>
      SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
    public override void Initialize(AnalysisContext context)
    {
      context.RegisterSyntaxNodeAction(
        AnalyzeNode, SyntaxKind.InvocationExpression);
    }
    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
    {
      var invocationExpr = (InvocationExpressionSyntax)context.Node;
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      if (memberAccessExpr?.Name.ToString() != "Match") return;
      var memberSymbol = context.SemanticModel.
        GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      if (!memberSymbol?.ToString().StartsWith(
        "System.Text.RegularExpressions.Regex.Match") ?? true) return;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      if ((argumentList?.Arguments.Count ?? 0) < 2) return;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      if (regexLiteral == null) return;
      var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
      if (!regexOpt.HasValue) return;
      var regex = regexOpt.Value as string;
      if (regex == null) return;
      try
      {
        System.Text.RegularExpressions.Regex.Match("", regex);
      }
      catch (ArgumentException e)
      {
        var diagnostic =
          Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message);
        context.ReportDiagnostic(diagnostic);
      }
    }
  }
}

轉型不變的語法樹

最後一次,當你在寫診斷分析儀,檢測到不正確正則運算式模式,第一步是使用語法視覺化檢視以識別模式的語法樹,正表明問題的代碼。你然後寫跑每次相關的節點類型的分析方法被發現。方法檢查有必要誤差曲線的語法節點的模式。

寫一個修補程式是一個類似的過程。你處理語法樹,專注于所需的新狀態的代碼檔的使用者應用您修復後。大多數代碼修復包括添加、 刪除或替換語法節點從當前的樹,產生新的語法樹。你可以直接在語法節點上操作或使用讓你的 Api 進行專案範圍的更改,如重命名。

一個非常重要的屬性,瞭解語法節點、 樹木和.NET 編譯器平臺中的符號是不可變的。一旦創建了一個語法節點或一棵樹,它不能修改 — — 一個給定的樹或節點物件將始終表示相同的 C# 或Visual Basic代碼。

用於轉換的原始程式碼 API 中不變性似乎有悖常理。如何可以添加、 移除和替換在語法樹中的子節點,如果既不是這棵樹,也不是它的節點可以改變嗎?在這裡有用是考慮.NET 字串類型,另一個不可變類型最有可能使用每一天。您執行操作,很多時候,轉換字串連接在一起,甚至替換子字串使用 String.Replace。 但是,沒有這些操作實際上更改原始字串物件。相反,每次調用返回新的字串物件,它表示字串的新的狀態。你可以將這個新物件分配回你原來的變數,但您傳遞到老的字串的任何方法將仍有原始值。

將參數節點添加到永恆不變的樹探索如何不變性適用于語法樹,會在代碼編輯器中,手動執行一個簡單的轉換和查看它如何影響語法樹。

在Visual Studio2015年預覽 (副檔名語法視覺化檢視安裝,請參閱以前的文章) 內, 創建一個新的 C# 代碼檔。它的所有內容替換為以下代碼:

class C
{
  void M()
  }
}

打開語法視覺化檢視,選擇查看 |其他視窗 |羅斯林語法視覺化檢視和代碼檔內的任意位置按一下來填充這棵樹。在語法視覺­izer 視窗中,按右鍵根 CompilationUnit 節點,然後選擇視圖定向語法圖。視覺化此語法樹結果像一個在圖中圖 2 (此處所示的圖省略表示空白的灰色和白色的瑣事節點)。藍色的參數­清單語法節點都有兩個綠色的孩子權杖代表它的圓括弧和沒有藍子語法節點,如清單中不包含任何參數。

語法樹轉換之前
圖 2 語法樹轉換之前

你會在這裡類比的變換是那個會添加一個新的參數屬於 int 類型。鍵入代碼"int 我"括弧的方法 M 的參數清單和手錶內語法視覺化檢視作為你的變化類型:

class C
{
  void M(int i)
  {
  }
}

請注意,甚至在您完成鍵入時您不完整的代碼包含編譯錯誤 (語法視覺化檢視中顯示為節點具有紅色背景),這棵樹是仍然連貫,編譯器猜測您的新代碼將形成一個有效的參數節點之前。這種彈性的編譯器錯誤語法樹是什麼允許 IDE 功能和您的診斷程式來有效的對付不完整的代碼。

再次按右鍵根 CompilationUnit 節點上,生成一個新的圖,應該看起來像圖 3 (再次,描繪在這裡沒有瑣事)。

Syntax Tree After the Transform
圖 3 語法樹變換後

請注意 ParameterList 現在有三個孩子,兩個括弧標記之前,再加上一個新的參數語法節點。作為您鍵入"int 我"在編輯器中,Visual Studio替換文檔的以前的語法樹這新的語法樹,它表示您新的原始程式碼。

執行全額重置作品不夠好,小的字串,是單個物件,但是語法樹呢?一個大型的代碼檔可能包含數千或數萬數以千計的語法節點和你當然不希望所有這些節點必須重新創建每次有人鍵入一個字元在一個檔內。這將產生孤立物件的垃圾回收器清理和嚴重傷害性能的噸。

幸運的是,語法節點的不變性質還提供了在這裡的轉義。因為當你做一個小小改變,就不會受到影響大部分的文檔中的節點,這些節點可以安全地重用作為為新樹中的兒童。幕後的內部節點中存儲的資料對於給定的語法節點只能向下指向節點的子節點。因為這些內部的節點沒有父級指標,它是安全的相同的內部節點,讓在一遍遍在許多反覆運算中的給定的語法樹,只要代碼的那一部分保持不變。

此節點再利用是指在一棵樹,需要對每一次擊鍵重新創建中的唯一節點是那些至少一個子體發生了變化,即窄窄的鏈子的祖先節點到根,如中所示圖 4。所有其他節點都按原樣重用。

在轉換期間替換下來的祖先節點
圖 4 在轉換期間替換下來的祖先節點

在這種情況下,核心變化是創建您新的參數節點,然後用新的 ParameterList 具有新的參數作為一個子節點插入替換 ParameterList。替換 ParameterList 也需要替換的祖先節點鏈作為每一個祖先更改清單的子節點,包括替換的節點。本文後面會用 SyntaxNode.ReplaceNode 方法,照顧所有祖先節點都替換為你為你的正則運算式分析器做替換那種。

你現在見到規劃代碼修復的一般模式:你開始在中使用代碼之前觸發診斷程式的狀態。然後您手動更改此修復程式應使,觀察對語法樹的影響。最後,你算出所需更換節點創建並返回一個新的語法樹,包含它們的代碼。

請確保你有你的專案打開,包含您創建最後一次的診斷。若要實現你的代碼修復,就會挖 CodeFixProvider.cs。

GetFixableDiagnosticIds 方法

修補程式和診斷程式他們解決由診斷 Id 是鬆散耦合。每個代碼修復的目標是一個或多個診斷 Id。每當Visual Studio見到具有匹配 ID 的診斷,它會詢問您代碼修補程式供應商是否您有代碼修復提供。鬆散耦合的基礎的 ID 字串允許一個分析器來的別人的分析器中或甚至修復內置編譯器錯誤和警告產生的診斷提供一種解決方案。

在這種情況下,您的分析器產生診斷程式和代碼修復。你可以看到的 GetFixableDiagnosticIds 方法已經返回的診斷的 ID,您定義在您診斷的類型,所以沒什麼可在此處進行更改。

ComputeFixesAsync 方法

ComputeFixesAsync 方法是代碼修復的主要驅動力。每當一個或多個匹配診斷發現給定時段內的代碼時,將調用此方法。

你可以看到範本的預設實現的 ComputeFixesAsync 方法掏出的第一次診斷從上下文 (在大多數情況下,只想要一個),並獲取診斷的範圍。下一行然後搜索語法樹從那要找到最近的型別宣告節點的範圍。在預設的範本規則,是其內容需要固定的相關節點。

你的情況,你寫的診斷分析儀在尋找,看看他們是否對 Regex.Match 的調用的調用。 為了説明分享您的診斷和你代碼修復之間的邏輯,請更改樹搜索 OfType 篩選器所述要找到那相同的 InvocationExpressionSyntax 節點的類型。重命名為"invocationExpr,"區域變數:

var invocationExpr = root.FindToken(
  diagnosticSpan.Start).Parent.AncestorsAndSelf()
  .OfType<InvocationExpressionSyntax>().First();

現在可以對診斷分析儀開頭相同的調用節點的引用。在 next 語句中,您將傳遞此節點到會計算你會果脯的代碼更改的方法­gesting 使此修復程式。重命名該方法從 MakeUppercaseAsync 到 FixRegexAsync,並更改修復程式描述為修復正則運算式:

context.RegisterFix(
  CodeAction.Create("Fix regex", c => FixRegexAsync(
  context.Document, invocationExpr, c)), diagnostic);

每次調用上下文的 RegisterFix 方法將新的代碼操作與診斷波形曲線上時,相關聯,會產生光燈泡內的功能表項目。請注意你實際上並沒有打電話尚未執行代碼轉換的 FixRegexAsync 方法。相反,該方法調用被包裹在Visual Studio能過一會再打一個 lambda 運算式。這是轉換的因為您的結果轉換的只需要當使用者實際上選擇您修復 RegEx 的專案。突出顯示或選擇修復專案時,Visual Studio將調用您的行動,以生成預覽或應用此修復程式。在那之前,Visual Studio避免運行您的修復方法,只是以防萬一你執行昂貴的操作例如,重命名解決方案範圍。

請注意代碼修復提供程式並不義務產生的每個實例的給定的診斷代碼修復。它往往是你能修復建議僅對某些情況你分析儀可以波形曲線的情況。如果你只能修復一些情況下,您應該首先在測試 ComputeFixesAsync 您需要確定是否可以修復的具體情況的任何條件。如果這些條件得不到滿足,您應該返回從 ComputeFixesAsync 而無需調用 RegisterFix。

對於此示例,你會提供修補程式的所有實例的診斷,所以沒有更多的條件來檢查。

FixRegexAsync 方法

現在,您得到的代碼修復心臟。目前寫的 FixRegexAsync 方法將文檔並生成已更新的解決方案。雖然診斷分析儀看看特定節點和符號,代碼修復可以更改在整個解決方案的代碼。你可以看看這裡的範本代碼打電話 Renamer.RenameSymbol­非同步,改變不只是符號的型別宣告,但也對整個解決方案那標誌的任何引用。

在這種情況下,您只希望對在當前文檔中,模式字串進行本地更改,所以您可以從任務更改方法的返回類型<解決方案>任務<文檔>。此簽名是仍然符合 ComputeFixes 中的 lambda 運算式­非同步,作為 CodeAction.Create 具有接受一份檔,而不是解決辦法的另一個重載。你也將需要更新的 typeDecl 參數,以匹配你正在從 ComputeFixesAsync 方法傳遞中的 InvocationExpressionSyntax 節點:

private async Task<Document> FixRegexAsync(Document document,
  InvocationExpressionSyntax invocationExpr, CancellationToken cancellationToken)

因為你不需要任何"讓大寫"邏輯,刪除的方法,以及身體。

尋找到替換節點你固定在今年上半年將看上去很像第一一半你診斷分析儀 — — 你需要挖進 InvocationExpression 找到可否告知您修復的方法調用的相關部分。事實上,你可以只是在 try-catch 塊 AnalyzeNode 法在今年上半年將複製。跳過的第一行,儘管,你已作為 invocationExpr 作為一個參數。因為你知道這是為你成功地發現了一個診斷的代碼,您可以刪除所有的"如果"檢查。只有其他轉變,使是語義模型提取文檔參數,如你不再有直接提供的語義模型的上下文。

當你完成這些更改時,您的 FixRegexAsync 方法的主體應如下所示:

var semanticModel = await document.GetSemanticModelAsync(cancellationToken);
var memberAccessExpr =
  invocationExpr.Expression as MemberAccessExpressionSyntax;
var memberSymbol =
  semanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
var regexLiteral =
  argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
var regexOpt = semanticModel.GetConstantValue(regexLiteral);
var regex = regexOpt.Value as string;

生成替換節點現在,你又有 RegExLiteral,代表你舊的字串文本,您需要生成新的一個。計算到底什麼你需要修復一個任意的正則運算式模式的字串一項大的任務,遠遠超出本文的範圍。作為現在的替身,您將只使用字串有效正則運算式,這確實是一個有效的正則運算式模式的字串。如果您決定要你自己去進一步,你應該開始小和目標你在非常特殊的正則運算式問題的修復。

低級的方式來產生新的語法節點的代入你的樹是通過上 SyntaxFactory 的成員。這些方法讓你在完全您選擇的形狀中創建每種類型的語法節點。然而,往往證明只是解析的表達你想要從文本,讓編譯器做所有繁重的操作來創建節點變得更容易。若要分析的代碼片段,只是調用 SyntaxFactory.ParseExpression 並指定字串的代碼:

var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"");

這個新的文本會工作以及在大多數情況下,替代,但它失去了一些東西。如果你還記得,語法標記可以有附加表示空白或注釋的瑣事。您將需要複製任何瑣事從舊的文本運算式,以確保不從舊代碼中刪除任何間距或評論。它也是良好的做法來標記您用"格式化程式"批註,通知您想要您新的節點,根據最終使用者代碼樣式設置格式的代碼修復引擎創建的新節點。您需要添加一條 using 指令 Microsoft.CodeAnalysis.Formatting 命名空間。與這些電子束­,你的 ParseExpression 電話看起來像這樣:

var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"")
  .WithLeadingTrivia(regexLiteral.GetLeadingTrivia())
  .WithTrailingTrivia(regexLiteral.GetTrailingTrivia())
  .WithAdditionalAnnotations(Formatter.Annotation);

交換新的節點到語法樹現在,你有一個新的語法節點字串,您可以替換舊的節點在語法樹中,生產與一個固定的正則運算式模式字串的新樹。

第一,你得到當前文檔語法樹的根節點:

var root = await document.GetSyntaxRootAsync();

現在,可以給出舊的語法節點交換和交換在新一個該語法根上調用 ReplaceNode 方法:

var newRoot = root.ReplaceNode(regexLiteral, newLiteral);

請記住您正在生成一個新的根節點。替換任何語法節點也要求您替換它到根的父母。正如你看到之前,.NET 編譯器平臺中的所有語法節點都是不可變的。此替換操作實際上只是返回一個新的根語法節點與目標節點和更換指示其祖先。

既然您已經有一個新的語法根用點燃的固定字串­,你可以走上一個更多級別的樹,以生成新的文檔物件,其中包含您最新的根。若要替換的根,在文檔上使用 WithSyntaxRoot 方法:

var newDocument = document.WithSyntaxRoot(newRoot);

這是你剛才看到時調用 WithLeadingTrivia 和其他方法對你解析的表達的相同與 API 模式。經常轉換 Roslyn 不可變物件模型中的現有物件時,您將看到這與模式。這個想法是類似于.NET String.Replace 方法,它返回一個新字串物件。

與轉換的文檔在手,你現在可以從 FixRegexAsync 返回它:

return newDocument;

您在 CodeFixProvider.cs 的代碼現在應該看起來像圖 5

圖 5 CodeFixProvider.cs 的完整代碼

using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
namespace RegexAnalyzer
{
  [ExportCodeFixProvider("RegexAnalyzerCodeFixProvider",
    LanguageNames.CSharp), Shared]
  public class RegexAnalyzerCodeFixProvider : CodeFixProvider
  {
    public sealed override ImmutableArray<string> GetFixableDiagnosticIds()
    {
      return ImmutableArray.Create(RegexAnalyzer.DiagnosticId);
    }
    public sealed override FixAllProvider GetFixAllProvider()
    {
      return WellKnownFixAllProviders.BatchFixer;
    }
    public sealed override async Task ComputeFixesAsync(CodeFixContext context)
    {
      var root =
        await context.Document.GetSyntaxRootAsync(context.CancellationToken)
        .ConfigureAwait(false);
      var diagnostic = context.Diagnostics.First();
      var diagnosticSpan = diagnostic.Location.SourceSpan;
      // Find the invocation expression identified by the diagnostic.
      var invocationExpr =   
        root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf()
        .OfType<InvocationExpressionSyntax>().First();
      // Register a code action that will invoke the fix.
      context.RegisterFix(
        CodeAction.Create("Fix regex", c =>
        FixRegexAsync(context.Document, invocationExpr, c)), diagnostic);
    }
    private async Task<Document> FixRegexAsync(Document document,
      InvocationExpressionSyntax invocationExpr,
      CancellationToken cancellationToken)
    {
      var semanticModel =
        await document.GetSemanticModelAsync(cancellationToken);
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      var memberSymbol =
        semanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      var regexOpt = semanticModel.GetConstantValue(regexLiteral);
      var regex = regexOpt.Value as string;
      var newLiteral = SyntaxFactory.ParseExpression("\"valid regex\"")
        .WithLeadingTrivia(regexLiteral.GetLeadingTrivia())
        .WithTrailingTrivia(regexLiteral.GetTrailingTrivia())
        .WithAdditionalAnnotations(Formatter.Annotation);
      var root = await document.GetSyntaxRootAsync();
      var newRoot = root.ReplaceNode(regexLiteral, newLiteral);
      var newDocument = document.WithSyntaxRoot(newRoot);
      return newDocument;
    }
  }
}

嘗試它就是這樣 !現在,您已經定義了代碼修復其變換運行時使用者會遇到你的診斷和修復從功能表中選擇燈泡。嘗試代碼修復,請按 F5 再次在Visual Studio的主要實例中,打開主控台應用程式。這一次,當您將游標放在你波形曲線上時,你應該看到左側顯示一個燈泡。點擊燈泡應該彈出一個功能表包含修復正則運算式代碼操作定義,如中所示圖 6。此功能表顯示預覽與內聯比較舊的文檔和新文檔之間創建,這表示您的代碼的狀態,如果您選擇要應用此修復程式。

嘗試您的代碼修復
圖 6 嘗試您的代碼修復

如果您選擇該功能表項目,Visual Studio將新文檔,並採用它作為該原始檔案的編輯緩衝區的目前狀態。現今已應用到您修復 !

祝賀

你做到了 !在大約 70 全行新代碼,您確定問題在您的使用者代碼中,活著,他打字、 以及它的錯誤,如紅字彎彎曲曲地浮出水面的一個代碼修復程式,可以把它收拾乾淨。你轉換語法樹,生成新的語法節點,一路走來,所有在你熟悉的目標域中的正則運算式操作時。

雖然你可以不斷地細化的診斷程式和代碼修復你寫,我發現分析儀用.NET 編譯器平臺建成讓你在短的時間內完成很多工。一旦你得到舒適的建築分析儀,你就會開始發現各種常見的問題您編碼生活的日常和檢測重複修補程式,您可以自動執行。

你將分析什麼?


AlexTurner 是的微軟,在那裡他已經醞釀了.NET 編譯器平臺 ("羅斯林") 專案的 C# 和Visual Basic善的託管語言團隊高級專案經理。他畢業于紐約州立大學石溪分校的電腦科學碩士,曾在生成、 PDC、 TechEd、 TechDays 和混合。

感謝以下的微軟技術專家對本文的審閱:Bill輔以辣椒和盧西安 Wischik
盧西恩 • Wischik 是VB/C# 語言設計團隊在 Microsoft,用VB的特殊責任。在加入微軟之前他在學術界對併發性理論與非同步工作。他是一個熱衷於水手和長距離游泳的選手。

Bill辣椒在語言 (CMU Common Lisp,狄倫,IronPython,和 C#) 工作和開發人員工具他職業生涯的大部分。他花了過去 17 年來在工作一切從核心功能Visual Studio,向動態語言運行時的語言,C# 的微軟開發部)。