教學課程:撰寫您的第一個分析器和程式碼修正

.NET Compiler Platform SDK 提供建立自訂診斷 (分析器) 、程式碼修正、程式碼重構,以及以 C# 或 Visual Basic 程式碼為目標的診斷隱藏器所需的工具。 分析器包含可辨識違反規則的程式碼。 您的程式碼修正包含可修正違規的程式碼。 您所實作的規則可以是程式碼結構、編碼樣式、命名慣例等任何項目。 .NET Compiler Platform 可提供開發人員在撰寫程式碼時執行分析所需的架構,以及修正程式碼所需的所有 Visual Studio UI 功能:在編輯器中顯示波浪線、填入 Visual Studio 錯誤清單、建立「燈泡」建議,以及顯示建議修正的各式預覽。

在本教學課程中,您將探索如何使用 Roslyn API 建立分析器和隨附的程式碼修正。 分析器是執行原始程式碼分析並向使用者報告問題的方式之一。 又或者,程式碼修正可以與分析器相關聯,以代表對使用者的原始程式碼進行修改。 本教學課程所建立的分析器會尋找可使用 const 修飾詞來宣告、但並未這麼做的區域變數宣告。 隨附的程式碼修正會修改這些宣告,而新增 const 修飾詞。

必要條件

您必須透過Visual Studio 安裝程式 安裝 .NET Compiler Platform SDK

安裝指示 - Visual Studio 安裝程式

有兩種不同方式可以在 [Visual Studio 安裝程式] 中尋找 [.NET Compiler Platform SDK]

使用 Visual Studio 安裝程式安裝 - 工作負載檢視

不會自動選取 .NET Compiler Platform SDK 作為 Visual Studio 延伸模組開發工作負載的一部分。 您必須選取它作為為選用元件。

  1. 執行 [Visual Studio 安裝程式]
  2. 選取 [修改]
  3. 核取 [Visual Studio 延伸模組開發]
  4. 在摘要樹狀結構中開啟 [Visual Studio 延伸模組開發] 節點。
  5. 核取 [.NET Compiler Platform SDK] 的方塊。 您將在選用元件下的最後處找到它。

(選擇性) 您可能也需要 [DGML 編輯器] 以在視覺化檢視中顯示圖形:

  1. 在摘要樹狀結構中開啟 [個別元件] 節點。
  2. 核取 [DGML 編輯器] 的方塊

使用 Visual Studio 安裝程式安裝 - [個別元件] 索引標籤

  1. 執行 [Visual Studio 安裝程式]
  2. 選取 [修改]
  3. 選取 [個別元件] 索引標籤
  4. 核取 [.NET Compiler Platform SDK] 的方塊。 您將在 [編譯器、建置工具與執行階段] 區段下的頂端找到它。

(選擇性) 您可能也需要 [DGML 編輯器] 以在視覺化檢視中顯示圖形:

  1. 核取 [DGML 編輯器] 的方塊。 您將在 [程式碼工具] 區段下找到它。

建立和驗證您的分析器時須執行幾個步驟:

  1. 建立解決方案。
  2. 註冊的分析器名稱和描述。
  3. 報告分析器警告和建議。
  4. 實作接受建議的程式碼修正。
  5. 透過單元測試來改善分析。

