Condividi tramite


Introduzione all'analisi della sintassi

In questa esercitazione si esaminerà l'API Sintassi. L'API Sintassi consente di accedere alle strutture di dati che descrivono un programma C# o Visual Basic. Queste strutture di dati hanno un numero sufficiente di dettagli che possono rappresentare completamente qualsiasi programma di qualsiasi dimensione. Queste strutture possono descrivere programmi completi che vengono compilati ed eseguiti correttamente. Possono anche descrivere programmi incompleti, mentre li scrivi, nell'editor.

Per abilitare questa espressione avanzata, le strutture di dati e le API che costituiscono l'API sintassi sono necessariamente complesse. Si inizierà con l'aspetto della struttura dei dati per il tipico programma "Hello World":

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

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

Esaminare il testo del programma precedente. Riconosci elementi familiari. L'intero testo rappresenta un singolo file di origine o un'unità di compilazione. Le prime tre righe del file di origine usano direttive. L'origine rimanente è contenuta in una dichiarazione dello spazio dei nomi. La dichiarazione dello spazio dei nomi contiene una dichiarazione di classe figlia. La dichiarazione di classe contiene una dichiarazione di metodo.

L'API Sintassi crea una struttura ad albero con la radice che rappresenta l'unità di compilazione. I nodi nell'albero rappresentano le direttive, la using dichiarazione dello spazio dei nomi e tutti gli altri elementi del programma. La struttura ad albero continua fino ai livelli più bassi: la stringa "Hello World!" è un token di stringa letterale che discende da un argomento. L'API Sintassi fornisce l'accesso alla struttura del programma. È possibile eseguire query per procedure di codice specifiche, esaminare l'intero albero per comprendere il codice e creare nuovi alberi modificando l'albero esistente.

Questa breve descrizione fornisce una panoramica del tipo di informazioni accessibili tramite l'API Sintassi. L'API Sintassi non è altro che un'API formale che descrive i costrutti di codice noti da C#. Le funzionalità complete includono informazioni sulla formattazione del codice, incluse interruzioni di riga, spazi bianchi e rientro. Usando queste informazioni, è possibile rappresentare completamente il codice scritto e letto dai programmatori umani o dal compilatore. L'uso di questa struttura consente di interagire con il codice sorgente a un livello profondamente significativo. Non sono più stringhe di testo, ma dati che rappresentano la struttura di un programma C#.

Per iniziare, è necessario installare .NET Compiler Platform SDK:

Istruzioni di installazione - Programma di installazione di Visual Studio

Esistono due modi diversi per trovare .NET Compiler Platform SDK nel programma di installazione di Visual Studio:

Eseguire l'installazione con il programma di installazione di Visual Studio - Visualizzazione Carichi di lavoro

Il .NET Compiler Platform SDK non viene selezionato automaticamente come parte del carico di lavoro per lo sviluppo di estensioni di Visual Studio. È necessario selezionarlo come componente facoltativo.

  1. Eseguire il programma di installazione di Visual Studio
  2. Selezionare Modifica
  3. Verificare l'attività di sviluppo delle estensioni di Visual Studio.
  4. Aprire il nodo di sviluppo dell'estensione di Visual Studio nell'albero di riepilogo.
  5. Assicurarsi che la casella .NET Compiler Platform SDK sia selezionata.
  6. Selezionare Modifica.

Facoltativamente, è anche necessario che l'editor DGML visualizzi i grafici nel visualizzatore:

  1. Aprire il nodo Singoli componenti nell'albero di riepilogo.
  2. Selezionare la casella per l'editor DGML

Eseguire l'installazione usando la scheda Programma di installazione di Visual Studio - Singoli componenti

  1. Eseguire il programma di installazione di Visual Studio
  2. Selezionare Modifica
  3. Selezionare la scheda Singoli componenti
  4. Selezionare la casella .NET Compiler Platform SDK. È disponibile nella parte superiore della sezione Compilatori, strumenti di compilazione e runtime .
  5. Selezionare Modifica.

Facoltativamente, è anche necessario che l'editor DGML visualizzi i grafici nel visualizzatore:

  1. Selezionare la casella di controllo per l'editor DGML. È disponibile nella sezione Strumenti di codice .

Comprendere gli alberi sintattici

Usare l'API Sintassi per qualsiasi analisi della struttura del codice C#. L'API Sintassi espone i parser, gli alberi della sintassi e le utilità per l'analisi e la costruzione di alberi della sintassi. Si tratta di come si cerca codice per elementi di sintassi specifici o si legge il codice per un programma.

Un albero della sintassi è una struttura di dati usata dai compilatori C# e Visual Basic per comprendere i programmi C# e Visual Basic. Gli alberi della sintassi vengono generati dallo stesso parser eseguito quando viene compilato un progetto o uno sviluppatore raggiunge F5. Gli alberi della sintassi hanno la massima fedeltà con il linguaggio; ogni bit di informazioni in un file di codice è rappresentato nell'albero. La scrittura di un albero della sintassi nel testo riproduce il testo originale esatto analizzato. Anche gli alberi della sintassi non sono modificabili; una volta creato un albero della sintassi non può mai essere modificato. Gli utilizzatori degli alberi possono analizzare gli alberi su più thread, senza blocchi o altre misure di concorrenza, sapendo che i dati non cambiano mai. È possibile usare le API per creare nuovi alberi risultanti dalla modifica di un albero esistente.

