Trabalhar com sintaxe

A árvore de sintaxe é uma estrutura de dados imutável e fundamental exposta pelas APIs do compilador. Essas árvores representam a estrutura lexical e sintática do código-fonte. Elas servem duas finalidades importantes:

  • Permitir que ferramentas – como um IDE, suplementos, ferramentas de análise de código e refatorações – vejam e processem a estrutura sintática do código-fonte no projeto do usuário.
  • Permitir que ferramentas – como refatorações e um IDE – criem, modifiquem e reorganizem o código-fonte de uma maneira natural sem a necessidade de uso de edições de texto diretas. Criando e manipulando árvores, as ferramentas podem criar e reorganizar o código-fonte com facilidade.

Árvores de sintaxe

Árvores de sintaxe são a estrutura principal 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 é entendida sem primeiro ser identificada e categorizada em um dos muitos elementos de linguagem estrutural conhecidos.

Observação

RoslynQuoter é uma ferramenta de código aberto que mostra as chamadas à API de fábrica de sintaxe usadas para construir a árvore de sintaxe de um programa. Para experimentá-la ao vivo, consulte http://roslynquoter.azurewebsites.net.

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

  • Elas têm todas as informações de origem com fidelidade total. A fidelidade total significa que a árvore de sintaxe contém cada informação encontrada no texto de origem, cada constructo gramatical, cada token lexical e todo o resto, incluindo espaço em branco, comentários e diretivas do pré-processador. Por exemplo, cada literal mencionado na fonte é representado exatamente como foi digitado. Através da representação de tokens ignorados ou ausentes, as árvores de sintaxe também capturam erros no código-fonte quando o programa está incompleto ou mal-formado.
  • 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 com raiz nesse nó. Essa habilidade significa que as árvores de sintaxe podem ser usadas como uma maneira de construir e editar o texto de origem. Ao criar uma árvore, por implicação, você criou o texto equivalente e, ao criar uma nova árvore com base nas alterações de uma árvore existente, você editou o texto efetivamente.
  • 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 nem 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 no modo como reutilizam os nós subjacentes, de forma que uma nova versão possa ser recompilada rapidamente e com pouca memória extra.

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

Nós de sintaxe

Nós de sintaxe são um dos elementos principais das árvores de sintaxe. Esses nós representam os constructos sintáticos como declarações, instruções, 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. Essa 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 ElseClauseSyntax opcional. Se o filho não estiver presente, a propriedade retornará nulo.

Tokens de sintaxe

Os tokens de sintaxe são os terminais da gramática da linguagem, que representam os menores fragmentos sintáticos 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 tipo SyntaxToken é 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 propriedade ValueText indica as mesmas informações que a propriedade Value; no entanto, essa propriedade sempre é tipada como String. Um identificador no texto de origem C# pode incluir caracteres de escape Unicode, embora a sintaxe da sequência de escape em si não seja considerada parte do nome do identificador. Portanto, embora o texto não processado abrangido pelo token inclua a sequência de escape, isso não ocorre com a propriedade ValueText. Em vez disso, ela inclui os caracteres Unicode identificados pelo escape. Por exemplo, se o texto de origem contiver um identificador gravado como \u03C0, a propriedade ValueText desse token retornará π.

Desafios de sintaxe

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

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ó. Apesar disso, como eles são importantes ao implementar um recurso como refatoração e para manter fidelidade total com o texto de origem, eles existem como parte da árvore de sintaxe.

Acesse os desafios inspecionando as coleções SyntaxToken.LeadingTrivia ou SyntaxToken.TrailingTrivia de um token. Quando o texto de origem é analisado, sequências de desafios são associadas aos tokens. Em geral, um token possui qualquer desafio após ele na mesma linha até o próximo token. Qualquer desafio após essa linha é associado ao próximo token. 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. Apesar disso, como eles fazem parte da árvore e cada um deles é associado um único token, você poderá acessar o token ao qual ele está associado usando a propriedade SyntaxTrivia.Token.

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 objeto TextSpan é a posição inicial e uma contagem de caracteres, ambas representadas como inteiros. Se TextSpan tem comprimento zero, ele se refere 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 total é indicado pelas barras verticais duplas (||). Ele inclui os mesmos caracteres do intervalo e os caracteres associados ao desafio à esquerda e à direita.

Variantes

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 a idioma. Cada linguagem, C# ou Visual Basic, tem uma única enumeração SyntaxKind (Microsoft.CodeAnalysis.CSharp.SyntaxKind e Microsoft.CodeAnalysis.VisualBasic.SyntaxKind, respectivamente), que lista todos os possíveis nós, tokens e elementos de trívia na gramática. Esta 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 desafios, essa propriedade é a única maneira de diferenciar um tipo de elemento de outro.

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

Dica

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

Errors

Mesmo quando o texto de origem contém erros de sintaxe, uma árvore de sintaxe completa com ida e volta para a origem é exposta. 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 determinado tipo de token, mas não o encontra, ele pode inserir um token ausente na árvore de sintaxe no local em que o token era esperado. Um token ausente representa o token real que era esperado, mas tem um intervalo vazio e sua propriedade SyntaxNode.IsMissing retorna true.

  • O analisador pode ignorar tokens até encontrar um no qual pode continuar a análise. Nesse caso, os tokens ignorados são anexados como um nó de desafio com o tipo SkippedTokensTrivia.