Udostępnij za pośrednictwem


Wprowadzenie do analizy semantycznej

W tym samouczku założono, że znasz interfejs API składni. Artykuł Wprowadzenie do analizy składni zawiera wystarczające wprowadzenie.

W tym samouczku zapoznasz się z interfejsami API symboli i powiązań. Te interfejsy API zawierają informacje o znaczeniu semantycznym programu. Umożliwiają one zadawanie pytań dotyczących typów reprezentowanych przez dowolny symbol w programie i odpowiadanie na nie.

Musisz zainstalować zestaw SDK .NET Compiler Platform:

Instrukcje dotyczące instalacji — Instalator programu Visual Studio

Istnieją dwa różne sposoby znajdowania zestawu SDK .NET Compiler Platform w Instalator programu Visual Studio:

Instalowanie przy użyciu widoku Instalator programu Visual Studio — obciążenia

Zestaw SDK .NET Compiler Platform nie jest automatycznie wybierany w ramach obciążenia programistycznego rozszerzenia programu Visual Studio. Musisz wybrać go jako składnik opcjonalny.

  1. Uruchamianie Instalator programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Sprawdź obciążenie programistyczne rozszerzenia programu Visual Studio .
  4. Otwórz węzeł programowania rozszerzenia programu Visual Studio w drzewie podsumowania.
  5. Zaznacz pole wyboru dla zestawu SDK .NET Compiler Platform. Znajdziesz go ostatnio w obszarze składników opcjonalnych.

Opcjonalnie chcesz również , aby edytor DGML wyświetlał wykresy w wizualizatorze:

  1. Otwórz węzeł Poszczególne składniki w drzewie podsumowania.
  2. Zaznacz pole wyboru edytora DGML

Instalowanie przy użyciu karty Instalator programu Visual Studio — poszczególne składniki

  1. Uruchamianie Instalator programu Visual Studio
  2. Wybierz pozycję Modyfikuj
  3. Wybieranie karty Poszczególne składniki
  4. Zaznacz pole wyboru dla zestawu SDK .NET Compiler Platform. Znajduje się on u góry w sekcji Kompilatory, narzędzia kompilacji i środowiska uruchomieniowe .

Opcjonalnie chcesz również , aby edytor DGML wyświetlał wykresy w wizualizatorze:

  1. Zaznacz pole wyboru edytora DGML. Znajdziesz go w sekcji Narzędzia kodu .

Opis kompilacji i symboli

W miarę pracy z zestawem SDK kompilatora platformy .NET zapoznasz się z różnicami między interfejsem API składni a interfejsem API semantycznym. Interfejs API składni umożliwia przyjrzenie się strukturze programu. Jednak często potrzebujesz bogatszych informacji o semantyce lub znaczeniu programu. Chociaż luźny plik kodu lub fragment kodu języka Visual Basic lub C# można przeanalizować syntaktycznie w izolacji, nie ma znaczenia, aby zadawać pytania, takie jak "jaki jest typ tej zmiennej" w próżni. Znaczenie nazwy typu może być zależne od odwołań do zestawów, importów przestrzeni nazw lub innych plików kodu. Odpowiedzi na te pytania są używane przy użyciu interfejsu API semantycznego, w szczególności Microsoft.CodeAnalysis.Compilation klasy .

Wystąpienie klasy jest analogiczne do pojedynczego Compilation projektu, jak widać w kompilatorze i reprezentuje wszystko, co jest potrzebne do skompilowania programu Visual Basic lub C#. Kompilacja zawiera zestaw plików źródłowych, które mają być kompilowane, odwołania do zestawów i opcje kompilatora. Możesz wnioskować o znaczeniu kodu przy użyciu wszystkich innych informacji w tym kontekście. Element Compilation umożliwia znajdowanie symboli — jednostek, takich jak typy, przestrzenie nazw, elementy członkowskie i zmienne, które odwołują się do nazw i innych wyrażeń. Proces kojarzenia nazw i wyrażeń z symbolami jest nazywany powiązaniem.

