Sdílet prostřednictvím


Začínáme s analýzou syntaxe

V tomto kurzu prozkoumáte rozhraní API syntaxe. Rozhraní API syntaxe poskytuje přístup k datovým strukturám, které popisují program jazyka C# nebo Visual Basic. Tyto datové struktury mají dostatek podrobností, aby mohly plně reprezentovat jakýkoli program libovolné velikosti. Tyto struktury mohou popsat úplné programy, které se kompilují a běží správně. Můžou také popisovat neúplné programy, jak je píšete, v editoru.

Aby bylo možné povolit tento bohatý výraz, jsou datové struktury a rozhraní API, která tvoří rozhraní API pro syntaxi, nutně složité. Začněme tím, jak vypadá datová struktura pro typický program "Hello World":

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

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

Podívejte se na text předchozí aplikace. Rozpoznáváte známé prvky. Celý text představuje jeden zdrojový soubor nebo kompilační jednotku. První tři řádky tohoto zdrojového souboru používají direktivy. Zbývající zdroj je obsažen v deklaraci oboru názvů. Deklarace oboru názvů obsahuje deklaraci podřízené třídy. Deklarace třídy obsahuje jednu deklaraci metody.

Rozhraní API pro syntaxi vytvoří stromovou strukturu s kořenem představujícím jednotku kompilace. Uzly ve stromu představují direktivy using, deklaraci oboru názvů a všechny ostatní prvky programu. Stromová struktura pokračuje až k nejnižším úrovním: řetězec "Hello World!" je řetězcový literálový token, který je potomkem argumentu. Rozhraní API syntaxe poskytuje přístup ke struktuře programu. Můžete se dotazovat na konkrétní postupy kódu, projít celý strom, abyste porozuměli kódu, a vytvořit nové stromy úpravou existujícího stromu.

Tento stručný popis poskytuje přehled o druhu informací přístupných pomocí rozhraní API syntaxe. Rozhraní API syntaxe není nic víc než formální rozhraní API, které popisuje známé konstrukce kódu, které znáte z jazyka C#. Mezi všechny možnosti patří informace o formátování kódu, včetně konců řádků, prázdných znaků a odsazení. Pomocí těchto informací můžete kód plně znázorňovat jako napsaný a přečtený lidmi programátory nebo kompilátorem. Použití této struktury umožňuje interakci se zdrojovým kódem na velmi smysluplné úrovni. Už to nejsou textové řetězce, ale data, která představují strukturu programu v jazyce C#.

Abyste mohli začít, budete si muset nainstalovat sadu .NET Compiler Platform SDK:

Pokyny k instalaci – Instalační program pro Visual Studio

Existují dva různé způsoby, jak najít .NET Compiler Platform SDK v Instalační program pro Visual Studio:

Instalace pomocí zobrazení Instalační program pro Visual Studio – Úlohy

Sada .NET Compiler Platform SDK není automaticky vybrána jako součást úlohy vývoj rozšíření sady Visual Studio. Musíte ji vybrat jako volitelnou komponentu.

  1. Spustit Instalační program pro Visual Studio
  2. Vyberte Upravit.
  3. Projděte si úlohu vývoj rozšíření sady Visual Studio .
  4. Otevřete uzel Vývoj rozšíření sady Visual Studio ve stromu souhrnu.
  5. Zaškrtněte políčko u .NET Compiler Platform SDK. Najdete ho jako poslední pod volitelnými komponentami.

Volitelně můžete také chtít, aby editor DGML zobrazoval grafy ve vizualizéru:

  1. Otevřete uzel Jednotlivé komponenty ve stromu souhrnu.
  2. Zaškrtněte políčko editoru DGML.

Instalace pomocí karty Instalační program pro Visual Studio – jednotlivé komponenty

  1. Spustit Instalační program pro Visual Studio
  2. Vyberte Upravit.
  3. Vyberte kartu Jednotlivé komponenty .
  4. Zaškrtněte políčko u .NET Compiler Platform SDK. Najdete ho nahoře v části Kompilátory, nástroje sestavení a moduly runtime .

Volitelně můžete také chtít, aby editor DGML zobrazoval grafy ve vizualizéru:

  1. Zaškrtněte políčko Editor DGML. Najdete ho v části Nástroje pro kód .

Principy syntaktických stromů

Rozhraní API syntaxe se používá pro jakoukoli analýzu struktury kódu jazyka C#. Rozhraní API syntaxe zveřejňuje analyzátory, syntaktické stromy a nástroje pro analýzu a vytváření syntaktických stromů. Je to způsob, jak hledat kód pro konkrétní prvky syntaxe nebo číst kód programu.

Syntaktická strom je datová struktura používaná kompilátory jazyka C# a Visual Basic k pochopení programů v jazyce C# a Visual Basic. Stromy syntaxe jsou vytvářeny stejným analyzátorem, který se spouští, když je projekt sestaven nebo když vývojář narazí na F5. Stromy syntaxe mají plnou věrnost jazyka; Každý bit informací v souboru kódu je reprezentován ve stromu. Zápisem syntaktického stromu na text se reprodukuje přesně původní text, který byl analyzován. Stromy syntaxe jsou také neměnné; po vytvoření syntaktického stromu nelze nikdy změnit. Příjemci stromů můžou analyzovat stromy na více vláknech, bez zámků nebo jiných měr souběžnosti, protože vědí, že se data nikdy nezmění. Pomocí rozhraní API můžete vytvářet nové stromy, které jsou výsledkem úpravy existujícího stromu.

