Freigeben über


Erste Schritte mit der semantischen Analyse

Bei diesem Tutorial wird davon ausgegangen, dass Sie mit der Syntax-API vertraut sind. Der Artikel Erste Schritte mit der Syntaxanalyse bietet eine Einführung zu diesem Thema.

Im vorliegenden Tutorial erkunden Sie die APIs für Symbol und Bindung. Diese APIs bieten Informationen zur semantischen Bedeutung eines Programms. Sie ermöglichen es Ihnen, Fragen zu den Typen zu stellen und zu beantworten, die durch ein Symbol in Ihrem Programm dargestellt werden.

Die Installation des SDK für die .NET Compiler Platform ist erforderlich.

Installationsanweisungen: Visual Studio-Installer

Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu finden:

Installation mithilfe des Visual Studio-Installers: Workloads im Überblick

Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.

  1. Führen Sie den Visual Studio-Installer aus.
  2. Wählen Sie Ändern aus.
  3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung.
  4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
  5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK. Sie finden es an letzter Stelle unter den optionalen Komponenten.

Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:

  1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
  2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor.

Installation mithilfe des Visual Studio-Installers: Registerkarte „Einzelne Komponenten“

  1. Führen Sie den Visual Studio-Installer aus.
  2. Wählen Sie Ändern aus.
  3. Klicken Sie auf die Registerkarte Einzelne Komponenten.
  4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK. Sie finden es an oberster Stelle im Abschnitt Compiler, Buildtools und Laufzeiten.

Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:

  1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor. Sie finden es im Abschnitt Codetools.

Grundlegendes zu Kompilierungen und Symbolen

Wenn Sie eine Weile mit dem .NET Compiler SDK arbeiten, werden Ihnen die Unterschiede zwischen der Syntax-API und der Semantik-API immer vertrauter. Die Syntax-API ermöglicht es Ihnen, die Struktur eines Programms zu untersuchen. In vielen Fällen benötigen Sie jedoch weitere Informationen zur Semantik bzw. Bedeutung eines Programms. Sie können zwar eine einzelne, losgelöste Codedatei oder einen isolierten Visual Basic- oder C#-Codeausschnitt syntaktisch analysieren, aber es ist wenig sinnvoll, Fragen wie „Welchen Typ weist diese Variable auf?“ sozusagen im luftleeren Raum zu stellen. Die Bedeutung eines Typnamens kann von Assemblyverweisen, Namespaceimporten oder anderen Codedateien abhängig sein. Diese Fragen werden mithilfe der Semantik-API, insbesondere der Microsoft.CodeAnalysis.Compilation-Klasse, beantwortet.

Eine Instanz von Compilation entspricht einem einzelnen Projekt aus Sicht des Compilers und repräsentiert alle Elemente, die zum Kompilieren eines Visual Basic- oder C#-Programms erforderlich sind. Die Kompilierung umfasst die Quelldateien, die kompiliert werden sollen, sowie Assemblyverweise und Compileroptionen. Sie können alle weiteren Informationen in diesem Kontext heranziehen, um die Bedeutung des Codes genau zu verstehen. Eine Compilation ermöglicht es Ihnen, Symbole zu suchen: Entitäten wie z.B. Typen, Namespaces, Member und Variablen, auf die Namen und andere Ausdrücke verweisen. Der Prozess der Verknüpfung von Namen und Ausdrücken mit Symbolen wird als Bindung bezeichnet.

Ebenso wie Microsoft.CodeAnalysis.SyntaxTree ist Compilation eine abstrakte Klasse mit sprachspezifischen Ableitungen. Wenn Sie eine Instanz von „Compilation“ erstellen, müssen Sie eine Factorymethode in der Klasse Microsoft.CodeAnalysis.CSharp.CSharpCompilation (oder Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation) aufrufen.

Abfragen von Symbolen

In diesem Tutorial betrachten wir noch einmal das Hello World-Programm. Diesmal fragen Sie die Symbole im Programm ab, um zu verstehen, welche Typen durch diese Symbole repräsentiert werden. Sie fragen die Typen in einem Namespace ab und lernen, wie Sie die verfügbaren Methoden in einem Typ finden.

Den fertig gestellten Code für dieses Beispiel finden Sie in unserem GitHub-Repository.

Hinweis

