Condividi tramite


Introduzione all'analisi della sintassi

In questa esercitazione verrà esaminata l'API Syntax. L'API Syntax consente l'accesso alle strutture di dati che descrivono un programma C# o Visual Basic. Queste strutture di dati includono dettagli sufficienti per rappresentare completamente un programma di qualsiasi dimensione. Queste strutture possono descrivere programmi completi che vengono compilati ed eseguiti correttamente. Possono anche descrivere programmi incompleti, durante la scrittura, nell'editor.

Per rendere possibile questa espressione elaborata, le strutture di dati e le API che costituiscono l'API Syntax sono necessariamente complesse. Per iniziare, verrà esaminato 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!");
        }
    }
}

Esaminando il testo del programma precedente si riconoscono elementi noti. L'intero testo rappresenta un singolo file di origine, ovvero un'unità di compilazione. Le prime tre righe di tale file di origine sono direttive using. L'origine rimanente è contenuta in una dichiarazione dello spazio dei nomi. La dichiarazione dello spazio dei nomi contiene una dichiarazione di classe figlio. La dichiarazione di classe contiene una dichiarazione di metodo.

L'API Syntax crea una struttura ad albero con la radice che rappresenta l'unità di compilazione. I nodi nell'albero rappresentano le direttive using, la 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 letterale stringa che è un discendente di un argomento. L'API Syntax consente l'accesso alla struttura del programma. È possibile eseguire query per ottenere specifiche procedure consigliate per la scrittura del codice, ripercorrere l'intera struttura ad albero per comprendere il codice e creare nuovi alberi modificando quello esistente.

Questa breve descrizione offre una panoramica del tipo di informazioni accessibili tramite l'API Syntax. L'API Syntax non è altro che un'API formale che descrive i costrutti di codice familiari, già noti da C#. Le funzionalità complete includono informazioni sulla formattazione del codice, inclusi spazi vuoti, interruzioni di riga e rientri. Usando queste informazioni è possibile rappresentare completamente il codice nel modo in cui viene scritto e letto dai programmatori umani o dal compilatore. L'uso di questa struttura consente di interagire con il codice sorgente su un livello molto significativo. Non si tratta più di semplici stringhe di testo, ma di 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 dei carichi di lavoro

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

  1. Eseguire il programma di installazione di Visual Studio.
  2. Selezionare Modifica
  3. Selezionare il carico di lavoro Sviluppo di estensioni di Visual Studio.
  4. Aprire il nodo Sviluppo di estensioni di Visual Studio nell'albero di riepilogo.
  5. Selezionare la casella di controllo per .NET Compiler Platform SDK. È l'ultima voce dei componenti facoltativi.

Facoltativamente, è possibile installare anche l'editor DGML per visualizzare 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 il Programma di installazione di Visual Studio: scheda Singoli componenti

  1. Eseguire il programma di installazione di Visual Studio.
  2. Selezionare Modifica
  3. Selezionare la scheda Singoli componenti
  4. Selezionare la casella di controllo per .NET Compiler Platform SDK. È la prima voce nella sezione Compilatori, strumenti di compilazione e runtime.

Facoltativamente, è possibile installare anche l'editor DGML per visualizzare i grafici nel visualizzatore:

  1. Selezionare la casella di controllo per Editor DGML. La voce è disponibile nella sezione Strumenti per il codice.

Informazioni sugli alberi della sintassi

È possibile usare l'API Syntax per qualsiasi analisi della struttura di codice C#. L'API Syntax espone i parser, gli alberi della sintassi e le utilità per l'analisi e costruzione di alberi della sintassi. Si tratta del modo in cui si cercano elementi di sintassi specifici nel codice 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 prodotti dallo stesso parser eseguito quando viene compilato un progetto o uno sviluppatore preme F5. Gli alberi della sintassi garantiscono la totale fedeltà al linguaggio. Ogni elemento di informazioni in un file di codice è rappresentato nell'albero. La scrittura di un albero della sintassi in formato di testo riproduce l'esatto testo originale analizzato. Gli alberi di sintassi sono anche non modificabili, ovvero non possono essere mai modificati dopo la creazione. I consumer degli alberi possono analizzarli su più thread, senza blocchi o altre misure di concorrenza, dando per scontato che i dati non cambiano mai. È possibile usare le API per creare nuovi alberi risultanti dalla modifica di un albero esistente.

