Udostępnij za pośrednictwem


Wprowadzenie do analizy składni

W tym samouczku zapoznasz się z interfejsem API składni. Interfejs API składni zapewnia dostęp do struktur danych opisujących program C# lub Visual Basic. Te struktury danych mają wystarczającą ilość szczegółów, które mogą w pełni reprezentować dowolny program o dowolnym rozmiarze. Te struktury mogą opisywać kompletne programy kompilujące i uruchamiane poprawnie. Mogą one również opisywać niekompletne programy, jak je pisać, w edytorze.

Aby włączyć to bogate wyrażenie, struktury danych i interfejsy API tworzące interfejs API składni są koniecznie złożone. Zacznijmy od tego, jak wygląda struktura danych dla typowego programu "Hello world":

using System;
using System.Collections.Generic;
using System.Linq;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Spójrz na tekst poprzedniego programu. Rozpoznajesz znane elementy. Cały tekst reprezentuje pojedynczy plik źródłowy lub jednostkę kompilacji. Pierwsze trzy wiersze tego pliku źródłowego używają dyrektyw. Pozostałe źródło znajduje się w deklaracji przestrzeni nazw. Deklaracja przestrzeni nazw zawiera deklarację klasy podrzędnej. Deklaracja klasy zawiera jedną deklarację metody.

Interfejs API składni tworzy strukturę drzewa z katalogiem głównym reprezentującym jednostkę kompilacji. Węzły w drzewie reprezentują dyrektywy using, deklarację przestrzeni nazw i wszystkie inne elementy programu. Struktura drzewa jest kontynuowana do najniższych poziomów: ciąg "Hello world!" to token literału ciągu, który jest malejącym argumentem. Interfejs API składni zapewnia dostęp do struktury programu. Możesz wykonać zapytanie dotyczące określonych praktyk kodu, przejść przez całe drzewo, aby zrozumieć kod i utworzyć nowe drzewa, modyfikując istniejące drzewo.

Ten krótki opis zawiera omówienie rodzaju informacji dostępnych przy użyciu interfejsu API składni. Interfejs API składni nie jest niczym więcej niż formalnym interfejsem API, który opisuje znane konstrukcje kodu znane z języka C#. Pełne możliwości obejmują informacje o sposobie formatowania kodu, w tym podziałów wierszy, białych znaków i wcięcia. Korzystając z tych informacji, można w pełni reprezentować kod napisany i odczytywany przez programistów lub kompilatora. Użycie tej struktury umożliwia interakcję z kodem źródłowym na głęboko znaczącym poziomie. Nie jest to już ciągi tekstowe, ale dane reprezentujące strukturę programu C#.

Aby rozpocząć pracę, musisz zainstalować zestaw SDK .NET Compiler Platform:

Instrukcje 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 jako część 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ł dewelopera rozszerzenia programu Visual Studio w drzewie podsumowania.
  5. Zaznacz pole wyboru zestawu SDK .NET Compiler Platform. Znajdziesz go ostatnio w obszarze opcjonalnych składników.

Opcjonalnie chcesz również , aby edytor DGML wyświetlał grafy 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. Wybierz kartę Poszczególne składniki
  4. Zaznacz pole wyboru zestawu SDK .NET Compiler Platform. Znajdziesz ją u góry w sekcji Kompilatory, narzędzia kompilacji i środowiska uruchomieniowe .

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

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

Opis drzew składni

Interfejs API składni służy do dowolnej analizy struktury kodu języka C#. Interfejs API składni uwidacznia analizatory, drzewa składni i narzędzia do analizowania i konstruowania drzew składniowych. Jest to sposób wyszukiwania kodu pod kątem określonych elementów składniowych lub odczytywania kodu dla programu.

Drzewo składni to struktura danych używana przez kompilatory języka C# i Visual Basic w celu zrozumienia programów c# i Visual Basic. Drzewa składni są tworzone przez ten sam analizator, który jest uruchamiany, gdy projekt jest kompilowany lub deweloper osiąga klawisz F5. Drzewa składni mają pełną wierność językowi; każdy bit informacji w pliku kodu jest reprezentowany w drzewie. Pisanie drzewa składni do tekstu odtwarza dokładny oryginalny tekst, który został przeanalizowany. Drzewa składni są również niezmienne; po utworzeniu drzewa składni nigdy nie można zmienić. Użytkownicy drzew mogą analizować drzewa na wielu wątkach, bez blokad lub innych miar współbieżności, wiedząc, że dane nigdy się nie zmieniają. Interfejsy API umożliwiają tworzenie nowych drzew, które są wynikiem modyfikowania istniejącego drzewa.