Die Syntaxstrukturtypen verwenden Vererbung, um die verschiedenen Syntaxelemente zu beschreiben, die an verschiedenen Positionen im Programm gültig sind. Bei der Verwendung dieser APIs müssen häufig Eigenschaften oder Sammlungsmember in bestimmte abgeleitete Typen umgewandelt werden. In den folgenden Beispielen sind die Zuweisung und die Umwandlung separate Anweisungen, bei denen explizit typisierte Variablen verwendet werden. Sie können den Code lesen, um die Rückgabetypen der API und den Laufzeittyp der zurückgegebenen Objekte zu sehen. In der Praxis ist es eher üblich, implizit typisierte Variablen zu verwenden und die Typen der zu untersuchenden Objekte mithilfe von API-Namen zu beschreiben.

Erstellen Sie ein neues Stand-Alone Code Analysis Tool-Projekt für C#:

  • Wählen Sie in Visual Studio Datei>Neu>Projekt aus, um das Dialogfeld „Neues Projekt“ anzuzeigen.
  • Wählen Sie unter Visual C#>Erweiterbarkeit die Option Stand-Alone Code Analysis Tool aus.
  • Nennen Sie Ihr Projekt SemanticQuickStart, und klicken Sie auf „OK“.

Sie werden das oben gezeigte einfache Programm „Hello World!“ analysieren. Fügen Sie den Text für das Hello World-Programm als Konstante in Ihre Program-Klasse ein:

        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!"");
        }
    }
}";

Anschließend fügen Sie den folgenden Code hinzu, um die Syntaxstruktur für den Codetext in der programText-Konstante zu erstellen. Fügen Sie Ihrer Main-Methode die folgende Zeile hinzu:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);

CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Anschließend erstellen Sie aus der bereits erstellten Struktur eine CSharpCompilation. Das Hello World-Beispiel basiert auf den Typen String und Console. Sie müssen auf die Assembly verweisen, die diese beiden Typen in Ihrer Kompilierung deklariert. Fügen Sie Ihrer Main-Methode folgende Zeile hinzu, um eine Kompilierung Ihrer Syntaxstruktur einschließlich des Verweises auf die entsprechende Assembly zu erstellen:

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

Die CSharpCompilation.AddReferences-Methode fügt Verweise zur Kompilierung hinzu. Die MetadataReference.CreateFromFile-Methode lädt eine Assembly als Verweis.

Abfragen des semantischen Modells

Sobald Sie über eine Compilation verfügen, können Sie diese nach einem SemanticModel für jede SyntaxTree in dieser Compilation abfragen. Sie können sich das Semantikmodell als die Quelle aller Informationen vorstellen, die Sie normalerweise aus IntelliSense erhalten. Ein SemanticModel kann Fragen wie die folgenden beantworten: „Welche Namen befinden sich an diesem Ort im Gültigkeitsbereich?“, „Auf welche Member kann von dieser Methode aus zugegriffen werden?“, „Welche Variablen werden in diesem Textblock verwendet?“ und „Vorauf verweist dieser Name/dieser Ausdruck?“. Fügen Sie diese Anweisung hinzu, um das semantische Modell zu erstellen:

SemanticModel model = compilation.GetSemanticModel(tree);

Binden eines Namens

Compilation erstellt SemanticModel aus SyntaxTree. Nach dem Erstellen des Modells können Sie es abfragen, um die erste using-Direktive zu suchen und die Symbolinformationen für den System-Namespace abzurufen. Fügen Sie die folgenden beiden Zeilen zu Ihrer Main-Methode hinzu, um das Semantikmodell zu erstellen und das Symbol für die erste using-Anweisung abzurufen:

// 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);

Nachdem das Modell abgerufen wurde, wird der Name in der ersten using-Anweisung gebunden, um eine Microsoft.CodeAnalysis.SymbolInfo für den System-Namespace abzurufen. Dieser Code verdeutlicht auch, dass Sie das Syntaxmodell verwenden, um die Struktur des Codes zu ermitteln. Das Semantikmodell verwenden Sie, um die Bedeutung des Codes zu verstehen. Das Syntaxmodell sucht die Zeichenfolge System in der using-Anweisung. Das Semantikmodell verfügt über alle Informationen zu den im System-Namespace definierten Typen.

