Partilhar via


Introdução à análise de sintaxe

Neste tutorial, irá explorar a API de Sintaxe. A API de Sintaxe fornece acesso às estruturas de dados que descrevem um programa C# ou Visual Basic. Estas estruturas de dados têm detalhes suficientes que podem representar totalmente qualquer programa de qualquer tamanho. Estas estruturas podem descrever programas completos que compilam e são executados corretamente. Também podem descrever programas incompletos, à medida que os escreve, no editor.

Para ativar esta expressão avançada, as estruturas de dados e as APIs que compõem a API de Sintaxe são necessariamente complexas. Vamos começar com o aspeto da estrutura de dados para o programa "Hello World" típico:

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

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

Observe o texto do programa anterior. Reconhece elementos familiares. O texto inteiro representa um único ficheiro de origem ou uma unidade de compilação. As primeiras três linhas desse ficheiro de origem estão a utilizar diretivas. A origem restante está contida numa declaração de espaço de nomes. A declaração de espaço de nomes contém uma declaração de classe subordinada. A declaração de classe contém uma declaração de método.

A API de Sintaxe cria uma estrutura de árvore com a raiz que representa a unidade de compilação. Os nós na árvore representam as diretivas de utilização, a declaração de espaço de nomes e todos os outros elementos do programa. A estrutura de árvore continua até aos níveis mais baixos: a cadeia "Hello World!" é um token literal de cadeia que é descendente de um argumento. A API de Sintaxe fornece acesso à estrutura do programa. Pode consultar práticas de código específicas, percorrer toda a árvore para compreender o código e criar novas árvores ao modificar a árvore existente.

Essa breve descrição fornece uma descrição geral do tipo de informações acessíveis através da API de Sintaxe. A API de Sintaxe não passa de uma API formal que descreve as construções de código familiares que conhece do C#. As capacidades completas incluem informações sobre como o código é formatado, incluindo quebras de linha, espaço em branco e avanço. Com estas informações, pode representar totalmente o código como escrito e lido por programadores humanos ou pelo compilador. A utilização desta estrutura permite-lhe interagir com o código fonte a um nível profundamente significativo. Já não são cadeias de texto, mas sim dados que representam a estrutura de um programa C#.

Para começar, terá de instalar o SDK da Plataforma do Compilador de .NET:

Instruções de instalação – Instalador do Visual Studio

Existem duas formas diferentes de encontrar o SDK de Plataforma do Compilador .NET no Instalador do Visual Studio:

Instalar com a vista Instalador do Visual Studio – Cargas de trabalho

O SDK da Plataforma de Compilador .NET não é selecionado automaticamente como parte da carga de trabalho de desenvolvimento da extensão do Visual Studio. Tem de selecioná-lo como um componente opcional.

  1. Executar o Instalador do Visual Studio
  2. Selecione Modificar
  3. Verifique a carga de trabalho de desenvolvimento da extensão do Visual Studio .
  4. Abra o nó de desenvolvimento da extensão do Visual Studio na árvore de resumo.
  5. Selecione a caixa do SDK de Plataforma do Compilador .NET. Irá encontrá-lo em último nos componentes opcionais.

Opcionalmente, também vai querer que o editor de DGML apresente gráficos no visualizador:

  1. Abra o nó Componentes individuais na árvore de resumo.
  2. Selecione a caixa do editor DGML

Instalar com o Instalador do Visual Studio – separador Componentes individuais

  1. Executar o Instalador do Visual Studio
  2. Selecione Modificar
  3. Selecione o separador Componentes individuais
  4. Selecione a caixa do SDK de Plataforma do Compilador .NET. Irá encontrá-la na parte superior na secção Compiladores, ferramentas de compilação e runtimes .

Opcionalmente, também vai querer que o editor de DGML apresente gráficos no visualizador:

  1. Selecione a caixa do editor DGML. Irá encontrá-lo na secção Ferramentas de código .

Compreender as árvores de sintaxe

Utilize a API de Sintaxe para qualquer análise da estrutura do código C#. A API de Sintaxe expõe os parsers, as árvores de sintaxe e utilitários para analisar e construir árvores de sintaxe. É assim que procura no código elementos de sintaxe específicos ou lê o código de um programa.

Uma árvore de sintaxe é uma estrutura de dados utilizada pelos compiladores C# e Visual Basic para compreender os programas C# e Visual Basic. As sintaxes são produzidas pelo mesmo analisador que é executado quando um projeto é criado ou um programador atinge F5. As árvores de sintaxe têm fidelidade total com a linguagem; todas as informações num ficheiro de código são representadas na árvore. Escrever uma árvore de sintaxe no texto reproduz o texto original exato que foi analisado. As árvores de sintaxe também são imutáveis; uma vez criada uma árvore de sintaxe nunca pode ser alterada. Os consumidores das árvores podem analisar as árvores em vários threads, sem bloqueios ou outras medidas de simultaneidade, sabendo que os dados nunca mudam. Pode utilizar APIs para criar novas árvores que resultem da modificação de uma árvore existente.