I quattro principali blocchi predefiniti degli alberi della sintassi sono:

Gli elementi semplici, i token e i nodi sono composti in modo gerarchico per formare un albero che rappresenta completamente tutti gli elementi in un frammento di codice Visual Basic o C#. È possibile visualizzare questa struttura usando la finestra Syntax Visualizer (Visualizzatore sintassi). In Visual Studio scegliere Visualizza> altrovisualizzatore sintassidi Windows>. Ad esempio, il file di origine C# precedente esaminato nella finestra Syntax Visualizer (Visualizzatore sintassi) ha l'aspetto illustrato nella figura seguente:

SintassiNode: Blu | SintassiToken: Verde | SintassiTrivia: file di codice C# rosso

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

Anche se è possibile trovare qualsiasi elemento in un file di codice usando le API Syntax, la maggior parte degli scenari d'uso riguarda l'analisi di piccoli frammenti di codice o la ricerca di istruzioni o frammenti specifici. Gli esempi che seguono mostrano due usi tipici per esplorare la struttura del codice o cercare singole istruzioni.

Attraversamento degli alberi

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

Attraversamento manuale

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

Nota

I tipi di albero della sintassi usano l'ereditarietà per descrivere i diversi elementi della sintassi validi in posizioni diverse nel programma. L'uso di queste API spesso significa eseguire il cast di proprietà o membri di raccolte in tipi derivati specifici. Negli esempi seguenti, l'assegnazione e i cast sono istruzioni separate, con variabili tipizzate 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 delle API per descrivere il tipo di oggetti in corso di analisi.

Creare un nuovo progetto C# Stand-Alone Code Analysis Tool (Strumento di analisi del codice autonomo):

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

Si analizzerà il programma "Hello World!" di base mostrato in precedenza. Aggiungere il testo per il programma Hello World come costante nella classe 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!"");
        }
    }
}";

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

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

Queste due righe creano l'albero e ne recuperano il nodo radice. È ora possibile esaminare i nodi dell'albero. Aggiungere queste righe al metodo Main 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 statements. They are:");
foreach (UsingDirectiveSyntax element in root.Usings)
    WriteLine($"\t{element.Name}");

Eseguire l'applicazione per vedere cosa ha individuato il codice sul nodo radice in questo albero.

In genere, l'attraversamento del codice viene eseguito per acquisire informazioni sul codice. In questo esempio, viene analizzato codice noto per esplorare le API. Aggiungere il codice seguente per esaminare il primo membro del nodo root:

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

Tale membro è di tipo Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax e rappresenta tutto nell'ambito della dichiarazione namespace HelloWorld. Aggiungere il codice seguente per individuare 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 verificare quanto appreso.

Ora che è noto che la dichiarazione è un Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, dichiarare una nuova variabile di quel tipo per esaminare la dichiarazione di classe. Questa classe contiene solo un membro: il metodo Main. Aggiungere il codice seguente per trovare il metodo Main ed eseguirne il cast su 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. A questo punto verranno visualizzati il tipo restituito del metodo Main, 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 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!");
        }

Metodi di query

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

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

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 metodo DescendantNodes per individuare lo stesso parametro, come nell'esempio precedente.

Eseguire il programma. Si può notare che l'espressione LINQ ha trovato lo stesso parametro individuato con l'esplorazione manuale dell'albero.

L'esempio usa istruzioni WriteLine per visualizzare informazioni sugli alberi della sintassi durante l'attraversamento. È anche possibile ottenere molte più informazioni eseguendo il programma terminato nel debugger. È possibile esaminare molti più metodi e proprietà che fanno parte dell'albero della sintassi creato per il programma Hello World.

