Compartilhar via


Trabalhar com sintaxe

A árvore de sintaxe é uma estrutura de dados imutável fundamental exposta pelas APIs do compilador. Essas árvores representam a estrutura lexical e sintactica do código-fonte. Eles servem a dois propósitos importantes:

  • Para permitir que ferramentas - como um IDE, suplementos, ferramentas de análise de código e refatorações - vejam e processem a estrutura sintactica do código-fonte no projeto de um usuário.
  • Para permitir que ferramentas, como uma IDE e processos de refatoração, criem, modifiquem e reorganizem o código-fonte de maneira natural, sem a necessidade de usar edições diretas de texto. Ao criar e manipular árvores, as ferramentas podem criar e reorganizar facilmente o código-fonte.

Árvores de sintaxe

As árvores de sintaxe são a estrutura primária usada para compilação, análise de código, associação, refatoração, recursos de IDE e geração de código. Nenhuma parte do código-fonte é compreendida sem que primeiro seja identificada e categorizada em um dos muitos elementos de linguagem estrutural conhecidos.

Observação

RoslynQuoter é uma ferramenta de software livre que mostra as chamadas à API de fábrica de sintaxe usadas para construir uma árvore de sintaxe de um programa. Para experimentá-lo ao vivo, veja http://roslynquoter.azurewebsites.net.

As árvores de sintaxe têm três atributos principais:

  • Eles mantêm todas as informações de origem em total fidelidade. A fidelidade total significa que a árvore de sintaxe contém todas as informações encontradas no texto de origem, cada construção gramatical, cada token léxico e tudo mais no meio, incluindo espaço em branco, comentários e diretivas de pré-processador. Por exemplo, cada literal mencionado na origem é representado exatamente como foi digitado. As árvores de sintaxe também capturam erros no código-fonte quando o programa está incompleto ou malformado, representando tokens ignorados ou ausentes.
  • Elas podem produzir o texto exato do qual foram analisados. Em qualquer nó de sintaxe, é possível obter a representação de texto da subárvore enraizada nesse nó. Essa capacidade significa que as árvores de sintaxe podem ser usadas como uma maneira de construir e editar o texto de origem. Ao criar uma árvore, você criou, por implicação, o texto equivalente e, ao fazer uma nova árvore com alterações em uma árvore existente, você editou efetivamente o texto.
  • Eles são imutáveis e thread-safe. Depois que uma árvore é obtida, ela é um instantâneo do estado atual do código e nunca é alterada. Isso permite que vários usuários interajam com a mesma árvore de sintaxe ao mesmo tempo em threads diferentes sem bloqueio ou duplicação. Como as árvores são imutáveis e nenhuma modificação pode ser feita diretamente em uma árvore, os métodos de fábrica ajudam a criar e modificar árvores de sintaxe criando instantâneos adicionais da árvore. As árvores são eficientes na maneira como reutilizam nós subjacentes, de modo que uma nova versão pode ser reconstruída rapidamente e com pouca memória extra.

Uma árvore de sintaxe é literalmente uma estrutura de dados de árvore, em que elementos estruturais não terminais pairam outros elementos. Cada árvore de sintaxe é composta por nós, tokens e desafios.

Nós de sintaxe

Os nós de sintaxe são um dos principais elementos das árvores de sintaxe. Esses nós representam construções sintáticas, como declarações, comandos, cláusulas e expressões. Cada categoria de nós de sintaxe é representada por uma classe separada derivada de Microsoft.CodeAnalysis.SyntaxNode. O conjunto de classes de nó não é extensível.

Todos os nós de sintaxe são nós não terminais na árvore de sintaxe, o que significa que eles sempre têm outros nós e tokens como filhos. Como filho de outro nó, cada nó tem um nó pai que pode ser acessado por meio da propriedade SyntaxNode.Parent. Como os nós e as árvores são imutáveis, o pai de um nó nunca é alterado. A raiz da árvore tem um pai nulo.

Cada nó tem um método SyntaxNode.ChildNodes(), que retorna uma lista de nós filho em ordem sequencial com base em sua posição no texto de origem. Esta lista não contém tokens. Cada nó também tem métodos para examinar os Descendentes, como DescendantNodes, DescendantTokens ou DescendantTrivia – que representam uma lista de todos os nós, tokens ou desafios, que existem na subárvore com raiz nesse nó.

Além disso, cada subclasse de nó de sintaxe expõe os mesmos filhos por meio de propriedades fortemente tipadas. Por exemplo, uma classe de nó BinaryExpressionSyntax tem três propriedades adicionais específicas aos operadores binários: Left, OperatorToken e Right. O tipo de Left e Right é ExpressionSyntax, e o tipo de OperatorToken é SyntaxToken.

Alguns nós de sintaxe têm filhos opcionais. Por exemplo, um IfStatementSyntax tem um opcional ElseClauseSyntax. Se o filho não estiver presente, a propriedade retornará nulo.

Tokens de sintaxe

Tokens de sintaxe são os terminais da gramática de linguagem, representando os menores fragmentos sintacticos do código. Eles nunca são os pais de outros nós ou tokens. Os tokens de sintaxe consistem em palavras-chave, identificadores, literais e pontuação.

Para fins de eficiência, o SyntaxToken tipo é um tipo de valor CLR. Portanto, ao contrário dos nós de sintaxe, há apenas uma estrutura para todos os tipos de tokens com uma combinação de propriedades que têm significado dependendo do tipo de token que está sendo representado.

Por exemplo, um token literal inteiro representa um valor numérico. Além do texto de origem não processado abrangido pelo token, o token literal tem uma propriedade Value que informa o valor inteiro decodificado exato. Essa propriedade é tipada como Object porque pode ser um dos muitos tipos primitivos.