Čtyři hlavní stavební bloky syntaktických stromů jsou:

Trivia, tokeny a uzly se skládají hierarchicky tak, aby vytvořily strom, který zcela představuje vše v fragmentu kódu jazyka Visual Basic nebo C#. Tuto strukturu můžete zobrazit pomocí okna Vizualizéru syntaxe . V sadě Visual Studio zvolte Zobrazit> dalšívizualizér syntaxeWindows>. Například předchozí zdrojový soubor jazyka C# prozkoumaný pomocí vizualizéru syntaxe vypadá jako na následujícím obrázku:

SyntaxNode: Modrý | SyntaxToken: Zelená | SyntaxTrivia: Red C# Code File

Když přejdete do této stromové struktury, můžete v souboru kódu najít libovolný příkaz, výraz, token nebo trochu prázdného místa.

I když můžete v souboru kódu najít cokoli pomocí rozhraní API syntaxe, většina scénářů zahrnuje zkoumání malých fragmentů kódu nebo vyhledávání konkrétních příkazů nebo fragmentů. Následující dva příklady ukazují typické použití k procházení struktury kódu nebo hledání jednotlivých příkazů.

Procházení stromů

Uzly ve stromu syntaxe můžete prozkoumat dvěma způsoby. Můžete procházet strom a prozkoumat jednotlivé uzly nebo se můžete dotazovat na konkrétní prvky nebo uzly.

Ruční procházení

Hotový kód pro tuto ukázku si můžete prohlédnout v našem úložišti GitHub.

Poznámka

Typy stromu syntaxe používají dědičnost k popisu různých elementů syntaxe, které jsou platné v různých umístěních v programu. Použití těchto rozhraní API často znamená přetypování vlastností nebo členů kolekce na konkrétní odvozené typy. V následujících příkladech jsou přiřazení a přetypování samostatnými příkazy, které používají proměnné s explicitně zadanými typy. Můžete si přečíst kód a zobrazit návratové typy rozhraní API a typ modulu runtime vrácených objektů. V praxi je častější používat implicitně typované proměnné a spoléhat se na názvy rozhraní API k popisu typu zkoumaných objektů.

Vytvořte nový projekt samostatného nástroje pro analýzu kódu v jazyce C#:

  • V sadě Visual Studio zvolte Soubor>Nový>projekt , aby se zobrazilo dialogové okno Nový projekt.
  • V části Visual C#>Extensibility (Rozšiřitelnost) zvolte Stand-Alone Code Analysis Tool (Samostatný nástroj pro analýzu kódu).
  • Projekt pojmenujte SyntaxTreeManualTraversal a klikněte na OK.

Budete analyzovat základní program "Hello World!" zobrazený dříve. Přidejte text pro Hello World program jako konstantu ve tříděProgram:

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

Dále přidejte následující kód, který vytvoří strom syntaxe pro text kódu v konstantě programText . Do metody Main přidejte následující řádek:

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

Tyto dva řádky vytvoří strom a načtou kořenový uzel tohoto stromu. Teď můžete prozkoumat uzly ve stromové struktuře. Přidejte do metody Main tyto řádky, abyste zobrazili některé vlastnosti kořenového uzlu ve stromu:

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

Spusťte aplikaci a podívejte se, co váš kód zjistil o kořenovém uzlu v tomto stromu.

Obvykle byste mohli procházet strom, abyste se dozvěděli o kódu. V tomto příkladu analyzujete kód, který znáte, abyste prozkoumali rozhraní API. Přidejte následující kód pro prozkoumání prvního člena root uzlu:

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

Tento člen je .Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax Představuje vše v rozsahu namespace HelloWorld deklarace. Přidejte následující kód, který zkontroluje, jaké uzly jsou deklarovány v rámci oboru HelloWorld názvů:

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

Spusťte program a podívejte se, co jste se naučili.

Teď, když víte, že deklarace je Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, deklarujte novou proměnnou tohoto typu, abyste prozkoumali deklaraci třídy. Tato třída obsahuje pouze jeden člen: metodu Main . Přidejte následující kód, který najde metodu Main a přetypuje ji na 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];

Uzel deklarace metody obsahuje všechny syntaktické informace o metodě. Pojďme zobrazit návratový Main typ metody, počet a typy argumentů a základní text metody. Přidejte následující kód:

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

Spuštěním programu zobrazte všechny informace, které jste o tomto programu zjistili:

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 dotazů

Kromě procházení stromů můžete strom syntaxe prozkoumat také pomocí metod dotazů definovaných v nástroji Microsoft.CodeAnalysis.SyntaxNode. Tyto metody by měly být okamžitě obeznámeny s XPath. Pomocí těchto metod s LINQ můžete rychle najít věci ve stromu. Obsahuje SyntaxNode metody dotazu, jako DescendantNodesjsou , AncestorsAndSelf a ChildNodes.