Percorrere in modo ricorsivo la sintassi

Spesso è necessario trovare tutti i nodi di un tipo specifico in un albero della sintassi, ad esempio ogni dichiarazione di proprietà in un file. Con l'estensione della classe Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker e l'override del metodo VisitPropertyDeclaration(PropertyDeclarationSyntax), si elabora ogni dichiarazione di proprietà in un albero della sintassi senza conoscerne in anticipo la struttura. CSharpSyntaxWalker è un tipo specifico di CSharpSyntaxVisitor che visita in modo ricorsivo un nodo e tutti i relativi nodi figlio.

Questo esempio implementa un CSharpSyntaxWalker che esamina un albero della sintassi. Raccoglie le direttive using individuate, che non importano uno spazio dei nomi System.

Creare un nuovo progetto C# Stand-Alone Code Analysis Tool (Strumento di analisi del codice autonomo) e denominarlo "SyntaxWalker."

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

Come nell'esempio precedente, è possibile definire una costante stringa per contenere il testo del programma che verrà analizzato:

        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 diverse posizioni: nel livello file, nello spazio dei nomi di livello superiore e nei due spazi dei nomi annidati. Questo esempio mette in evidenza uno scenario fondamentale per l'uso della classe CSharpSyntaxWalker per eseguire query nel codice. Sarebbe un'operazione complessa visitare ogni nodo nell'albero della sintassi radice per trovare le dichiarazioni using. Si crea invece una classe derivata e si esegue l'override del metodo che viene chiamato solo quando il nodo corrente nell'albero è una direttiva using. Non vengono eseguite altre operazioni su qualsiasi altro tipo di nodo. Questo singolo metodo esamina ogni istruzione using e crea una raccolta degli spazi dei nomi non inclusi nello spazio dei nomi System. Si compila un CSharpSyntaxWalker che esamina tutte le istruzioni using, ma solo le istruzioni using.

Dopo aver definito il testo del programma, è necessario creare un SyntaxTree e ottenere la radice di tale albero:

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

Creare poi una nuova classe. In Visual Studio scegliereAggiungi nuovo elemento del progetto>. Nella finestra di dialogo Aggiungi nuovo elemento digitare UsingCollector.cs come nome del file.

Implementare la funzionalità del visitatore di using nella classe UsingCollector. Per iniziare, far derivare la classe UsingCollector da CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

È necessario spazio di archiviazione per i nodi dello spazio dei nomi raccolti. Dichiarare una proprietà pubblica di sola lettura nella classe UsingCollector. Questa variabile viene usata per archiviare i nodi UsingDirectiveSyntax trovati:

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

La classe di base, CSharpSyntaxWalker 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, si è interessati a qualsiasi direttiva using. Questo significa che è necessario eseguire l'override del metodo VisitUsingDirective(UsingDirectiveSyntax). L'unico argomento di questo metodo è un oggetto Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax. Ciò rappresenta un vantaggio importante rispetto all'uso dei visitatori: i metodi sottoposti a override vengono chiamati con argomenti di cui è già stato eseguito il cast al tipo di nodo specifico. La classe Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax include una proprietà Name che archivia il nome dello spazio dei nomi importato. Si tratta di un oggetto Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Aggiungere il codice seguente nell'override di 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, sono state aggiunte svariate istruzioni WriteLine per facilitare la comprensione di questo metodo. È possibile vedere quando viene chiamato e quali argomenti vengono passati al metodo ogni volta.

Infine, è necessario aggiungere due righe di codice per creare UsingCollector e fare in modo che visiti il nodo radice raccogliendo tutte le istruzioni using. Aggiungere quindi un ciclo foreach per visualizzare tutte le istruzioni using trovate dallo strumento di raccolta:

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

Compilare ed eseguire il programma. Dovrebbe venire 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 Syntax per individuare tipi specifici di istruzioni e dichiarazioni C# in codice sorgente C#.