A ValueText propriedade informa as mesmas informações que a Value propriedade; no entanto, essa propriedade sempre é digitada como String. Um identificador no texto de origem em C# pode incluir caracteres de escape Unicode, mas a sintaxe da sequência de escape em si não é considerada parte do nome do identificador. Portanto, embora o texto bruto abrangido pelo token inclua a sequência de escape, a propriedade ValueText não. Em vez disso, ela inclui os caracteres Unicode identificados pelo escape. Por exemplo, se o texto de origem contiver um identificador escrito como \u03C0, a ValueText propriedade desse token retornará π.

Curiosidades de sintaxe

As curiosidades de sintaxe representam as partes do texto de origem que são em grande parte insignificantes para a compreensão normal do código, como espaço em branco, comentários e diretivas de pré-processador. Assim como os tokens de sintaxe, as curiosidades são tipos de valor. O tipo único Microsoft.CodeAnalysis.SyntaxTrivia é usado para descrever todos os tipos de curiosidades.

Como os desafios não fazem parte da sintaxe de linguagem normal e podem aparecer em qualquer lugar entre dois tokens quaisquer, eles não são incluídos na árvore de sintaxe como um filho de um nó. No entanto, como eles são importantes ao implementar um recurso como refatoração e manter a fidelidade total com o texto de origem, eles existem como parte da árvore de sintaxe.

Você pode acessar curiosidades inspecionando as coleções SyntaxToken.LeadingTrivia ou SyntaxToken.TrailingTrivia de um token. Quando o texto de origem é analisado, sequências de curiosidades são associadas a tokens. Em geral, um token possui qualquer desafio após ele na mesma linha até o próximo token. Todas as curiosidades após essa linha são associadas ao token a seguir. O primeiro token no arquivo de origem obtém todos as desafios iniciais e a última sequência de desafios no arquivo é anexada ao token de fim do arquivo, que, de outro modo, tem largura zero.

Ao contrário dos nós e tokens de sintaxe, os desafios de sintaxe não têm pais. No entanto, como eles fazem parte da árvore e cada um está associado a um único token, você pode acessar o token ao qual ele está associado usando a SyntaxTrivia.Token propriedade.

Intervalos

Cada nó, token ou desafio conhece sua posição dentro do texto de origem e o número de caracteres no qual ele consiste. Uma posição de texto é representada como um inteiro de 32 bits, que é um índice char baseado em zero. Um TextSpan objeto é a posição inicial e uma contagem de caracteres, ambos representados como inteiros. Se TextSpan tiver um comprimento zero, ele se referirá a um local entre dois caracteres.

Cada nó tem duas propriedades TextSpan: Span e FullSpan.

A propriedade Span é o intervalo de texto do início do primeiro token na subárvore do nó ao final do último token. Esse intervalo não inclui nenhum desafio à esquerda ou à direita.

A propriedade FullSpan é o intervalo de texto que inclui o intervalo normal do nó mais o intervalo de qualquer desafio à esquerda ou à direita.

Por exemplo:

      if (x > 3)
      {
||        // this is bad
          |throw new Exception("Not right.");|  // better exception?||
      }

O nó de instrução dentro do bloco tem um intervalo indicado pelas barras verticais simples (|). Ele inclui os caracteres throw new Exception("Not right.");. O intervalo completo é indicado pelas barras verticais duplas (||). Ele inclui os mesmos caracteres do intervalo e os caracteres associados ao desafio à esquerda e à direita.

Tipos

Cada nó, token ou desafio tem uma propriedade SyntaxNode.RawKind, do tipo System.Int32, que identifica o elemento de sintaxe exato representado. Esse valor pode ser convertido em uma enumeração específica do idioma. Cada linguagem, C# ou Visual Basic, tem uma única SyntaxKind enumeração (Microsoft.CodeAnalysis.CSharp.SyntaxKind e Microsoft.CodeAnalysis.VisualBasic.SyntaxKind, respectivamente) que lista todos os possíveis nós, tokens e elementos triviais na gramática. Essa conversão pode ser feita automaticamente acessando os métodos de extensão CSharpExtensions.Kind ou VisualBasicExtensions.Kind.

A propriedade RawKind permite a desambiguidade fácil de tipos de nó de sintaxe que compartilham a mesma classe de nó. Para tokens e curiosidades, essa propriedade é a única maneira de distinguir um tipo de elemento de outro.

Por exemplo, uma única BinaryExpressionSyntax classe tem Left, OperatorTokene Right como crianças. A propriedade Kind distingue se ela é um tipo AddExpression, SubtractExpression ou MultiplyExpression de nó de sintaxe.

Dica

É recomendável verificar os tipos usando métodos de extensão IsKind (para C#) ou IsKind (para VB).

Erros

Mesmo quando o texto de origem contém erros de sintaxe, uma árvore de sintaxe completa que é reversível para a origem é revelada. Quando o analisador encontra um código que não está em conformidade com a sintaxe definida da linguagem, ele usa uma das duas técnicas para criar uma árvore de sintaxe:

  • Se o analisador espera um tipo específico de token, mas não o encontra, ele pode inserir um token ausente na árvore de sintaxe no local que o token era esperado. Um token ausente representa o token real esperado, mas ele tem um intervalo vazio e sua SyntaxNode.IsMissing propriedade retorna true.

  • O analisador pode ignorar tokens até encontrar um em que possa continuar analisando. Nesse caso, os tokens ignorados são anexados como um nó de desafio com o tipo SkippedTokensTrivia.