建立方案

  • 在 Visual Studio 中選擇 [檔案 > 新增 > 專案...],以顯示 [新增專案] 對話方塊。
  • [Visual C# > 擴充性] 下,選擇 [具有程式碼修正的分析器] (.NET [標準) ]。
  • 將您的專案命名為 "MakeConst",然後按一下 [確定]。

注意

您可能會收到編譯錯誤 (MSB4062:無法載入 "CompareBuildTaskVersion" 工作)。 若要修正此問題,請使用 NuGet 套件管理員更新方案中的 NuGet 套件,或在 [套件管理員主控台] 視窗中使用 Update-Package

瀏覽分析器範本

具有程式碼修正範本的分析器會建立五個專案:

  • MakeConst,其中包含分析器。
  • MakeConst.CodeFixes,其中包含程式碼修正。
  • MakeConst.Package,用來產生分析器和程式碼修正的 NuGet 套件。
  • MakeConst.Test,這是單元測試專案。
  • MakeConst.Vsix,這是啟動已載入新分析器 Visual Studio 第二個執行個體的預設啟動專案。 按 F5 以啟動 VSIX 專案。

注意

分析器應該以 .NET Standard 2.0 為目標,因為它們可以在 .NET Core 環境中執行, (命令列建置) 和.NET Framework環境 (Visual Studio) 。

提示

當您執行分析器時,您會啟動 Visual Studio 的第二個複本。 此複本會使用不同的登錄區來儲存設定。 這可讓您區分兩個 Visual Studio 複本中的的視覺化設定。 您可以挑選不同的佈景主題,用於 Visual Studio 的實驗性執行。 此外,請勿使用 Visual Studio 的實驗性執行漫遊您的設定或登入 Visual Studio 帳戶。 設定會因此而不同。

Hive 不僅包含開發中的分析器,也包含任何先前開啟的分析器。 若要重設 Roslyn hive,您必須從 %LocalAppData%\Microsoft\VisualStudio 手動將其刪除。 Roslyn hive 的資料夾名稱將會以 Roslyn 結尾,例如 16.0_9ae182f9Roslyn。 請注意,您可能需要清除解決方案,並在刪除 Hive 之後加以重建。

在您剛啟動的第二個 Visual Studio 執行個體中,建立新的 C# 主控台應用程式專案 (任何目標架構都會運作-- 分析器會在來源層級運作。) 將滑鼠停留在具有波浪底線的權杖上,分析器所提供的警告文字會隨即出現。

範本會建立分析器,以針對每個在類型名稱中包含小寫字母的類型宣告回報警告,如下圖所示:

報告警告的分析器

範本也會提供程式碼修正,以將任何包含小寫字元的類型名稱全部變更為大寫。 您可以按一下隨警告顯示的燈泡,以查看建議的變更。 若接受建議的變更,即會在解決方案中將類型名稱和所有參考更新為該類型。 您已了解作用中的初始分析器,現在請關閉第二個 Visual Studio 執行個體,並返回分析器專案。

您不需要啟動 Visual Studio 的第二個複本,並建立新的程式碼來測試分析器中的每一項變更。 範本也會為您建立單元測試專案。 該專案包含兩項測試。 TestMethod1 顯示會分析程式碼而不觸發診斷的一般測試格式。 TestMethod2 顯示會觸發程序然後套用建議程式碼修正的測試格式。 當您建置分析器和程式碼修正時,您會為不同的程式碼結構撰寫測試,以確認您的工作。 分析器的單元測試會比使用 Visual Studio 以互動方式測試快得多。

提示

當您知道哪些程式碼建構應該和不應觸發分析器時,分析器單元測試會是很棒的工具。 在 Visual Studio 的另一個複本中載入您的分析器,可極有效地瀏覽和尋找您可能未考量到的建構。

本教學課程中,您撰寫的分析器會向使用者報告可轉換成區域常數的任何區域變數宣告。 例如,請考慮下列程式碼:

int x = 0;
Console.WriteLine(x);

在上述程式碼中,會為 x 指派常數值,且一律不會修改。 此值可使用 const 修飾詞來宣告:

const int x = 0;
Console.WriteLine(x);

其中涉及用以判斷變數是否可設為常數的分析,這需要進行語法分析、初始設定式運算式的常數分析,和資料流程分析,以確保一律不會寫入變數。 .NET Compiler Platform 所提供的 API 可讓您更輕鬆地執行這項分析。

建立分析器註冊

範本會在 MakeConstAnalyzer.cs 檔案中建立初始 DiagnosticAnalyzer 類別,。 此初始分析器會顯示每個分析器的兩個重要屬性。

  • 每個診斷分析器都必須提供 [DiagnosticAnalyzer] 屬性,以說明它據以運作的語言。
  • 每個診斷分析器都必須 (直接或間接) 衍生自 DiagnosticAnalyzer 類別。

範本也會顯示屬於任何分析器的基本功能:

  1. 註冊動作。 動作代表應觸發分析器以檢查程式碼是否違規的程式碼變更。 當 Visual Studio 偵測到與已註冊的動作相符的程式碼編輯時,就會呼叫分析器已註冊的方法。
  2. 更新診斷。 您的分析器在偵測到違規時,將會建立 Visual Studio 用來向使用者通報違規情事的診斷物件。

您可以在 DiagnosticAnalyzer.Initialize(AnalysisContext) 方法的覆寫中註冊動作。 在本教學課程中,您會瀏覽語法節點以尋找區域宣告,並查看有哪些含有常數值。 如果某個宣告可能是常數,分析器即會建立並報告診斷。

第一個步驟是更新註冊常數和 Initialize 方法,讓這些常數表示您的「設為常數」分析器。 大部分的字串常數都會定義在字串資源檔中。 您應遵循該作法,以方便當地語系化。 開啟 MakeConst 分析器專案的 Resources.resx 檔案。 這會顯示資源編輯器。 更新字串資源,如下所示:

  • AnalyzerDescription 變更為「Variables that are not modified should be made constants.」。
  • AnalyzerMessageFormat 變更為「Variable '{0}' can be made constant」。
  • AnalyzerTitle 變更為「Variable can be made constant」。

完成作業後資源編輯器應會如下圖所示:

更新字串資源

其餘變更位於分析器檔案中。 請在 Visual Studio 中開啟 MakeConstAnalyzer.cs。 將已註冊的動作從以符號為目標變更為以語法為目標。 在 MakeConstAnalyzerAnalyzer.Initialize 方法中,找出符號的動作是以哪一行註冊的:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

將其取代為以下這一行:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

完成此變更後,您可以刪除 AnalyzeSymbol 方法。 此分析器會檢查 SyntaxKind.LocalDeclarationStatement,而非 SymbolKind.NamedType 陳述式。 請注意,AnalyzeNode 的下方有紅色波浪線。 您剛才新增的程式碼會參考尚未宣告的 AnalyzeNode 方法。 請使用下列程式碼宣告該方法:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

MakeConstAnalyzer.cs 中將 Category 變更為 Usage,如下列程式碼所示:

private const string Category = "Usage";

尋找可能是常數的區域宣告

現在我們將撰寫 AnalyzeNode 方法的第一個版本。 它會尋找可能是 const、但實際並不是的單一區域宣告,如下列程式碼所示:

int x = 0;
Console.WriteLine(x);

第一個步驟是尋找區域宣告。 在 MakeConstAnalyzer.cs 中將下列程式碼新增至 AnalyzeNode

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

這項轉換一定會成功,因為您的分析器已註冊區域宣告的變更,而且也僅限於區域宣告。 沒有其他節點類型會觸發對 AnalyzeNode 方法的呼叫。 接下來,請檢查宣告中是否有任何 const 修飾詞。 如有發現,請立即傳回。 下列程式碼會尋找區域宣告上的任何 const 修飾詞:

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

最後,您必須確認變數有可能是 const。 其意義是確定變數在初始化後未曾進行指派。

您將使用 SyntaxNodeAnalysisContext 執行某種語意分析。 您可以使用 context 引數判斷區域變數宣告是否可設為 constMicrosoft.CodeAnalysis.SemanticModel 代表單一來源檔案中的所有語意資訊。 您可以在說明語意模型的文章中深入了解相關資訊。 您將使用 Microsoft.CodeAnalysis.SemanticModel 執行區域宣告陳述式的資料流程分析。 然後,您可以使用此資料流程分析的結果,確保區域變數不會在其他任何位置以新值寫入。 呼叫 GetDeclaredSymbol 擴充方法以擷取變數的 ILocalSymbol,並確認它並未包含於資料流程分析的 DataFlowAnalysis.WrittenOutside 集合中。 將下列程式碼新增至 AnalyzeNode 方法的結尾:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

剛才新增的程式碼可確保變數不會修改,因而可設為 const。 現在我們將引發診斷。 請在 AnalyzeNode 中新增下列程式碼,作為最後一行:

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

您可以按 F5 執行您的分析器,以查看進度。 您可以載入先前建立的主控台應用程式,然後新增下列測試程式碼:

int x = 0;
Console.WriteLine(x);

此時應該會出現燈泡,且您的分析器應會報告診斷。 不過,依您的 Visual Studio 版本而定,您會看到:

  • 燈泡仍使用範本產生的程式碼修正,將指出它可以改為大寫。
  • 編輯器頂端的橫幅訊息,指出「'MakeConstCodeFixProvider' 發生錯誤並已停用」。 這是因為程式碼修正提供者尚未變更,但仍預期會尋找 TypeDeclarationSyntax 項目,而不是 LocalDeclarationStatementSyntax 項目。

下一節會說明如何撰寫程式碼修正。

撰寫程式碼修正

分析器可提供一或多個程式碼修正。 程式碼修正可定義編輯,用以解決報告的問題。 對於您所建立的分析器,您可以提供插入 const 關鍵字的程式碼修正:

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

使用者可從編輯器和 Visual Studio 中的燈泡 UI 加以選擇,並變更程式碼。

開啟 CodeFixResources.resx 檔案,並將 CodeFixTitle 變更為 「Make constant」。

開啟範本所新增的 MakeConstCodeFixProvider.cs 檔案。 此程式碼修正已連線到您的診斷分析器所產生的診斷識別碼,但尚未實作正確的程式碼轉換。

接著,請刪除 MakeUppercaseAsync 方法。 該方法已不適用。

所有程式碼修正提供者皆衍生自 CodeFixProvider。 它們全都會覆寫 CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext),以報告可用的程式碼修正。 在 RegisterCodeFixesAsync 中,將您要搜尋的上階節點類型變更為符合診斷的 LocalDeclarationStatementSyntax

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

