Share via


Kom igång med syntaxanalys

I den här självstudien utforskar du syntax-API:et. Syntax-API:et ger åtkomst till de datastrukturer som beskriver ett C#- eller Visual Basic-program. Dessa datastrukturer har tillräckligt med information för att de fullt ut kan representera alla program av valfri storlek. Dessa strukturer kan beskriva kompletta program som kompilerar och körs korrekt. De kan också beskriva ofullständiga program, när du skriver dem, i redigeraren.

För att aktivera det här omfattande uttrycket är datastrukturerna och API:erna som utgör syntax-API:et nödvändigtvis komplexa. Vi börjar med hur datastrukturen ser ut för det typiska programmet "Hello World":

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

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

Titta på texten i föregående program. Du känner igen välbekanta element. Hela texten representerar en enda källfil eller en kompileringsenhet. De tre första raderna i källfilen använder direktiv. Den återstående källan finns i en namnområdesdeklaration. Namnområdesdeklarationen innehåller en underordnad klassdeklaration. Klassdeklarationen innehåller en metoddeklaration.

Syntax-API:et skapar en trädstruktur med roten som representerar kompileringsenheten. Noder i trädet representerar användningsdirektiv, namnområdesdeklaration och alla andra element i programmet. Trädstrukturen fortsätter ned till de lägsta nivåerna: strängen "Hello World!" är en strängliteral token som är en underordnad till ett argument. Syntax-API:et ger åtkomst till programmets struktur. Du kan fråga efter specifika kodmetoder, gå genom hela trädet för att förstå koden och skapa nya träd genom att ändra det befintliga trädet.

Den korta beskrivningen ger en översikt över vilken typ av information som är tillgänglig med hjälp av syntax-API:et. Syntax-API:et är inget annat än ett formellt API som beskriver de välbekanta kodkonstruktioner som du känner till från C#. De fullständiga funktionerna innehåller information om hur koden formateras, inklusive radbrytningar, tomt utrymme och indrag. Med den här informationen kan du helt representera koden som skriven och läst av mänskliga programmerare eller kompilatorn. Med den här strukturen kan du interagera med källkoden på en djupt meningsfull nivå. Det är inte längre textsträngar, utan data som representerar strukturen för ett C#-program.

För att komma igång måste du installera .NET Compiler Platform SDK:

Installationsinstruktioner – Installationsprogram för Visual Studio

Det finns två olika sätt att hitta .NET Compiler Platform SDK i Visual Studio Installer:

Installera med Visual Studio Installer – arbetsbelastningsvyn

.NET Compiler Platform SDK väljs inte automatiskt som en del av arbetsbelastningen för Utveckling av Visual Studio-tillägg. Du måste välja den som en valfri komponent.

  1. Köra Installationsprogrammet för Visual Studio
  2. Välj Ändra
  3. Kontrollera arbetsbelastningen för Utveckling av Visual Studio-tillägg .
  4. Öppna visual studiotilläggets utvecklingsnod i sammanfattningsträdet.
  5. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den sist under de valfria komponenterna.

Du vill också att DGML-redigeraren ska visa grafer i visualiseraren:

  1. Öppna noden Enskilda komponenter i sammanfattningsträdet.
  2. Markera kryssrutan för DGML-redigeraren

Installera med hjälp av fliken Installationsprogram för Visual Studio – enskilda komponenter

  1. Köra Installationsprogrammet för Visual Studio
  2. Välj Ändra
  3. Välj fliken Enskilda komponenter
  4. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den längst upp under avsnittet Kompilatorer, byggverktyg och körning.

Du vill också att DGML-redigeraren ska visa grafer i visualiseraren:

  1. Markera kryssrutan för DGML-redigeraren. Du hittar den under avsnittet Kodverktyg .

Förstå syntaxträd

Du använder syntax-API:et för alla analyser av strukturen för C#-kod. Syntax-API:et exponerar parsarna, syntaxträden och verktygen för att analysera och konstruera syntaxträd. Det är så du söker kod efter specifika syntaxelement eller läser koden för ett program.

Ett syntaxträd är en datastruktur som används av C#- och Visual Basic-kompilatorerna för att förstå C#- och Visual Basic-program. Syntaxträd skapas av samma parser som körs när ett projekt skapas eller en utvecklare når F5. Syntaxträden har full återgivning med språket. all information i en kodfil representeras i trädet. Om du skriver ett syntaxträd till text återskapas den exakta ursprungliga texten som parsades. Syntaxträden är också oföränderliga. när ett syntaxträd har skapats kan det aldrig ändras. Användare av träden kan analysera träden på flera trådar, utan lås eller andra samtidighetsåtgärder, med vetskap om att data aldrig ändras. Du kan använda API:er för att skapa nya träd som är resultatet av att ändra ett befintligt träd.

