開始使用語意分析

本教學課程假設您熟悉 Syntax API。 開始使用語意分析一文提供充分的簡介。

在本教學課程中,您會探索 SymbolBinding API。 這些 API 提供程式的語意相關資訊。 它們可讓您詢問和回答程式中任何符號所表示之類型的問題。

您必須安裝 .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 編輯器] 的方塊。 您將在 [程式碼工具] 區段下找到它。

了解編譯和符號

更深入處理 .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" 範例依賴 StringConsole 類型。 您需要參考在編譯中宣告這兩種類型的組件。 將下行新增至 Main 方法,以建立語法樹狀結構的編譯,包含適當組件的參考:

var compilation = CSharpCompilation.Create("HelloWorld")
    .AddReferences(MetadataReference.CreateFromFile(
        typeof(string).Assembly.Location))
    .AddSyntaxTrees(tree);

CSharpCompilation.AddReferences 方法會新增編譯參考。 MetadataReference.CreateFromFile 方法會將組件載入為參考。

查詢語意模型

具有 Compilation 之後,您可以要求該 Compilation 中所含之任何 SyntaxTreeSemanticModel。 您可以將語意模型視為通常取自 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。 此屬性會傳回這個運算式所參照的符號。 針對未參照任何項目的運算式 (例如數值常值),此屬性為 nullSymbolInfo.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 查詢,以建立傳回 stringstring 類型上所宣告的一系列所有公用方法。 此查詢過於複雜,因此請逐行建置它,然後將它重新建構為單一查詢。 此查詢的來源是 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 來尋找並顯示屬於此程式之符號的相關資訊。