接著,變更最後一行以註冊程式碼修正。 您的修正將會建立在將 const 修飾詞新增至現有宣告後而產生的新文件:

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

在剛才新增程式碼中,您會在符號 MakeConstAsync 上看到紅色波浪線。 請為 MakeConstAsync 新增宣告,如下列程式碼所示:

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

新的 MakeConstAsync 方法會將代表使用者來源檔案的 Document 轉換為新的 Document,此時其中包含 const 宣告。

您可以建立要在宣告陳述式前面插入的新 const 關鍵字權杖。 請務必先從宣告陳述式的第一個權杖中移除任何前置邏輯,再將其附加至 const 權杖。 將下列程式碼加入 MakeConstAsync 方法:

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

接著,使用下列程式碼將 const 權杖新增宣告:

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

然後請將新的宣告格式化,以符合 C# 格式化規則。 將變更格式化以符合現有的程式碼,可產生更理想的體驗。 緊跟在現有程式碼後面新增下列陳述式:

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

此程式碼需要新的命名空間。 將下列 using 指示詞新增至檔案的頂端:

using Microsoft.CodeAnalysis.Formatting;

最後一個步驟是進行編輯。 此程序有三個步驟:

  1. 取得現有文件的控制代碼。
  2. 將現有宣告取代為新宣告,以建立新文件。
  3. 傳回新文件。