De fyra primära byggstenarna i syntaxträd är:

Trivia, token och noder består hierarkiskt för att bilda ett träd som helt representerar allt i ett fragment av Visual Basic- eller C#-kod. Du kan se den här strukturen med hjälp av fönstret Syntaxvisualiserare . I Visual Studio väljer du Visa>annanWindows-syntaxvisualiserare>. Till exempel ser den föregående C#-källfilen som undersöktes med syntaxvisualiseraren ut som följande bild:

SyntaxNod: Blå | SyntaxToken: Grön | SyntaxTrivia: Red C#-kodfil

Genom att navigera i den här trädstrukturen kan du hitta valfri instruktion, uttryck, token eller lite tomt utrymme i en kodfil.

Även om du kan hitta vad som helst i en kodfil med hjälp av Syntax-API:er, omfattar de flesta scenarier att undersöka små kodfragment eller söka efter specifika instruktioner eller fragment. De två exemplen som följer visar vanliga användningsområden för att bläddra i kodens struktur eller söka efter enkla instruktioner.

Bläddring av träd

Du kan undersöka noderna i ett syntaxträd på två sätt. Du kan bläddra i trädet för att undersöka varje nod, eller så kan du fråga efter specifika element eller noder.

Manuell bläddering

Du kan se den färdiga koden för det här exemplet på vår GitHub-lagringsplats.

Anteckning

Syntaxträdstyperna använder arv för att beskriva de olika syntaxelement som är giltiga på olika platser i programmet. Om du använder dessa API:er innebär det ofta att du gjuter egenskaper eller samlingsmedlemmar till specifika härledda typer. I följande exempel är tilldelningen och avgjutningarna separata instruktioner med hjälp av uttryckligen inskrivna variabler. Du kan läsa koden för att se returtyperna för API:et och körningstypen för de objekt som returneras. I praktiken är det vanligare att använda implicit skrivna variabler och förlita sig på API-namn för att beskriva typen av objekt som undersöks.

Skapa ett nytt C#-projekt för fristående kodanalysverktyg :

  • I Visual Studio väljer du Nyttprojekt för>> att visa dialogrutan Nytt projekt.
  • Under Visual C#>Utökningsbarhet väljer du Fristående kodanalysverktyg.
  • Ge projektet namnet "SyntaxTreeManualTraversal" och klicka på OK.

Du ska analysera det grundläggande programmet "Hello World!" som visades tidigare. Lägg till texten för Hello World-programmet som en konstant i klassenProgram:

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

Lägg sedan till följande kod för att skapa syntaxträdet för kodtexten i konstanten programText . Lägg till följande rad i din Main metod:

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

De två linjerna skapar trädet och hämtar rotnoden i trädet. Nu kan du undersöka noderna i trädet. Lägg till dessa rader i metoden Main för att visa några av egenskaperna för rotnoden i trädet:

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

Kör programmet för att se vad koden har upptäckt om rotnoden i det här trädet.

Vanligtvis går du igenom trädet för att lära dig mer om koden. I det här exemplet analyserar du kod som du vet för att utforska API:erna. Lägg till följande kod för att undersöka den första medlemmen i root noden:

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

Den medlemmen är en Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Den representerar allt som ingår i deklarationen namespace HelloWorld . Lägg till följande kod för att undersöka vilka noder som deklareras i HelloWorld namnområdet:

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

Kör programmet för att se vad du har lärt dig.

Nu när du vet att deklarationen är en Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntaxdeklarerar du en ny variabel av den typen för att undersöka klassdeklarationen. Den här klassen innehåller bara en medlem: Main metoden . Lägg till följande kod för att hitta Main metoden och omvandla den till en 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];

Metoddeklarationsnoden innehåller all syntaktisk information om metoden. Nu ska vi visa returtypen Main för metoden, antalet och typerna av argumenten och brödtexten för metoden. Lägg till följande 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];

Kör programmet för att se all information som du har upptäckt om det här programmet:

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

Frågemetoder

Förutom att bläddra i träd kan du även utforska syntaxträdet med hjälp av de frågemetoder som definierats på Microsoft.CodeAnalysis.SyntaxNode. Dessa metoder bör vara omedelbart bekanta för alla som är bekanta med XPath. Du kan använda dessa metoder med LINQ för att snabbt hitta saker i ett träd. SyntaxNode har frågemetoder som DescendantNodes, AncestorsAndSelf och ChildNodes.