I quattro blocchi predefiniti principali degli alberi della sintassi sono:

I trivia, i token e i nodi sono composti gerarchicamente per formare un albero che rappresenta completamente tutto ciò che si trova in un frammento di codice Visual Basic o C#. È possibile visualizzare questa struttura usando la finestra Visualizzatore di sintassi . In Visual Studio, scegliere Visualizza>Altre finestre>Visualizzatore sintassi. Ad esempio, il file di origine C# precedente esaminato usando il visualizzatore di sintassi è simile alla figura seguente:

SyntaxNode: Blu | SyntaxToken: Verde | SyntaxTrivia: File di codice C# rosso

Spostandosi in questa struttura ad albero, è possibile trovare qualsiasi istruzione, espressione, token o bit di spazio vuoto in un file di codice.

Sebbene sia possibile trovare qualsiasi elemento in un file di codice usando le API di sintassi, la maggior parte degli scenari implica l'analisi di piccoli frammenti di codice o la ricerca di istruzioni o frammenti specifici. I due esempi seguenti mostrano usi tipici per esplorare la struttura del codice o cercare singole istruzioni.

Attraversamento di alberi

È possibile esaminare i nodi in un albero della sintassi in due modi. È possibile attraversare l'albero per esaminare ogni nodo oppure eseguire query per elementi o nodi specifici.

Attraversamento manuale

È possibile visualizzare il codice completato per questo esempio nel repository GitHub.

Annotazioni

I tipi albero della sintassi usano l'ereditarietà per descrivere i diversi elementi della sintassi validi in posizioni diverse del programma. L'uso di queste API spesso significa effettuare il casting di proprietà o membri di raccolta a tipi derivati specifici. Negli esempi seguenti, l'assegnazione e i cast sono istruzioni separate, usando variabili tipate in modo esplicito. È possibile leggere il codice per visualizzare i tipi restituiti dell'API e il tipo di runtime degli oggetti restituiti. In pratica, è più comune usare variabili tipizzate in modo implicito e basarsi sui nomi api per descrivere il tipo di oggetti da esaminare.

Creare un nuovo progetto C# Stand-Alone Code Analysis Tool :

  • In Visual Studio scegliere File>nuovo>progetto per visualizzare la finestra di dialogo Nuovo progetto.
  • In Visual C#>Extensibility scegliere Strumento di analisi del codice indipendente.
  • Denominare il progetto "SyntaxTreeManualTraversal" e fare clic su OK.

Si analizzerà il programma "Hello World!" di base illustrato in precedenza. Aggiungere il testo per il programma Hello World come costante nella Program classe:

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

Aggiungere quindi il codice seguente per compilare l'albero della sintassi per il testo del codice nella programText costante . Aggiungere la riga seguente al Main metodo :

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

Queste due righe creano l'albero e recuperano il nodo radice di tale albero. È ora possibile esaminare i nodi nell'albero. Aggiungere queste righe al Main metodo per visualizzare alcune delle proprietà del nodo radice nell'albero:

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 directives. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

Esegui l'applicazione per vedere cosa il tuo codice ha scoperto sul nodo radice di questo albero.

In genere, si percorre l'albero per ottenere informazioni sul codice. In questo esempio si sta analizzando il codice noto per esplorare le API. Aggiungere il codice seguente per esaminare il primo membro del root nodo:

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

Quel membro è un Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Rappresenta tutti gli elementi nell'ambito della namespace HelloWorld dichiarazione. Aggiungi il codice seguente per esaminare i nodi dichiarati all'interno dello spazio dei nomi HelloWorld.

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

Eseguire il programma per vedere cosa si è appreso.

Ora che si sa che la dichiarazione è un Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, dichiarare una nuova variabile di tale tipo per esaminare la dichiarazione di classe. Questa classe contiene solo un membro: il Main metodo . Aggiungere il seguente codice per trovare il metodo Main ed eseguirne il cast in un 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];

Il nodo della dichiarazione del metodo contiene tutte le informazioni sintattiche sul metodo . Verrà ora visualizzato il tipo restituito del Main metodo, il numero e i tipi degli argomenti e il testo del corpo del metodo. Aggiungere il codice seguente:

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

Eseguire il programma per visualizzare tutte le informazioni individuate su questo programma:

The tree is a CompilationUnit node.
The tree has 1 elements in it.
The tree has 4 using directives. 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!");
        }

Metodi di interrogazione

Oltre a attraversare alberi, è anche possibile esplorare l'albero della sintassi usando i metodi di query definiti in Microsoft.CodeAnalysis.SyntaxNode. Questi metodi devono essere immediatamente familiari con chiunque abbia familiarità con XPath. È possibile usare questi metodi con LINQ per trovare rapidamente elementi in un albero. SyntaxNode dispone di metodi di query come DescendantNodes, AncestorsAndSelf e ChildNodes.