將下列程式碼新增至 MakeConstAsync 方法的結尾:

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

您的程式碼修正已可供試用。 按 F5,在 Visual Studio 的第二個執行個體中執行分析器專案。 在第二個 Visual Studio 執行個體中建立新的 C# 主控台應用程式專案,並將數個以常數值初始化的區域變數宣告新增至 Main 方法。 您會看到系統將其報告為警告,如下所示。

可設為常數警告

您已完成許多進度。 可設為 const 的宣告底下會出現波浪線。 但仍有工作尚待完成。 如果您依序以 ijk 的順序將 const 新增至宣告,則可正常運作。 但是,如果您以不同的順序從 k 開始新增 const 修飾詞,分析器將會產生錯誤:除非 ij 皆已為 const,否則 k 無法宣告為 const。 您必須執行更多分析,以確保能夠以不同的方式讓變數完成宣告和初始化。

建立單元測試

在可設為常數的宣告案例中,如果情況單純,您的分析器和程式碼修正就可正常運作。 但在許多宣告陳述式中,這項實作都有可能發生錯誤。 您將使用範本所撰寫的單元測試程式庫,來處理這些案例。 其速度會比重複開啟第二個 Visual Studio 複本快得多。

在單元測試專案中開啟 MakeConstUnitTests.cs 檔案。 範本依循分析器和程式碼修正單元測試的兩個常見模式,建立了兩項測試。 TestMethod1 顯示可確保分析器不會在不當時機報告診斷的測試模式。 TestMethod2 顯示會報告診斷並執行程式碼修正的模式。