Du kan använda dessa frågemetoder för att hitta argumentet till Main metoden som ett alternativ till att navigera i trädet. Lägg till följande kod längst ned i Main metoden:

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

Den första instruktionen DescendantNodes använder ett LINQ-uttryck och -metoden för att hitta samma parameter som i föregående exempel.

Kör programmet och du kan se att LINQ-uttrycket hittade samma parameter som att navigera i trädet manuellt.

Exemplet använder WriteLine instruktioner för att visa information om syntaxträden när de bläddras igenom. Du kan också lära dig mycket mer genom att köra det färdiga programmet under felsökningsprogrammet. Du kan undersöka fler av de egenskaper och metoder som ingår i syntaxträdet som skapats för Hello World-programmet.

Syntaxvandrare

Ofta vill du hitta alla noder av en viss typ i ett syntaxträd, till exempel varje egenskapsdeklaration i en fil. Genom att Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker utöka klassen och åsidosätta VisitPropertyDeclaration(PropertyDeclarationSyntax) metoden bearbetar du varje egenskapsdeklaration i ett syntaxträd utan att känna till dess struktur i förväg. CSharpSyntaxWalker är en specifik typ av CSharpSyntaxVisitor som rekursivt besöker en nod och var och en av dess underordnade.

Det här exemplet implementerar en CSharpSyntaxWalker som undersöker ett syntaxträd. Den samlar in using direktiv som den hittar som inte importerar ett System namnområde.

Skapa ett nytt C#-projekt för fristående kodanalysverktyg . ge den namnet "SyntaxWalker".

Du kan se den färdiga koden för det här exemplet på vår GitHub-lagringsplats. Exemplet på GitHub innehåller båda projekten som beskrivs i den här självstudien.

Precis som i föregående exempel kan du definiera en strängkonstant för att lagra texten i det program som du ska analysera:

        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 { }
    }
}";

Den här källtexten innehåller using direktiv spridda över fyra olika platser: filnivån, i namnområdet på den översta nivån och i de två kapslade namnrymderna. Det här exemplet visar ett kärnscenario för att använda CSharpSyntaxWalker klassen för att fråga kod. Det skulle vara besvärligt att besöka varje nod i rotsyntaxträdet för att hitta med hjälp av deklarationer. I stället skapar du en härledd klass och åsidosätter den metod som anropas endast när den aktuella noden i trädet är ett användningsdirektiv. Besökaren arbetar inte med andra nodtyper. Den här enkla metoden undersöker var och en av -uttrycken using och skapar en samling namnområden som inte finns i System namnområdet. Du skapar en CSharpSyntaxWalker som undersöker alla using -instruktioner, men bara -uttrycken using .

Nu när du har definierat programtexten måste du skapa en SyntaxTree och hämta roten för det trädet:

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

Skapa sedan en ny klass. Välj ProjectAdd New Item (Lägg till nytt objekt)> i Visual Studio. I dialogrutan Lägg till nytt objekt skriver du UsingCollector.cs som filnamn.

Du implementerar besöksfunktionerna usingUsingCollector i klassen. Börja med att göra klassen UsingCollector härledd från CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

Du behöver lagring för att lagra de namnområdesnoder som du samlar in. Deklarera en offentlig skrivskyddad egenskap i klassen. Du använder den UsingCollector här variabeln för att lagra de UsingDirectiveSyntax noder som du hittar:

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

Basklassen CSharpSyntaxWalker implementerar logiken för att besöka varje nod i syntaxträdet. Den härledda klassen åsidosätter de metoder som anropas för de specifika noder som du är intresserad av. I det här fallet är du intresserad av alla using direktiv. Det innebär att du måste åsidosätta VisitUsingDirective(UsingDirectiveSyntax) metoden. Det enda argumentet för den här metoden är ett Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax objekt. Det är en viktig fördel med att använda besökarna: de anropar de åsidosatta metoderna med argument som redan har kastats till den specifika nodtypen. Klassen Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax har en Name egenskap som lagrar namnet på namnområdet som importeras. Det är en Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Lägg till följande kod i åsidosättningen 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);
    }
}

Precis som i det tidigare exemplet har du lagt till en mängd WriteLine olika instruktioner för att underlätta förståelsen av den här metoden. Du kan se när den anropas och vilka argument som skickas till den varje gång.

Slutligen måste du lägga till två kodrader för att skapa UsingCollector och låta den besöka rotnoden och samla in alla using -instruktioner. Lägg sedan till en foreach loop för att visa alla using instruktioner som din insamlare hittade:

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

Kompilera och kör programmet. Du bör se följande utdata:

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

Grattis! Du har använt syntax-API :et för att hitta specifika typer av C#-instruktioner och deklarationer i C#-källkoden.