Podobnie jak Microsoft.CodeAnalysis.SyntaxTree, Compilation jest abstrakcyjną klasą z pochodnymi specyficznymi dla języka. Podczas tworzenia wystąpienia kompilacji należy wywołać metodę fabryki w Microsoft.CodeAnalysis.CSharp.CSharpCompilation klasie (lub Microsoft.CodeAnalysis.VisualBasic.VisualBasicCompilation).

Wykonywanie zapytań dotyczących symboli

W tym samouczku ponownie przyjrzysz się programowi "Hello world". Tym razem wykonujesz zapytanie dotyczące symboli w programie, aby zrozumieć, jakie typy te symbole reprezentują. Odpytujesz typy w przestrzeni nazw i dowiesz się, jak znaleźć metody dostępne dla typu.

Gotowy kod dla tego przykładu można zobaczyć w naszym repozytorium GitHub.

Uwaga

Typy drzewa składni używają dziedziczenia do opisywania różnych elementów składni, które są prawidłowe w różnych lokalizacjach w programie. Korzystanie z tych interfejsów API często oznacza rzutowanie właściwości lub elementów członkowskich kolekcji do określonych typów pochodnych. W poniższych przykładach przypisanie i rzutowanie są oddzielnymi instrukcjami, używając jawnie wpisanych zmiennych. Możesz odczytać kod, aby zobaczyć zwracane typy interfejsu API i typ środowiska uruchomieniowego zwracanych obiektów. W praktyce częściej używa się niejawnie typiowanych zmiennych i polega na nazwach interfejsów API w celu opisania badanego typu obiektów.

Utwórz nowy projekt narzędzia analizy kodu autonomicznego w języku C#:

  • W programie Visual Studio wybierz pozycję Plik>nowy>projekt , aby wyświetlić okno dialogowe Nowy projekt.
  • W obszarze Visual C#>Extensibility (Rozszerzalność) wybierz pozycję Stand-Alone Code Analysis Tool (Autonomiczne narzędzie do analizy kodu).
  • Nadaj projektowi nazwę "SemanticQuickStart" i kliknij przycisk OK.

Przeanalizujesz pokazany wcześniej podstawowy program "Hello world!". Dodaj tekst programu Hello world jako stałą w Program klasie:

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

Następnie dodaj następujący kod, aby skompilować drzewo składni dla tekstu kodu w stałej programText . Dodaj następujący wiersz do metody Main :

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);

CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Następnie skompiluj element CSharpCompilation z już utworzonego drzewa. Przykład "Hello world" opiera się na typach String i Console . Należy odwołać się do zestawu, który deklaruje te dwa typy w kompilacji. Dodaj następujący wiersz do metody Main , aby utworzyć kompilację drzewa składni, w tym odwołanie do odpowiedniego zestawu:

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

Metoda CSharpCompilation.AddReferences dodaje odwołania do kompilacji. Metoda MetadataReference.CreateFromFile ładuje zestaw jako odwołanie.

Wykonywanie zapytań względem modelu semantycznego

Gdy masz element Compilation , możesz go poprosić o SemanticModel element dla dowolnego SyntaxTree pliku zawartego w tym Compilationobiekcie . Model semantyczny można traktować jako źródło wszystkich informacji, które zwykle uzyskuje się z funkcji IntelliSense. Można SemanticModel odpowiedzieć na pytania, takie jak "Jakie nazwy znajdują się w zakresie w tej lokalizacji?", "Jakie elementy członkowskie są dostępne z tej metody?", "Jakie zmienne są używane w tym bloku tekstu?", i "Do czego odnosi się ta nazwa/wyrażenie?" Dodaj tę instrukcję, aby utworzyć model semantyczny:

SemanticModel model = compilation.GetSemanticModel(tree);

Wiązanie nazwy

Obiekt Compilation tworzy element SemanticModel na podstawie elementu SyntaxTree. Po utworzeniu modelu możesz wykonać zapytanie w celu znalezienia pierwszej using dyrektywy i pobrać informacje o symbolu System dla przestrzeni nazw. Dodaj te dwa wiersze do metody Main , aby utworzyć model semantyczny i pobrać symbol dla pierwszej instrukcji 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);

