.NET 編譯程式平臺 SDK 提供建立自定義診斷(分析器)、程式代碼修正、程式代碼重構,以及以 C# 或 Visual Basic 程式代碼為目標的診斷隱藏器所需的工具。 分析器包含可辨識規則違規的程序代碼。 您的 程式代碼修正 包含修正違規的程序代碼。 您實作的規則可以涵蓋從程式代碼結構到程式風格、命名慣例,以及更多。 .NET 編譯程式平臺提供執行分析的架構,因為開發人員正在撰寫程式代碼,以及所有用於修正程序代碼的 Visual Studio UI 功能:在編輯器中顯示波浪線、填入 Visual Studio 錯誤清單、建立「燈泡」建議,以及顯示建議修正的豐富預覽。
在本教學課程中,您將探索如何使用 Roslyn API 建立 分析器和 隨附的程式 代碼修正 。 分析器是執行原始程式碼分析和向使用者回報問題的方法。 選擇性地,可以將程式碼修正與分析器相關聯,以表示使用者原始程式碼的修改。 本教學課程會建立一個分析器,以尋找可以使用 const 修飾詞宣告但尚未使用的局部變數宣告。 隨附的程式代碼修正會修改這些宣告以新增 const 修飾詞。
先決條件
- Visual Studio 2019 16.8 版或更新版本
您必須透過 Visual Studio 安裝程式安裝 .NET 編譯程式平臺 SDK :
安裝指示 - Visual Studio 安裝程式
在 Visual Studio 安裝程式中尋找 .NET 編譯程序平臺 SDK 有兩種不同的方式:
使用 Visual Studio 安裝程式進行安裝 - 工作負載檢視
.NET 編譯程式平臺 SDK 不會自動選取為 Visual Studio 延伸模塊開發工作負載的一部分。 您必須將其選取為選擇性元件。
- 執行 Visual Studio 安裝程式
- 選取 [修改]
- 檢查 Visual Studio 延伸模組開發 工作負載。
- 在摘要樹狀結構中開啟 Visual Studio延伸模組開發 節點。
- 勾選 .NET 編譯器平台 SDK 的方塊。 您會在可選元件的最下面找到它。
您可以選擇性地讓 DGML 編輯器 在視覺化檢視中顯示圖形:
- 開啟摘要樹狀目錄中的 [個別元件 ] 節點。
- 勾選 DGML 編輯器 方塊
使用 Visual Studio 安裝程式,在「個別元件」索引標籤中進行安裝
- 執行 Visual Studio 安裝程式
- 選取 [修改]
- 選擇 [個別元件] 索引標籤
- 勾選 .NET 編譯器平台 SDK 的方塊。 您會在 [ 編譯程式]、[建置工具和運行時間] 區 段下方的頂端找到它。
您可以選擇性地讓 DGML 編輯器 在視覺化檢視中顯示圖形:
- 勾選 DGML 編輯器 的方框。 您會在 [ 程序代碼工具 ] 區段底下找到它。
建立及驗證分析器有數個步驟:
- 建立方案。
- 註冊分析器名稱和描述。
- 報告分析器警告和建議。
- 實作程式代碼修正以接受建議。
- 透過單元測試改善分析。
建立解決方案
- 在 Visual Studio 中,選擇 [ 檔案 > 新 > 專案... ] 以顯示 [新增專案] 對話框。
- 在 [Visual C# > 擴充性] 底下,選擇 [具有程式代碼修正的分析器][.NET Standard]。
- 將專案命名為 「MakeConst」,然後按兩下 [確定]。
備註
您可能會收到編譯錯誤 (MSB4062:無法載入 “CompareBuildTaskVersion” 工作)。 若要修正此問題,請使用 NuGet 套件管理員更新方案中的 NuGet 套件,或在 [套件管理員主控台] 視窗中使用 Update-Package 。
探索分析器範本
具有程式代碼修正範本的分析器會建立五個專案:
- MakeConst,其中包含分析器。
- MakeConst.Code Fix,其中包含程序代碼修正。
- 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,您必須從 \Microsoft\VisualStudio%LocalAppData% 手動將其刪除。 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 編譯程序平臺提供 API,讓您更輕鬆地執行此分析。
建立分析器註冊
範本會在 DiagnosticAnalyzer 檔案中建立初始類別。 此初始分析器會顯示每個分析器的兩個重要屬性。
- 每個診斷分析器都必須提供
[DiagnosticAnalyzer]描述其運作語言的屬性。 - 每個診斷分析器都必須從 DiagnosticAnalyzer 類別衍生 (直接或間接) 。
此樣本也會顯示屬於任何分析器一部分的基本功能:
- 記錄操作。 這些動作代表程式代碼變更,應該觸發分析器來檢查是否有違規的程序代碼。 當 Visual Studio 偵測到符合已註冊動作的程式代碼編輯時,它會呼叫分析器的已註冊方法。
- 建立診斷。 當您的分析器偵測到違規時,它會建立 Visual Studio 用來通知使用者違規的診斷物件。
您會在 方法的 DiagnosticAnalyzer.Initialize(AnalysisContext) 覆寫中註冊動作。 在本教學課程中,您將瀏覽 語法節點,尋找本機宣告,並查看其中哪些具有常數值。 如果宣告可以是常數,您的分析器將會建立並報告診斷。
第一個步驟是更新註冊常數和 Initialize 方法,讓這些常數指出您的「Make Const」分析器。 大部分的字串常數都是在字串資源檔中定義。 您應該遵循這種做法,以便更輕鬆地進行在地化。 開啟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)
{
}
將 Category 中的 Usage 變更為 「」,如下列程式代碼所示:
private const string Category = "Usage";
尋找可能為 const 的本機宣告
是時候撰寫方法的第一個版本 AnalyzeNode 了。 它應該尋找可能是 const 但並非如此的單一本地宣告,如下列程式代碼所示:
int x = 0;
Console.WriteLine(x);
第一個步驟是尋找程式本地宣告。 將下列程式代碼新增至 AnalyzeNodeMakeConstAnalyzer.cs:
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 自變數來判斷是否可以進行 const局部變數宣告。
Microsoft.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);
使用者從編輯器中的燈泡 UI 中選擇它,Visual Studio 會變更程式代碼。
開啟 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;
最後一個步驟是進行編輯。 此程式有三個步驟:
- 取得現有文件的句柄。
- 將現有的宣告取代為新的宣告,以建立新的檔。
- 傳回新檔。
將下列程式代碼新增至 方法的 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。 但仍有工作要做。 如果您在宣告中首先新增const,接著從i開始,然後是j,最後是k,這可正常運作。 但是,如果您以不同的順序新增 const 修飾詞,從 k開始,您的分析器就會產生錯誤:除非 k 和 const 都已經i,否則 j 無法被宣告為 const。 您必須執行更多分析,以確保您可以以不同的方式宣告和初始化變數。
建立單元測試
您的分析器和程式代碼修正適用於可設為 const 的單一宣告簡單案例。 有許多可能的宣告陳述,在此實作中可能出錯。 您將使用範本編寫的單元測試庫來處理這些案例。 其速度比重複開啟第二個 Visual Studio 複本快得多。
在單元測試項目中開啟 MakeConstUnitTests.cs 檔案。 此範本已建立兩個測試,以遵循分析器和程式代碼修正單元測試的兩個常見模式。
TestMethod1 顯示確保分析器不在不應該回報診斷時回報的測試模式。
TestMethod2 顯示報告診斷和執行程式代碼修正的模式。
此範本會使用 Microsoft.CodeAnalysis.Testing 套件來進行單元測試。
小提示
測試庫支援特殊的標記語法,包括以下內容:
-
[|text|]:表示診斷已針對text進行回報。 預設情況下,此表單僅可用於測試分析器,這些分析器需要由DiagnosticDescriptor提供且僅包含一個DiagnosticAnalyzer.SupportedDiagnostics。 -
{|ExpectedDiagnosticId:text|}:表示針對 Id 回報具有ExpectedDiagnosticIdtext的診斷。
使用下列測試方法取代 類別中的 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++);
}
}
");
}
[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);
}
}
");
}
這些測試會通過,因為您的分析器已經處理這些條件:
- 數據流分析偵測到初始化之後指派的變數。
-
const宣告已藉由檢查是否含有const關鍵詞而被篩選掉。 - 沒有初始化表達式的宣告是由偵測宣告外部指派的數據流分析所處理。
接下來,新增尚未處理之條件的測試方法:
宣告(初始值不是常數),因為它不能是編譯時間常數:
[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 不能。 因此,這個敘述不能當作 const 宣告。
再次執行測試,您會看到最後兩個測試案例失敗。
更新分析器以忽略正確的宣告
您需要對分析器的 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# 語言不支援的宣告。 若要修正此錯誤,程式代碼修正必須以推斷的類型名稱取代 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 都可以使用您剛才學到的相同技術來解決。
若要修正第一個錯誤,請先開啟 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# 主控台應用程式專案,並在 Main 方法中加入
int x = "abc";。 由於第一個錯誤修正,此局部變數宣告不應該回報任何警告(雖然編譯程序錯誤如預期般)。 - 接下來,將 新增
object s = "abc";至 Main 方法。 由於第二個錯誤修正,因此不應報告任何警告。 - 最後,新增另一個使用 關鍵詞的
var局部變數。 您會注意到有警告被顯示,而建議會出現在左下方。 - 將編輯器游標移至波浪線上方,然後按 Ctrl+.。 顯示建議的程式碼修復。 選取程式代碼修正時,請注意
var關鍵詞現在已正確處理。
最後,新增下列程式代碼:
int i = 2;
int j = 32;
int k = i + j;
在這些變更之後,您只會在前兩個變數上取得紅色波浪線。 將 const 新增至 i 和 j ,您會在 k 收到新的警告,因為它現在可以是 const。
祝賀! 您已建立第一個執行即時程式代碼分析的 .NET 編譯程序平臺延伸模組,以偵測問題並提供快速修正來修正問題。 一路上,您已了解許多屬於 .NET 編譯程序平臺 SDK (Roslyn API) 的程式代碼 API。 您可以針對範例 GitHub 存放庫中 已完成的範例 檢查您的工作。