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.
- Uruchamianie Instalator programu Visual Studio
- Wybierz pozycję Modyfikuj
- Sprawdź obciążenie programistyczne rozszerzenia programu Visual Studio .
- Otwórz węzeł programowania rozszerzenia programu Visual Studio w drzewie podsumowania.
- 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:
- Otwórz węzeł Poszczególne składniki w drzewie podsumowania.
- Zaznacz pole wyboru edytora DGML
Instalowanie przy użyciu karty Instalator programu Visual Studio — poszczególne składniki
- Uruchamianie Instalator programu Visual Studio
- Wybierz pozycję Modyfikuj
- Wybieranie karty Poszczególne składniki
- 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:
- 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 string
wartość . 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.