Tyto metody dotazu můžete použít k vyhledání argumentu pro metodu Main jako alternativu k navigaci ve stromu. Na konec Main metody přidejte následující kód:

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

První příkaz používá výraz LINQ a metodu DescendantNodes k vyhledání stejného parametru jako v předchozím příkladu.

Spusťte program a uvidíte, že výraz LINQ našel stejný parametr jako ruční navigace ve stromu.

Ukázka používá WriteLine příkazy k zobrazení informací o stromech syntaxe při procházení. Mnohem víc se dozvíte také spuštěním dokončeného programu v ladicím programu. Můžete prozkoumat více vlastností a metod, které jsou součástí stromu syntaxe vytvořeného pro program hello world.

Chodci syntaxe

Často chcete najít všechny uzly určitého typu ve stromu syntaxe, například každou deklaraci vlastnosti v souboru. Rozšířením Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker třídy a přepsáním VisitPropertyDeclaration(PropertyDeclarationSyntax) metody zpracujete každou deklaraci vlastnosti ve stromu syntaxe, aniž byste předem věděli její strukturu. CSharpSyntaxWalker je specifický druh CSharpSyntaxVisitor , který rekurzivně navštěvuje uzel a každou jeho podřízenou položku.

Tento příklad implementuje CSharpSyntaxWalker objekt, který zkoumá strom syntaxe. Shromažďuje nalezené direktivy using , které neimportují System obor názvů.

Vytvořit nový projekt samostatného nástroje pro analýzu kódu v jazyce C#; pojmenujte ho "SyntaxWalker".

Hotový kód pro tuto ukázku si můžete prohlédnout v našem úložišti GitHub. Ukázka na GitHubu obsahuje oba projekty popsané v tomto kurzu.

Stejně jako v předchozí ukázce můžete definovat řetězcovou konstantu, která bude obsahovat text programu, který budete analyzovat:

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

Tento zdrojový text obsahuje using direktivy rozptýlené na čtyřech různých místech: na úrovni souboru, v oboru názvů nejvyšší úrovně a ve dvou vnořených oborech názvů. Tento příklad zvýrazňuje základní scénář použití CSharpSyntaxWalker třídy k dotazování kódu. Bylo by těžkopádné navštívit všechny uzly v kořenovém stromu syntaxe a najít pomocí deklarací. Místo toho vytvoříte odvozenou třídu a přepíšete metodu, která se volá pouze v případě, že aktuální uzel ve stromu je direktivou using. Návštěvník nepracuje na žádném jiném typu uzlů. Tato jediná metoda prozkoumá jednotlivé using příkazy a vytvoří kolekci oborů názvů, které nejsou v System oboru názvů. Vytvoříte příkaz CSharpSyntaxWalker , který prozkoumá všechny using příkazy, ale jenom using příkazy.

Teď, když jste definovali text programu, musíte vytvořit SyntaxTree a získat kořen tohoto stromu:

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

Dále vytvořte novou třídu. V sadě Visual Studio zvolte Přidatnovou položkuprojektu>. V dialogovém okně Přidat novou položku jako název souboru zadejte UsingCollector.cs .

Implementujete using funkci návštěvníka ve UsingCollector třídě. Začněte tím, že třídu odvozíte UsingCollector z CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

K uložení uzlů oboru názvů, které shromažďujete, potřebujete úložiště. Deklarujte ve UsingCollector třídě veřejnou vlastnost jen pro čtení. Pomocí této proměnné uložíte UsingDirectiveSyntax uzly, které najdete:

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

Základní třída implementuje logiku pro CSharpSyntaxWalker návštěvu jednotlivých uzlů ve stromu syntaxe. Odvozená třída přepíše metody volané pro konkrétní uzly, které vás zajímají. V tomto případě vás zajímají všechny using direktivy. To znamená, že musíte přepsat metodu VisitUsingDirective(UsingDirectiveSyntax) . Jedním argumentem pro tuto metodu Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax je objekt. To je důležitá výhoda použití návštěvníků: volají přepsané metody s argumenty, které už jsou přetypované na konkrétní typ uzlu. Třída Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntaxName vlastnost, která ukládá název importovaného oboru názvů. Jedná se o Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Do přepsání přidejte následující kód 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);
    }
}

Stejně jako v předchozím příkladu jste přidali různé WriteLine příkazy, které vám pomůžou porozumět této metodě. Uvidíte, kdy se volá a jaké argumenty se do něj pokaždé předají.

Nakonec musíte přidat dva řádky kódu, abyste vytvořili UsingCollector a aby navštívil kořenový uzel a shromažďovaly using všechny příkazy. Potom přidejte smyčku foreach , která zobrazí všechny using příkazy nalezené v kolekci:

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

Zkompilujte a spusťte program. Měl by se zobrazit následující výstup:

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

Gratulujeme! Použili jste rozhraní API syntaxe k vyhledání konkrétních typů příkazů a deklarací jazyka C# ve zdrojovém kódu jazyka C#.