Os quatro principais blocos modulares de árvores de sintaxe são:

Trivia, tokens e nós são compostos hierárquico para formar uma árvore que representa completamente tudo num fragmento de código do Visual Basic ou C#. Pode ver esta estrutura com a janela Visualização de Sintaxe . No Visual Studio, selecione Ver> OutroVisualizador de Sintaxe doWindows>. Por exemplo, o ficheiro de origem C# anterior examinado com o Visualizador de Sintaxe tem o seguinte aspeto:

SintaxNode: Azul | SyntaxToken: Verde | SintaxeTrivia: Ficheiro de Código C# Vermelho

Ao navegar nesta estrutura de árvore, pode encontrar qualquer instrução, expressão, token ou pouco espaço em branco num ficheiro de código.

Embora possa encontrar qualquer coisa num ficheiro de código com as APIs de Sintaxe, a maioria dos cenários envolve examinar pequenos fragmentos de código ou procurar instruções ou fragmentos específicos. Os dois exemplos que se seguem mostram utilizações típicas para procurar a estrutura do código ou procurar instruções individuais.

Atravessar árvores

Pode examinar os nós numa árvore de sintaxe de duas formas. Pode atravessar a árvore para examinar cada nó ou pode consultar elementos ou nós específicos.

Percurso manual

Pode ver o código concluído para este exemplo no nosso repositório do GitHub.

Nota

Os tipos de Árvore de Sintaxe utilizam a herança para descrever os diferentes elementos de sintaxe que são válidos em diferentes localizações do programa. A utilização destas APIs geralmente significa lançar propriedades ou membros da coleção para tipos derivados específicos. Nos exemplos seguintes, a atribuição e as castas são instruções separadas, utilizando variáveis explicitamente digitadas. Pode ler o código para ver os tipos de retorno da API e o tipo de runtime dos objetos devolvidos. Na prática, é mais comum utilizar variáveis implicitamente digitadas e depender de nomes de API para descrever o tipo de objetos que estão a ser examinados.

Crie um novo projeto do C# Stand-Alone Code Analysis Tool :

  • No Visual Studio, selecione Ficheiro>Novo>Projeto para apresentar a caixa de diálogo Novo Projeto.
  • Em Visual C#>Extensibility, selecione Stand-Alone Code Analysis Tool.
  • Atribua o nome "SyntaxTreeManualTraversal" ao projeto e clique em OK.

Vai analisar o programa básico "Hello World!" apresentado anteriormente. Adicione o texto do programa Hello World como uma constante na sua Program turma:

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

Em seguida, adicione o seguinte código para criar a árvore de sintaxe para o texto de código na programText constante. Adicione a seguinte linha ao seu Main método:

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

Essas duas linhas criam a árvore e obtêm o nó raiz dessa árvore. Agora, pode examinar os nós na árvore. Adicione estas linhas ao método Main para apresentar algumas das propriedades do nó raiz na árvore:

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

Execute a aplicação para ver o que o código detetou sobre o nó raiz nesta árvore.

Normalmente, percorreria a árvore para saber mais sobre o código. Neste exemplo, está a analisar o código que sabe para explorar as APIs. Adicione o seguinte código para examinar o primeiro membro do root nó:

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

Esse membro é um Microsoft.CodeAnalysis.CSharp.Syntax.NamespaceDeclarationSyntax. Representa tudo no âmbito da namespace HelloWorld declaração. Adicione o seguinte código para examinar que nós são declarados dentro do HelloWorld espaço de nomes:

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

Execute o programa para ver o que aprendeu.

Agora que sabe que a declaração é um Microsoft.CodeAnalysis.CSharp.Syntax.ClassDeclarationSyntax, declare uma nova variável desse tipo para examinar a declaração de classe. Esta classe contém apenas um membro: o Main método . Adicione o seguinte código para localizar o método e despeja-o Main num 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];

O nó de declaração do método contém todas as informações sintáticas sobre o método . Vamos apresentar o tipo de retorno do Main método, o número e os tipos dos argumentos e o corpo de texto do método. Adicione o seguinte código:

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

Execute o programa para ver todas as informações que descobriu sobre este programa:

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

Métodos de consulta

Além de percorrer árvores, também pode explorar a árvore de sintaxe com os métodos de consulta definidos em Microsoft.CodeAnalysis.SyntaxNode. Estes métodos devem estar imediatamente familiarizados com qualquer pessoa familiarizada com o XPath. Pode utilizar estes métodos com LINQ para encontrar rapidamente itens numa árvore. Tem SyntaxNode métodos de consulta como DescendantNodes, AncestorsAndSelf e ChildNodes.