È possibile usare questi metodi di query per trovare l'argomento per il Main metodo come alternativa all'esplorazione dell'albero. Aggiungere il codice seguente alla fine del Main metodo:

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

La prima istruzione usa un'espressione LINQ e il DescendantNodes metodo per individuare lo stesso parametro dell'esempio precedente.

Eseguire il programma, ed è possibile notare che l'espressione LINQ ha trovato lo stesso parametro dell'esplorazione manuale dell'albero.

Nell'esempio vengono utilizzate istruzioni WriteLine per visualizzare informazioni sugli alberi della sintassi mentre vengono attraversati. È anche possibile ottenere altre informazioni eseguendo il programma completato nel debugger. È possibile esaminare più proprietà e metodi che fanno parte dell'albero della sintassi creato per il programma hello world.

Analizzatori di sintassi

Spesso si vogliono trovare tutti i nodi di un tipo specifico in un albero della sintassi, ad esempio ogni dichiarazione di proprietà in un file. Estendi la classe Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker e sovrascrivi il metodo VisitPropertyDeclaration(PropertyDeclarationSyntax), elaborando ogni dichiarazione di proprietà in un albero della sintassi senza conoscerne la struttura in anticipo. CSharpSyntaxWalker è un tipo specifico di CSharpSyntaxVisitor che visita in modo ricorsivo un nodo e ognuno dei relativi elementi figlio.

In questo esempio viene implementato un oggetto CSharpSyntaxWalker che esamina un albero della sintassi. Raccoglie le direttive rilevate using che non importano un System spazio dei nomi.

Creare un nuovo progetto C# Stand-Alone Code Analysis Tool ; denominarlo "SyntaxWalker".

È possibile visualizzare il codice completato per questo esempio nel repository GitHub. L'esempio in GitHub contiene entrambi i progetti descritti in questa esercitazione.

Come nell'esempio precedente, è possibile definire una costante stringa per contenere il testo del programma da analizzare:

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

Questo testo di origine contiene direttive using distribuite in quattro posizioni diverse: a livello di file, nello spazio dei nomi di primo livello e nei due spazi dei nomi annidati. In questo esempio viene evidenziato uno scenario di base per l'uso della classe per eseguire query sul CSharpSyntaxWalker codice. Sarebbe complesso visitare ogni nodo nell'albero della sintassi radice per trovare le dichiarazioni using. Al contrario, si crea una classe derivata ed si esegue l'override del metodo che viene chiamato solo quando il nodo corrente nell'albero è una using direttiva. Il visitatore non esegue alcuna operazione su altri tipi di nodo. Questo singolo metodo esamina ciascuna delle using direttive e compila una raccolta dei namespace che non si trovano nel namespace System. Si compila un oggetto CSharpSyntaxWalker che esamina tutte le using direttive, ma solo le using direttive .

Dopo aver definito il testo del programma, è necessario creare un SyntaxTree oggetto e ottenere la radice dell'albero:

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

Creare quindi una nuova classe. In Visual Studio scegliere Progetto>Aggiungi nuovo elemento. Nella finestra di dialogo Aggiungi nuovo elementodigitare UsingCollector.cs come nome file.

Implementate la funzionalità using visitor nella classe UsingCollector. Per iniziare, fare in modo che la UsingCollector classe derivi da CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

È necessario spazio di archiviazione per contenere i nodi dello spazio dei nomi che stai raccogliendo. Dichiarare una proprietà di sola lettura pubblica nella UsingCollector classe . Usare questa variabile per archiviare i UsingDirectiveSyntax nodi disponibili:

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

La classe CSharpSyntaxWalker base implementa la logica per visitare ogni nodo nell'albero della sintassi. La classe derivata esegue l'override dei metodi chiamati per i nodi specifici a cui si è interessati. In questo caso, ti interessa ogni using direttiva. Ciò significa che è necessario eseguire l'override del VisitUsingDirective(UsingDirectiveSyntax) metodo . L'argomento di questo metodo è un oggetto Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax. Questo è un vantaggio importante per l'impiego dei visitatori: chiamano i metodi sovrascritti con argomenti già convertiti al tipo di nodo specifico. La Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax classe ha una Name proprietà che archivia il nome dello spazio dei nomi da importare. Si tratta di un oggetto Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Aggiungere il codice seguente nell'override 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);
    }
}

Come per l'esempio precedente, hai aggiunto diverse WriteLine istruzioni per facilitare la comprensione di questo metodo. È possibile vedere quando viene chiamato e quali argomenti vengono passati ogni volta.

Infine, è necessario aggiungere due righe di codice per creare UsingCollector e fare in modo che visiti il nodo radice, raccogliendo tutte le using direttive. Aggiungere quindi un foreach ciclo per visualizzare tutte le direttive che il vostro using collettore ha trovato:

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

Compilare ed eseguire il programma. Verrà visualizzato l'output seguente:

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

Congratulazioni! È stata usata l'API Sintassi per individuare tipi specifici di direttive e dichiarazioni nel codice sorgente C#.