Powyższy kod pokazuje, jak powiązać nazwę w pierwszej using dyrektywie, aby pobrać Microsoft.CodeAnalysis.SymbolInfo element dla System przestrzeni nazw. Powyższy kod ilustruje również, że używasz modelu składni do znalezienia struktury kodu; używasz modelu semantycznego , aby zrozumieć jego znaczenie. Model składni znajduje ciąg System w instrukcji using. Model semantyczny zawiera wszystkie informacje o typach zdefiniowanych w System przestrzeni nazw.

SymbolInfo Z obiektu można uzyskać Microsoft.CodeAnalysis.ISymbol za pomocą SymbolInfo.Symbol właściwości . Ta właściwość zwraca symbol, do których odwołuje się to wyrażenie. W przypadku wyrażeń, które nie odwołują się do niczego (na przykład literałów liczbowych), ta właściwość to null. Jeśli parametr SymbolInfo.Symbol nie ma wartości null, ISymbol.Kind oznacza typ symbolu. W tym przykładzie ISymbol.Kind właściwość jest właściwością SymbolKind.Namespace. Dodaj następujący kod do metody Main . Pobiera symbol przestrzeni System nazw, a następnie wyświetla wszystkie podrzędne przestrzenie nazw zadeklarowane w System przestrzeni nazw:

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

Uruchom program i powinny zostać wyświetlone następujące dane wyjściowe:

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

Uwaga

Dane wyjściowe nie zawierają każdej przestrzeni nazw, która jest podrzędną przestrzenią System nazw. Wyświetla każdą przestrzeń nazw, która znajduje się w tej kompilacji, która odwołuje się tylko do zestawu, w którym System.String jest zadeklarowany. Wszystkie przestrzenie nazw zadeklarowane w innych zestawach nie są znane tej kompilacji

Wiązanie wyrażenia

Powyższy kod pokazuje, jak znaleźć symbol przez powiązanie z nazwą. Istnieją inne wyrażenia w programie języka C#, które mogą być powiązane, które nie są nazwami. Aby zademonstrować tę możliwość, uzyskajmy dostęp do powiązania z prostym literałem ciągu.

Program "Hello world" zawiera Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntaxciąg "Hello, World!" wyświetlany w konsoli.

Ciąg "Hello, World!" można znaleźć, wyszukując literał pojedynczego ciągu w programie. Następnie po zlokalizowaniu węzła składni pobierz informacje o typie dla tego węzła z modelu semantycznego. Dodaj następujący kod do Main metody:

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

Struktura Microsoft.CodeAnalysis.TypeInfo zawiera TypeInfo.Type właściwość, która umożliwia dostęp do semantycznych informacji o typie literału. W tym przykładzie string jest to typ. Dodaj deklarację, która przypisuje tę właściwość do zmiennej lokalnej:

var stringTypeSymbol = (INamedTypeSymbol?)literalInfo.Type;

Aby ukończyć ten samouczek, skompilujmy zapytanie LINQ, które tworzy sekwencję wszystkich metod publicznych zadeklarowanych w typie string , który zwraca stringwartość . To zapytanie staje się złożone, więc skompilujmy go według wiersza, a następnie zrekonstruujmy je jako pojedyncze zapytanie. Źródłem tego zapytania jest sekwencja wszystkich elementów członkowskich zadeklarowanych w typie string :

var allMembers = stringTypeSymbol?.GetMembers();

Ta sekwencja źródłowa zawiera wszystkie elementy członkowskie, w tym właściwości i pola, więc przefiltruj ImmutableArray<T>.OfType ją przy użyciu metody , aby znaleźć elementy, które są Microsoft.CodeAnalysis.IMethodSymbol obiektami:

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

Następnie dodaj kolejny filtr, aby zwrócić tylko te metody, które są publiczne i zwracają polecenie string:

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

Wybierz tylko właściwość name i tylko odrębne nazwy, usuwając wszelkie przeciążenia:

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

Pełne zapytanie można również skompilować przy użyciu składni zapytania LINQ, a następnie wyświetlić wszystkie nazwy metod w konsoli programu :

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

Skompiluj i uruchom program. Powinny zostać wyświetlone następujące dane wyjściowe:

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

Interfejs API semantyczny został użyty do znajdowania i wyświetlania informacji o symbolach, które są częścią tego programu.