Pode utilizar estes métodos de consulta para encontrar o argumento para o Main método como uma alternativa à navegação na árvore. Adicione o seguinte código à parte inferior do seu Main método:

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

A primeira instrução utiliza uma expressão LINQ e o DescendantNodes método para localizar o mesmo parâmetro que no exemplo anterior.

Execute o programa e pode ver que a expressão LINQ encontrou o mesmo parâmetro que navegar manualmente na árvore.

O exemplo utiliza WriteLine instruções para apresentar informações sobre as árvores de sintaxe à medida que são atravessadas. Também pode saber muito mais ao executar o programa concluído no depurador. Pode examinar mais das propriedades e métodos que fazem parte da árvore de sintaxe criada para o programa hello world.

Caminhantes de sintaxe

Muitas vezes, pretende localizar todos os nós de um tipo específico numa árvore de sintaxe, por exemplo, cada declaração de propriedade num ficheiro. Ao expandir a Microsoft.CodeAnalysis.CSharp.CSharpSyntaxWalker classe e substituir o VisitPropertyDeclaration(PropertyDeclarationSyntax) método, processa todas as declarações de propriedades numa árvore de sintaxe sem conhecer previamente a sua estrutura. CSharpSyntaxWalker é um tipo específico de CSharpSyntaxVisitor que visita recursivamente um nó e cada um dos seus filhos.

Este exemplo implementa um CSharpSyntaxWalker que examina uma árvore de sintaxe. using Recolhe diretivas que descobre que não estão a importar um System espaço de nomes.

Criar um novo projeto do C# Stand-Alone Code Analysis Tool ; dê-lhe o nome "SyntaxWalker".

Pode ver o código concluído para este exemplo no nosso repositório do GitHub. O exemplo no GitHub contém ambos os projetos descritos neste tutorial.

Tal como no exemplo anterior, pode definir uma constante de cadeia para conter o texto do programa que vai analisar:

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

Este texto de origem contém diretivas espalhadas using por quatro localizações diferentes: ao nível do ficheiro, no espaço de nomes de nível superior e nos dois espaços de nomes aninhados. Este exemplo realça um cenário principal para utilizar a CSharpSyntaxWalker classe para consultar código. Seria complicado visitar todos os nós na árvore de sintaxe raiz para encontrar com declarações. Em vez disso, cria uma classe derivada e substitui o método que é chamado apenas quando o nó atual na árvore é uma diretiva de utilização. O seu visitante não trabalha em nenhum outro tipo de nó. Este método único examina cada uma das using instruções e cria uma coleção dos espaços de nomes que não estão no System espaço de nomes. Cria um CSharpSyntaxWalker que examina todas as using instruções, mas apenas as using instruções.

Agora que definiu o texto do programa, tem de criar um SyntaxTree e obter a raiz dessa árvore:

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

Em seguida, crie uma nova classe. No Visual Studio, selecioneAdicionar Novo Item do Project>. Na caixa de diálogo Adicionar Novo Item , escreva UsingCollector.cs como nome de ficheiro.

Implementa a funcionalidade de using visitante na UsingCollector turma. Comece por fazer com que a UsingCollector classe derive de CSharpSyntaxWalker.

class UsingCollector : CSharpSyntaxWalker

Precisa de armazenamento para conter os nós de espaço de nomes que está a recolher. Declare uma propriedade só de leitura pública na UsingCollector classe ; utilize esta variável para armazenar os UsingDirectiveSyntax nós que encontrar:

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

A classe CSharpSyntaxWalker base implementa a lógica para visitar cada nó na árvore de sintaxe. A classe derivada substitui os métodos chamados para os nós específicos em que está interessado. Neste caso, está interessado em qualquer using diretiva. Isto significa que tem de substituir o VisitUsingDirective(UsingDirectiveSyntax) método . O único argumento para este método é um Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax objeto. Esta é uma vantagem importante para utilizar os visitantes: chamam os métodos substituídos com argumentos já lançados para o tipo de nó específico. A Microsoft.CodeAnalysis.CSharp.Syntax.UsingDirectiveSyntax classe tem uma Name propriedade que armazena o nome do espaço de nomes que está a ser importado. É um Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax. Adicione o seguinte código na substituição 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);
    }
}

Tal como no exemplo anterior, adicionou uma variedade de WriteLine instruções para ajudar a compreender este método. Pode ver quando é chamado e quais os argumentos que lhe são transmitidos de cada vez.

Por fim, tem de adicionar duas linhas de código para criar o UsingCollector e fazê-lo visitar o nó raiz, recolhendo todas as using instruções. Em seguida, adicione um foreach ciclo para apresentar todas as instruções que o using recoletor encontrou:

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

Compile e execute o programa. Deverá ver o seguinte resultado:

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

Parabéns! Utilizou a API de Sintaxe para localizar tipos específicos de declarações e declarações C# no código fonte C#.