Cztery podstawowe bloki konstrukcyjne drzew składniowych to:

Trivia, tokeny i węzły składają się hierarchicznie, aby utworzyć drzewo, które całkowicie reprezentuje wszystko w fragmentach kodu Visual Basic lub C#. Ta struktura jest widoczna przy użyciu okna Wizualizator składni . W programie Visual Studio wybierz pozycję Wyświetl> innewizualizator składni systemuWindows>. Na przykład poprzedni plik źródłowy języka C# zbadany przy użyciu wizualizatora składni wygląda jak na poniższej ilustracji:

SkładniaNode: Niebieski | SkładniaToken: Zielony | SkładniaTrivia: Czerwony plik kodu języka C#

Nawigując po tej strukturze drzewa, możesz znaleźć dowolną instrukcję, wyrażenie, token lub bit odstępu w pliku kodu.

Chociaż możesz znaleźć dowolny element w pliku kodu przy użyciu interfejsów API składni, większość scenariuszy obejmuje badanie małych fragmentów kodu lub wyszukiwanie określonych instrukcji lub fragmentów. W dwóch przykładach pokazano typowe zastosowania do przeglądania struktury kodu lub wyszukiwania pojedynczych instrukcji.

Przechodzenie drzew

Węzły można zbadać w drzewie składni na dwa sposoby. Możesz przejść przez drzewo, aby zbadać każdy węzeł lub wykonać zapytanie dotyczące określonych elementów lub węzłów.

Przechodzenie ręczne

Gotowy kod 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 składowych 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 zwróconych obiektów. W praktyce częściej używa się niejawnie wpisanych zmiennych i polega na nazwach interfejsów API w celu opisania typu analizowanych obiektów.

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

  • W programie Visual Studio wybierz pozycję Plik>nowy> projekt, aby wyświetlić okno dialogowe Nowyprojekt.
  • W obszarze Rozszerzalność języka Visual C#>wybierz pozycję Narzędzie do analizy kodu autonomicznego.
  • Nadaj projektowi nazwę "SyntaxTreeManualTraversal" i kliknij przycisk OK.

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

        const string programText =
@"using System;
using System.Collections;
using System.Linq;
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 Main metody:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Te dwa wiersze tworzą drzewo i pobierają węzeł główny tego drzewa. Teraz możesz sprawdzić węzły w drzewie. Dodaj te wiersze do metody, Main aby wyświetlić niektóre właściwości węzła głównego w drzewie:

WriteLine($"The tree is a {root.Kind()} node.");
WriteLine($"The tree has {root.Members.Count} elements in it.");
WriteLine($"The tree has {root.Usings.Count} using statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

Uruchom aplikację, aby zobaczyć, co kod wykrył na temat węzła głównego w tym drzewie.

Zazwyczaj można przechodzić przez drzewo, aby dowiedzieć się więcej o kodzie. W tym przykładzie analizujesz kod, który znasz, aby eksplorować interfejsy API. Dodaj następujący kod, aby zbadać pierwszy element członkowski węzła root :

MemberDeclarationSyntax firstMember = root.Members[0];
WriteLine($"The first member is a {firstMember.Kind()}.");
var helloWorldDeclaration = (NamespaceDeclarationSyntax)firstMember;

Ten element członkowski jest elementem Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Reprezentuje wszystko w zakresie deklaracji namespace HelloWorld . Dodaj następujący kod, aby sprawdzić, które węzły są deklarowane w HelloWorld przestrzeni nazw:

WriteLine($"There are {helloWorldDeclaration.Members.Count} members declared in this namespace.");
WriteLine($"The first member is a {helloWorldDeclaration.Members[0].Kind()}.");

Uruchom program, aby zobaczyć zdobytą wiedzę.

Teraz, gdy wiesz, że deklaracja jest deklaracją Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, zadeklaruj nową zmienną tego typu, aby zbadać deklarację klasy. Ta klasa zawiera tylko jeden element członkowski: metodę Main . Dodaj następujący kod, aby znaleźć metodę Main i rzutować ją na element Microsoft.CodeAnalysis.CSharp.Syntax.MethodDeclarationSyntax.

var programDeclaration = (ClassDeclarationSyntax)helloWorldDeclaration.Members[0];
WriteLine($"There are {programDeclaration.Members.Count} members declared in the {programDeclaration.Identifier} class.");
WriteLine($"The first member is a {programDeclaration.Members[0].Kind()}.");
var mainDeclaration = (MethodDeclarationSyntax)programDeclaration.Members[0];

Węzeł deklaracji metody zawiera wszystkie informacje składniowe dotyczące metody. Wyświetlmy typ zwracany Main metody, liczbę i typy argumentów oraz tekst treści metody. Dodaj następujący kod:

WriteLine($"The return type of the {mainDeclaration.Identifier} method is {mainDeclaration.ReturnType}.");
WriteLine($"The method has {mainDeclaration.ParameterList.Parameters.Count} parameters.");
foreach (ParameterSyntax item in mainDeclaration.ParameterList.Parameters)
    WriteLine($"The type of the {item.Identifier} parameter is {item.Type}.");
WriteLine($"The body text of the {mainDeclaration.Identifier} method follows:");
WriteLine(mainDeclaration.Body?.ToFullString());

var argsParameter = mainDeclaration.ParameterList.Parameters[0];

Uruchom program, aby wyświetlić wszystkie odnalezione informacje o tym programie:

The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using statements. They are:
        System
        System.Collections
        System.Linq
        System.Text
The first member is a NamespaceDeclaration.
There are 1 members declared in this namespace.
The first member is a ClassDeclaration.
There are 1 members declared in the Program class.
The first member is a MethodDeclaration.
The return type of the Main method is void.
The method has 1 parameters.
The type of the args parameter is string[].
The body text of the Main method follows:
        {
            Console.WriteLine("Hello, World!");
        }

Metody zapytań

Oprócz przechodzenia drzew można również eksplorować drzewo składni przy użyciu metod zapytań zdefiniowanych w elemecie Microsoft.CodeAnalysis.SyntaxNode. Te metody powinny być natychmiast znane każdemu, kto zna program XPath. Możesz użyć tych metod z LINQ, aby szybko znaleźć rzeczy w drzewie. Zawiera SyntaxNode metody zapytań, takie jak DescendantNodes, AncestorsAndSelf i ChildNodes.

Za pomocą tych metod zapytań można znaleźć argument Main metody jako alternatywę dla nawigowania po drzewie. Dodaj następujący kod w dolnej części Main metody:

var firstParameters = from methodDeclaration in root.DescendantNodes()
                                        .OfType<MethodDeclarationSyntax>()
                      where methodDeclaration.Identifier.ValueText == "Main"
                      select methodDeclaration.ParameterList.Parameters.First();

var argsParameter2 = firstParameters.Single();

WriteLine(argsParameter == argsParameter2);

Pierwsza instrukcja używa wyrażenia LINQ i DescendantNodes metody do zlokalizowania tego samego parametru co w poprzednim przykładzie.

Uruchom program i zobaczysz, że wyrażenie LINQ znalazło ten sam parametr co ręczne nawigowanie po drzewie.

Przykład używa WriteLine instrukcji do wyświetlania informacji o drzewach składni w miarę przechodzenia. Możesz również dowiedzieć się o wiele więcej, uruchamiając gotowy program w debugerze. Możesz sprawdzić więcej właściwości i metod, które są częścią drzewa składni utworzonego dla programu hello world.

Przewodniki składniowe

Często chcesz znaleźć wszystkie węzły określonego typu w drzewie składni, na przykład każda deklaracja właściwości w pliku. Rozszerzając klasę Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker i przesłaniając VisitPropertyDeclaration(PropertyDeclarationSyntax) metodę, należy przetworzyć każdą deklarację właściwości w drzewie składni bez wcześniejszej znajomości jego struktury. CSharpSyntaxWalker jest konkretnym rodzajem CSharpSyntaxVisitor , który rekursywnie odwiedza węzeł i każde z jego elementów podrzędnych.

W tym przykładzie zaimplementowana CSharpSyntaxWalker jest kontrolka, która sprawdza drzewo składniowe. Zbiera using dyrektywy, które stwierdza, że nie importuje System przestrzeni nazw.

Utwórz nowy projekt narzędzia analizy kodu autonomicznego języka C#; nadaj mu nazwę "SyntaxWalker".

Gotowy kod można zobaczyć w naszym repozytorium GitHub. Przykład w usłudze GitHub zawiera oba projekty opisane w tym samouczku.

Podobnie jak w poprzednim przykładzie, możesz zdefiniować stałą ciągu do przechowywania tekstu programu, który będzie analizowany:

        const string programText =
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;

    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;

        class Foo { }
    }

    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;

        class Bar { }
    }
}";

