開始使用語意分析
本教學課程假設您熟悉 Syntax API。 開始使用語意分析一文提供充分的簡介。
在本教學課程中,您會探索 Symbol 和 Binding API。 這些 API 提供程式的語意相關資訊。 它們可讓您詢問和回答程式中任何符號所表示之類型的問題。
您必須安裝 .NET Compiler Platform SDK:
安裝指示 - Visual Studio 安裝程式
有兩種不同方式可以在 [Visual Studio 安裝程式] 中尋找 [.NET Compiler Platform SDK]:
使用 Visual Studio 安裝程式安裝 - 工作負載檢視
不會自動選取 .NET Compiler Platform SDK 作為 Visual Studio 延伸模組開發工作負載的一部分。 您必須選取它作為為選用元件。
- 執行 [Visual Studio 安裝程式]
- 選取 [修改]
- 核取 [Visual Studio 延伸模組開發]。
- 在摘要樹狀結構中開啟 [Visual Studio 延伸模組開發] 節點。
- 核取 [.NET Compiler Platform SDK] 的方塊。 您將在選用元件下的最後處找到它。
(選擇性) 您可能也需要 [DGML 編輯器] 以在視覺化檢視中顯示圖形:
- 在摘要樹狀結構中開啟 [個別元件] 節點。
- 核取 [DGML 編輯器] 的方塊
使用 Visual Studio 安裝程式安裝 - [個別元件] 索引標籤
- 執行 [Visual Studio 安裝程式]
- 選取 [修改]
- 選取 [個別元件] 索引標籤
- 核取 [.NET Compiler Platform SDK] 的方塊。 您將在 [編譯器、建置工具與執行階段] 區段下的頂端找到它。
(選擇性) 您可能也需要 [DGML 編輯器] 以在視覺化檢視中顯示圖形:
- 核取 [DGML 編輯器] 的方塊。 您將在 [程式碼工具] 區段下找到它。
了解編譯和符號
更深入處理 .NET Compiler SDK 時,就會熟悉 Syntax API 與 Semantic API 之間的差異。 Syntax API 可讓您查看程式的「結構」。 不過,您通常需要程式的更豐富語意資訊或「意義」。 不嚴密的 Visual Studio 或 C# 程式碼檔案或程式碼片段雖能單獨執行語意分析,但在隔離的環境中,詢問 "what's the type of this variable" 這類問題沒有意義。 類型名稱的意義可能依存於組件參考、命名空間匯入或其他程式碼檔。 這些問題是使用 Semantic API 來回答,具體來說是 Microsoft.CodeAnalysis.Compilation 類別。
Compilation 執行個體類似編譯器所看到的單一專案,並且代表編譯 Visual Basic 或 C# 程式所需的所有項目。 編譯包含要編譯的一組來源檔案、組件參考及編譯器選項。 您可以使用此內容中的所有其他資訊來理解程式碼的意義。 Compilation 可讓您尋找符號 - 實體,例如名稱和其他運算式所參照的類型、命名空間、成員和變數。 將名稱與具有符號的運算式建立關聯的程序稱為繫結。
與 Microsoft.CodeAnalysis.SyntaxTree 類似,Compilation 是具有語言特定衍生的抽象類別。 建立編譯執行個體時,您必須在 Microsoft.CodeAnalysis.CSharp.CSharpCompilation (或 Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation) 類別上叫用 Factory 方法。
查詢符號
在本教學課程中,您會重新看到 "Hello World" 程式。 此時,您查詢程式中的符號來了解這些符號所代表的類型。 您查詢命名空間中的類型,並學習如何尋找類型上可用的方法。
您可以在 GitHub 存放庫中查看此範例中完成的程式碼。
注意
語法樹狀結構類型使用繼承,來描述適用於程式中不同位置的不同語法項目。 使用這些 API 通常表示將屬性或集合成員轉換成特定衍生類型。 在下列範例中,指派和轉換是使用明確類型變數的個別陳述式。 您可以閱讀程式碼,以查看 API 的傳回型別以及所傳回物件的執行階段類型。 在實務上,較常見使用隱含型別變數,並依賴 API 名稱來描述要檢查的物件類型。
建立新的 C# 獨立程式碼分析工具專案:
- 在 Visual Studio 中,選擇 [檔案]>[新增]>[專案] 來顯示 [新增專案] 對話方塊。
- 在 Visual C#>擴充性下,選擇 [獨立程式碼分析工具]。
- 將專案命名為 "SemanticQuickStart",然後按一下 [確定]。
您應先分析之前顯示的基本 "Hello World!" 程式。
將 Hello World 程式的文字新增為 Program
類別中的常數:
const string programText =
@"using System;
using System.Collections.Generic;
using System.Text;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
}";
接下來,新增下列程式碼來建置 programText
常數中程式碼文字的語法樹狀結構。 請將下列這一行新增到您的 Main
方法:
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();
接下來,從您已建立的樹狀結構建置 CSharpCompilation。 "Hello World" 範例依賴 String 和 Console 類型。 您需要參考在編譯中宣告這兩種類型的組件。 將下行新增至 Main
方法,以建立語法樹狀結構的編譯,包含適當組件的參考:
var compilation = CSharpCompilation.Create("HelloWorld")
.AddReferences(MetadataReference.CreateFromFile(
typeof(string).Assembly.Location))
.AddSyntaxTrees(tree);
CSharpCompilation.AddReferences 方法會新增編譯參考。 MetadataReference.CreateFromFile 方法會將組件載入為參考。
查詢語意模型
具有 Compilation 之後,您可以要求該 Compilation 中所含之任何 SyntaxTree 的 SemanticModel。 您可以將語意模型視為通常取自 intellisense 之所有資訊的來源。 SemanticModel 可回答像是 "What names are in scope at this location?"、"What members are accessible from this method?"、"What variables are used in this block of text?" 及 "What does this name/expression refer to?" 等問題。您可以新增下列陳述式,以建立語意模型:
SemanticModel model = compilation.GetSemanticModel(tree);
繫結名稱
Compilation 會從 SyntaxTree 建立 SemanticModel。 建立模型之後,您可以查詢它來尋找第一個 using
指示詞,並擷取 System
命名空間的符號資訊。 將這兩行新增至 Main
方法來建立語意模型,並擷取第一個 using 陳述式的符號:
// Use the syntax tree to find "using System;"
UsingDirectiveSyntax usingSystem = root.Usings[0];
NameSyntax systemName = usingSystem.Name;
// Use the semantic model for symbol information:
SymbolInfo nameInfo = model.GetSymbolInfo(systemName);
上述程式碼示範了如何在第一個 using
指示詞中繫結名稱,以為 System
命名空間擷取 Microsoft.CodeAnalysis.SymbolInfo。 上述程式碼也會說明您使用語法模型來尋找程式碼結構;您使用語法模型來了解其意義。 語法模型會在 using 陳述式中尋找字串 System
。 語法模型具有 System
命名空間中所定義類型的所有資訊。
從 SymbolInfo 物件,可以使用 SymbolInfo.Symbol 屬性來取得 Microsoft.CodeAnalysis.ISymbol。 此屬性會傳回這個運算式所參照的符號。 針對未參照任何項目的運算式 (例如數值常值),此屬性為 null
。 SymbolInfo.Symbol 不是 Null 時,ISymbol.Kind 表示符號的類型。 在此範例中,ISymbol.Kind 屬性為 SymbolKind.Namespace。 將下列程式碼新增至 Main
方法。 它會擷取 System
命名空間的符號,然後顯示 System
命名空間中所宣告的所有子命名空間:
var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
{
Console.WriteLine(ns);
}
}
執行程式,而且您應該會看到下列輸出:
System.Collections
System.Configuration
System.Deployment
System.Diagnostics
System.Globalization
System.IO
System.Numerics
System.Reflection
System.Resources
System.Runtime
System.Security
System.StubHelpers
System.Text
System.Threading
Press any key to continue . . .
注意
輸出未包含為 System
命名空間之子命名空間的每個命名空間。 它會顯示存在於此編譯中的每個命名空間,而這些命名空間只會參考宣告 System.String
的組件。 此編譯不知道其他組件中所宣告的任何命名空間
繫結運算式
上述程式碼示範如何繫結至名稱來尋找符號。 C# 程式中具有可繫結且不是名稱的其他運算式。 若要示範這項功能,請存取與簡單字串常值的繫結。
"Hello World" 程式含 Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax,"Hello World!" 字串會顯示在主控台上。
只要在程式中,尋找單一字串常值,就能找到 "Hello World!" 字串。 然後,在找到語法節點之後,取得語意模型中該節點的類型資訊。 將下列程式碼新增至 Main
方法:
// Use the syntax model to find the literal string:
LiteralExpressionSyntax helloWorldString = root.DescendantNodes()
.OfType<LiteralExpressionSyntax>()
.Single();
// Use the semantic model for type information:
TypeInfo literalInfo = model.GetTypeInfo(helloWorldString);
Microsoft.CodeAnalysis.TypeInfo 結構包含 TypeInfo.Type 屬性,可讓您存取常值型別的相關語意資訊。 在此範例中,這是 string
類型。 新增將此屬性指派給區域變數的宣告:
var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;
若要完成本教學課程,請建置 LINQ 查詢,以建立傳回 string
之 string
類型上所宣告的一系列所有公用方法。 此查詢過於複雜,因此請逐行建置它,然後將它重新建構為單一查詢。 此查詢的來源是 string
類型上所宣告的所有成員序列:
var allMembers = stringTypeSymbol?.GetMembers();
該來源序列包含所有成員 (包含屬性和欄位),因此使用 ImmutableArray<T>.OfType 方法進行篩選來尋找本身為 Microsoft.CodeAnalysis.IMethodSymbol 物件的項目:
var methods = allMembers?.OfType<IMethodSymbol>();
接下來,新增另一個篩選只傳回為公用並傳回 string
的方法:
var publicStringReturningMethods = methods?
.Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
m.DeclaredAccessibility == Accessibility.Public);
僅選取 name 屬性,並透過移除任何多載,僅選取不同的名稱:
var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();
您也可以使用 LINQ 查詢語法建置完整的查詢,然後在主控台上顯示所有的方法名稱:
foreach (string name in (from method in stringTypeSymbol?
.GetMembers().OfType<IMethodSymbol>()
where SymbolEqualityComparer.Default.Equals(method.ReturnType, stringTypeSymbol) &&
method.DeclaredAccessibility == Accessibility.Public
select method.Name).Distinct())
{
Console.WriteLine(name);
}
建置並執行程式。 您應該會看見下列輸出:
Join
Substring
Trim
TrimStart
TrimEnd
Normalize
PadLeft
PadRight
ToLower
ToLowerInvariant
ToUpper
ToUpperInvariant
ToString
Insert
Replace
Remove
Format
Copy
Concat
Intern
IsInterned
Press any key to continue . . .
您已使用 Semantic API 來尋找並顯示屬於此程式之符號的相關資訊。