Aus dem SymbolInfo-Objekt können Sie mithilfe der SymbolInfo.Symbol-Eigenschaft das Microsoft.CodeAnalysis.ISymbol abrufen. Diese Eigenschaft gibt das Symbol zurück, auf das dieser Ausdruck verweist. Bei Ausdrücken, die auf nichts verweisen (wie z.B. numerische Literale), lautet diese Eigenschaft null. Wenn SymbolInfo.Symbol nicht null ist, bezeichnet ISymbol.Kind den Typ des Symbols. In diesem Beispiel ist die ISymbol.Kind-Eigenschaft ein SymbolKind.Namespace. Fügen Sie der Main-Methode den folgenden Code hinzu. Der Code ruft das Symbol für den System-Namespace ab und zeigt alle untergeordneten Namespaces an, die im System-Namespace deklariert sind:

var systemSymbol = (INamespaceSymbol?)nameInfo.Symbol;
if (systemSymbol?.GetNamespaceMembers() is not null)
{
    foreach (INamespaceSymbol ns in systemSymbol?.GetNamespaceMembers()!)
    {
        Console.WriteLine(ns);
    }
}

Führen Sie das Programm aus. Folgende Ausgabe sollte angezeigt werden:

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 . . .

Hinweis

Die Ausgabe umfasst nicht jeden Namespace, der ein untergeordneter Namespace des System-Namespace ist. Sie zeigt jeden Namespace an, der in dieser Kompilierung vorhanden ist. Diese verweist nur auf die Assembly, in der System.String deklariert ist. Namespaces, die in anderen Assemblys deklariert wurden, sind dieser Kompilierung unbekannt.

Binden eines Ausdrucks

Der oben gezeigte Code veranschaulicht, wie Sie ein Symbol suchen, indem Sie es an einen Namen binden. Es gibt weitere Ausdrücke in einem C#-Programm, die gebunden werden können, aber keine Namen sind. Um diese Funktion zu demonstrieren, sehen wir uns die Bindung an ein einfaches Zeichenfolgenliteral an.

Das Programm „Hello World“ enthält eine Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax (die Zeichenfolge „Hello, World!“, die der Konsole angezeigt wird).

Sie finden die „Hello, World!“-Zeichenfolge, indem Sie das einzelne Zeichenfolgenliteral im Programm suchen. Wenn Sie den Syntaxknoten gefunden haben, rufen Sie die Typinformationen für diesen Knoten aus dem Semantikmodell ab. Fügen Sie der Main-Methode den folgenden Code hinzu:

// 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);

Die Microsoft.CodeAnalysis.TypeInfo-Struktur enthält eine TypeInfo.Type-Eigenschaft, die Zugriff auf die semantischen Informationen zum Typ des Literals erlaubt. In diesem Beispiel handelt es sich um den Typ string. Fügen Sie eine Deklaration hinzu, die diese Eigenschaft einer lokalen Variable zuweist:

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

Zum Abschluss dieses Tutorials erstellen wir eine LINQ-Abfrage, die eine Sequenz aller öffentlichen Methoden erstellt, die im string-Typ deklariert sind und eine string zurückgeben. Diese Abfrage wird ziemlich komplex – wir bauen sie daher Zeile für Zeile auf und rekonstruieren sie dann als Einzelabfrage. Die Quelle dieser Abfrage ist die Sequenz aller Member, die im string-Typ deklariert sind:

var allMembers = stringTypeSymbol?.GetMembers();

Diese Quellsequenz enthält alle Member, einschließlich Eigenschaften und Feldern. Filtern Sie die Sequenz daher mit der ImmutableArray<T>.OfType-Methode, um die Elemente zu finden, bei denen es sich um Microsoft.CodeAnalysis.IMethodSymbol-Objekte handelt:

var methods = allMembers?.OfType<IMethodSymbol>();

Danach fügen Sie einen weiteren Filter hinzu, um nur die Methoden zurückzugeben, die öffentlich sind und eine string zurückgeben:

var publicStringReturningMethods = methods?
    .Where(m => SymbolEqualityComparer.Default.Equals(m.ReturnType, stringTypeSymbol) &&
    m.DeclaredAccessibility == Accessibility.Public);

Wählen Sie nur die name-Eigenschaft und nur eindeutige Namen aus, indem Sie alle Überladungen entfernen:

var distinctMethods = publicStringReturningMethods?.Select(m => m.Name).Distinct();

Sie können auch mithilfe der LINQ-Abfragesyntax die vollständige Abfrage erstellen und dann alle Methodennamen in der Konsole anzeigen:

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);
}

Erstellen Sie das Programm, und führen Sie es aus. Die folgende Ausgabe wird angezeigt.

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 . . .

Sie haben die Semantik-API verwendet, um Informationen zu den Symbolen zu finden und anzuzeigen, die zu diesem Programm gehören.