Ten tekst źródłowy zawiera using dyrektywy rozproszone w czterech różnych lokalizacjach: poziom pliku, w przestrzeni nazw najwyższego poziomu i w dwóch zagnieżdżonych przestrzeniach nazw. W tym przykładzie wyróżniono podstawowy scenariusz użycia CSharpSyntaxWalker klasy do wykonywania zapytań w kodzie. Byłoby kłopotliwe, aby odwiedzić każdy węzeł w drzewie składni głównej w celu znalezienia przy użyciu deklaracji. Zamiast tego należy utworzyć klasę pochodną i zastąpić metodę, która jest wywoływana tylko wtedy, gdy bieżący węzeł w drzewie jest dyrektywą using. Gość nie wykonuje żadnej pracy w innych typach węzłów. Ta pojedyncza metoda analizuje poszczególne using instrukcje i tworzy kolekcję przestrzeni nazw, które nie są w System przestrzeni nazw. Tworzy się element CSharpSyntaxWalker , który analizuje wszystkie using instrukcje, ale tylko instrukcje using .

Teraz, po zdefiniowaniu tekstu programu, musisz utworzyć element SyntaxTree i pobrać katalog główny tego drzewa:

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

Następnie utwórz nową klasę. W programie Visual Studio wybierz pozycję Projekt>Dodaj nowy element. W oknie dialogowym Dodawanie nowego elementu wpisz UsingCollector.cs jako nazwę pliku.

Funkcje gościa są implementowanie using w UsingCollector klasie . Zacznij od tworzenia klasy pochodzącej UsingCollector z CSharpSyntaxWalkerklasy .

class UsingCollector : CSharpSyntaxWalker

Magazyn jest potrzebny do przechowywania zbieranych węzłów przestrzeni nazw. Zadeklaruj publiczną właściwość tylko do odczytu w UsingCollector klasie. Ta zmienna służy do przechowywania UsingDirectiveSyntax znajdowanych węzłów:

public ICollection<UsingDirectiveSyntax> Usings { get; } = new List<UsingDirectiveSyntax>();

Klasa podstawowa implementuje logikę, CSharpSyntaxWalker aby odwiedzić każdy węzeł w drzewie składni. Klasa pochodna zastępuje metody wywoływane dla określonych węzłów, które cię interesują. W tym przypadku interesuje Cię każda using dyrektywa. Oznacza to, że należy zastąpić metodę VisitUsingDirective(UsingDirectiveSyntax) . Jednym z argumentów tej metody jest Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax obiekt. Jest to ważna zaleta korzystania z odwiedzających: nazywają one metody przesłonięcia z argumentami już rzutnymi na określony typ węzła. Klasa Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax ma Name właściwość, która przechowuje nazwę importowanej przestrzeni nazw. Jest to .Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax Dodaj następujący kod w przesłonięć VisitUsingDirective(UsingDirectiveSyntax) :

public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
    WriteLine($"\tVisitUsingDirective called with {node.Name}.");
    if (node.Name.ToString() != "System" &&
        !node.Name.ToString().StartsWith("System."))
    {
        WriteLine($"\t\tSuccess. Adding {node.Name}.");
        this.Usings.Add(node);
    }
}

Podobnie jak w poprzednim przykładzie, dodano różne instrukcje WriteLine ułatwiające zrozumienie tej metody. Możesz zobaczyć, kiedy jest wywoływana, i jakie argumenty są przekazywane do niego za każdym razem.

Na koniec musisz dodać dwa wiersze kodu, aby utworzyć UsingCollector element i odwiedzić węzeł główny, zbierając wszystkie using instrukcje. Następnie dodaj pętlę foreach , aby wyświetlić wszystkie using instrukcje znalezione przez moduł zbierający:

var collector = new UsingCollector();
collector.Visit(root);
foreach (var directive in collector.Usings)
{
    WriteLine(directive.Name);
}

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

        VisitUsingDirective called with System.
        VisitUsingDirective called with System.Collections.Generic.
        VisitUsingDirective called with System.Linq.
        VisitUsingDirective called with System.Text.
        VisitUsingDirective called with Microsoft.CodeAnalysis.
                Success. Adding Microsoft.CodeAnalysis.
        VisitUsingDirective called with Microsoft.CodeAnalysis.CSharp.
                Success. Adding Microsoft.CodeAnalysis.CSharp.
        VisitUsingDirective called with Microsoft.
                Success. Adding Microsoft.
        VisitUsingDirective called with System.ComponentModel.
        VisitUsingDirective called with Microsoft.Win32.
                Success. Adding Microsoft.Win32.
        VisitUsingDirective called with System.Runtime.InteropServices.
        VisitUsingDirective called with System.CodeDom.
        VisitUsingDirective called with Microsoft.CSharp.
                Success. Adding Microsoft.CSharp.
Microsoft.CodeAnalysis
Microsoft.CodeAnalysis.CSharp
Microsoft
Microsoft.Win32
Microsoft.CSharp
Press any key to continue . . .

Gratulacje! Interfejs API składni służy do lokalizowania określonych rodzajów instrukcji i deklaracji języka C# w kodzie źródłowym języka C#.