此範本會使用 Microsoft.CodeAnalysis.Testing 套件進行單元測試。

提示

測試程式庫支援特殊的標記語法,包括下列項目:

  • [|text|]:表示 text 回報的診斷。 根據預設,此表單只能用於測試分析器,其中只由 DiagnosticAnalyzer.SupportedDiagnostics 提供一個 DiagnosticDescriptor
  • {|ExpectedDiagnosticId:text|}:表示 text 回報的 IdExpectedDiagnosticId 診斷。

以下列測試方法取代 MakeConstUnitTest 類別中的範本測試:

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

執行此測試以確定其通過。 在 Visual Studio 中選取 [測試]>[視窗]>[測試總管],以開啟 [測試總管]。 然後,選取 [全部執行]。

建立有效宣告的測試

一般而言,分析器應盡快結束,而執行最少量的工作。 Visual Studio 會以使用者編輯程式碼的形式呼叫已註冊的分析器。 回應能力是關鍵需求。 不應引發診斷的程式碼有數個測試案例。 您的分析器已處理其中一個測試,也就是變數會在初始化後進行指派的案例。 新增下列測試方法來代表該案例:

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }

這項測試也會通過。 接著,為尚未處理的狀況新增常數:

  • 因為已是常數,而已經是 const 的宣告:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • 因為沒有可使用的值,而沒有初始設定式的宣告:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • 因為不可以是編譯時期常數,因此初始設定式不是常數的宣告:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

如果 C# 允許以多個宣告作為一個陳述式,情況可能會更複雜。 請考慮使用下列測試案例字串常數:

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

變數 i 可設為常數,但變數 j 不可。 因此,此陳述式不可設為常數宣告。

重新執行您的測試,您將會發現這些新的測試案例失敗。

更新分析器以忽略正確宣告

您需要為分析器的 AnalyzeNode 方法加入某些功能,以篩選出符合這些條件的程式碼。 這些全都是相關條件,因此類似的變更將會修正所有的此類條件。 對 AnalyzeNode 進行下列變更:

  • 您的語意分析檢查了單一變數宣告。 此程式碼必須位於會對在相同陳述式中宣告的所有變數進行檢查的 foreach 迴圈中。
  • 每個宣告的變數都必須有初始設定式。
  • 每個宣告變數的初始設定式都必須是編譯時期常數。

在您的 AnalyzeNode 方法中,替換掉原始的語意分析:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

取代為下列程式碼片段:

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

第一個 foreach 迴圈會使用語法分析檢查每個變數宣告。 第一次檢查可確保變數具有初始設定式。 第二次檢查可確保初始設定式是常數。 第二個迴圈具有原始的語意分析。 語意檢查位於個別的迴圈中,因為它對效能有較大的影響。 重新執行測試後,您應該會看到測試全部通過。

新增最終修改

就快要完成了。 您的分析器還有幾種條件需要處理。 使用者撰寫程式碼時,Visual Studio 會呼叫分析器。 經呼叫的分析器常用來處理未編譯的程式碼。 診斷分析器的 AnalyzeNode 方法不會檢查常數值是否可轉換成變數類型。 因此,目前的實作會直接將不正確的宣告 (例如 int i = "abc") 轉換為區域常數。 為此案例新增測試方法:

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

此外,參考類型未正確處理。 參考類型唯一允許的常數值為 null;案例中的例外為 System.String,它可允許字串常值。 換句話說,const string s = "abc" 是合法的,const object s = "abc" 則否。 下列程式碼片段會驗證該條件:

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

為求周密,您必須新增另一個測試,以確定您可以建立字串的常數宣告。 下列程式碼片段會定義引發診斷的程式碼和套用修正後的程式碼:

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

最後,如果以 var 關鍵字宣告變數,程式碼修正將執行錯誤的動作並產生 const var 宣告,但 C# 語言並不加以支援。 若要修正此 Bug,程式碼修正必須將 var 關鍵字取代為推斷的類型名稱:

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

幸運的是,上述所有 Bug 都可以使用您剛才學習的技術來解決。

若要修正第一個 Bug,請先開啟 MakeConstAnalyzer.cs,並找出會檢查每個區域宣告的初始設定式以確保已為其指派常數值的 foreach 迴圈。 在即將執行第一個 foreach 迴圈之前,呼叫 context.SemanticModel.GetTypeInfo() 以擷取與區域宣告的宣告類型有關的詳細資訊:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

然後,在您的 foreach 迴圈中檢查每個初始設定式,以確定它可轉換成變數類型。 確定初始設定式是常數之後,請新增下列檢查:

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

後續變更會以上一次的變更為建置基礎。 在第一個 foreach 迴圈右大括號前面新增下列程式碼,以在常數為字串或 Null 時檢查區域宣告的類型。

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

您必須在程式碼修正提供者中撰寫更多程式碼,以將 var 的關鍵字取代為正確的類型名稱。 返回 MakeConstCodeFixProvider.cs。 您將新增的程式碼會執行下列步驟:

  • 檢查宣告是否為 var 宣告,如果是則:
  • 為推斷的類型建立新類型。
  • 確定類型宣告不是別名。 若是如此,則可以宣告 const var
  • 確定 var 不是此程式中的類型名稱。 (若是如此,則 const var 合法)。
  • 簡化完整類型名稱

程式碼看似不少。 其實並不然。 請將宣告和初始化 newLocal 的那一行取代為下列程式碼。 它會 newModifiers 初始化之後立即執行:

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

您必須新增一個 using 指示詞才能使用 Simplifier 類型:

using Microsoft.CodeAnalysis.Simplification;

執行測試,應該全部都會通過。 請執行您完成的分析器,這值得自傲。 按 Ctrl+F5,在已載入 Roslyn Preview 延伸模組的第二個 Visual Studio 執行個體中執行分析器專案。

  • 在第二個 Visual Studio 執行個體中建立新的 C# 主控台應用程式專案,並將 int x = "abc"; 新增至 Main 方法。 由於有第一個 Bug 修正,系統應該不會針對此區域變數宣告發出警告 (雖然如預期發生了編譯器錯誤)。
  • 接著,將 object s = "abc"; 新增至 Main 方法。 由於有第二個 Bug 修正,應該不會出現警告。
  • 最後,新增另一個使用 var 關鍵字的區域變數。 您會看到回報的警告,且左下方會出現建議。
  • 請將編輯器插入號移至波浪底線上方,然後按 Ctrl+。 以顯示建議的程式碼修正。 選取您的程式碼修正時,請留意 var 的關鍵字現在已可正確處理。

最後,新增下列程式碼:

int i = 2;
int j = 32;
int k = i + j;

完成這些變更後,將只有前兩個變數上會有紅色波浪線。 將 const 新增至 ij 後,將會出現新的 k 警告,因為它現在已可為 const

恭喜! 您已建立第一個 .NET Compiler Platform 延伸模組,可執行即時程式碼分析以偵測問題,並提供快速修正加以更正。 在本文中,您已認識 .NET Compiler Platform SDK (Roslyn API) 所包含的多種程式碼 API。 您可以根據範例 GitHub 存放庫中已完成的範例檢查您的工作。

其他資源