Trabalhar com sintaxe
A árvore de sintaxe é uma estrutura de dados imutável fundamental exposta pelas APIs do compilador. Estas árvores representam a estrutura lexical e sintática do código-fonte. Servem 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 sintática do código-fonte no projeto de um usuário.
- Para permitir que ferramentas - como refatorações e um IDE - criem, modifiquem e reorganizem o código-fonte de forma natural sem ter que usar edições diretas de texto. Ao criar e manipular árvores, as ferramentas podem facilmente criar e reorganizar 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, vinculação, refatoração, recursos do IDE e geração de código. Nenhuma parte do código-fonte é compreendida sem primeiro ser identificada e categorizada em um dos muitos elementos de linguagem estrutural bem conhecidos.
Nota
RoslynQuoter é uma ferramenta de código aberto que mostra as chamadas de API de fábrica de sintaxe usadas para construir a árvore de sintaxe de um programa. Para experimentar ao vivo, veja http://roslynquoter.azurewebsites.net.
As árvores de sintaxe têm três atributos principais:
- Eles detêm todas as informações de origem em total fidelidade. Fidelidade total significa que a árvore de sintaxe contém todas as informações encontradas no texto de origem, todas as construções gramaticais, todos os tokens lexicais e tudo o mais, incluindo espaço em branco, comentários e diretivas de pré-processador. Por exemplo, cada literal mencionado na fonte é 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.
- Eles podem produzir o texto exato a partir do qual foram analisados. A partir de 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, implicitamente, o texto equivalente e, ao fazer uma nova árvore a partir de alterações em uma árvore existente, você efetivamente editou 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 muda. 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 forma como reutilizam os nós subjacentes, para que uma nova versão possa ser reconstruída rapidamente e com pouca memória extra.
Uma árvore de sintaxe é literalmente uma estrutura de dados de árvore, onde elementos estruturais não terminais são pais de outros elementos. Cada árvore de sintaxe é composta de nós, tokens e curiosidades.
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, instruções, orações 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 através da SyntaxNode.Parent propriedade. Como nós e árvores são imutáveis, o pai de um nó nunca muda. A raiz da árvore tem um pai nulo.
Cada nó tem um SyntaxNode.ChildNodes() método, 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 Descendentes, como DescendantNodes, DescendantTokens, ou DescendantTrivia - que representam uma lista de todos os nós, tokens ou curiosidades que existem na subárvore enraizada por esse nó.
Além disso, cada subclasse de nó de sintaxe expõe todos os mesmos filhos por meio de propriedades fortemente tipadas. Por exemplo, uma BinaryExpressionSyntax classe de nó tem três propriedades adicionais específicas para operadores binários: Left, OperatorTokene 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á null.
Tokens de sintaxe
Os tokens de sintaxe são os terminais da gramática da linguagem, representando os menores fragmentos sintáticos do código. Eles nunca são 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 mistura 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 bruto que o token abrange, o token literal tem uma Value propriedade que informa o valor inteiro decodificado exato. Esta propriedade é digitada como Object porque pode ser um dos muitos tipos primitivos.
A ValueText propriedade informa as mesmas informações que a propriedade, no entanto, esta propriedade é sempre digitada Value como String. Um identificador no texto de origem do 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 estendido pelo token inclua a sequência de escape, a ValueText propriedade não. Em vez disso, ele 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 da sintaxe
As trivialidades 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. Como os tokens de sintaxe, as trivialidades são tipos de valor. O tipo único Microsoft.CodeAnalysis.SyntaxTrivia é usado para descrever todos os tipos de trivialidades.
Como as trivialidades não fazem parte da sintaxe normal da linguagem e podem aparecer em qualquer lugar entre quaisquer dois tokens, elas não são incluídas na árvore de sintaxe como um filho de um nó. No entanto, como eles são importantes ao implementar um recurso como a refatoração e para manter a fidelidade total com o texto de origem, eles existem como parte da árvore de sintaxe.
Você pode acessar as curiosidades SyntaxToken.LeadingTrivia inspecionando um token ou SyntaxToken.TrailingTrivia coleções. Quando o texto de origem é analisado, sequências de curiosidades são associadas a tokens. Em geral, um token possui qualquer curiosidade depois dele na mesma linha até o próximo token. Qualquer curiosidade após essa linha é associada ao token a seguir. O primeiro token no arquivo de origem obtém todas as curiosidades iniciais, e a última sequência de curiosidades no arquivo é colada no token de fim de arquivo, que de outra forma tem largura zero.
Ao contrário dos nós e tokens de sintaxe, as trivialidades 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 está associado usando a SyntaxTrivia.Token propriedade.
Espaças
Cada nó, token ou curiosidade sabe sua posição dentro do texto de origem e o número de caracteres em que ele consiste. Uma posição de texto é representada como um inteiro de 32 bits, que é um índice baseado em char
zero. Um TextSpan objeto é a posição inicial e uma contagem de caracteres, ambos representados como inteiros. Se TextSpan tiver um comprimento zero, refere-se a uma localização entre dois caracteres.
Cada nó tem duas TextSpan propriedades: Span e FullSpan.
A Span propriedade é a extensão de texto desde o início do primeiro token na subárvore do nó até o final do último token. Esta extensão não inclui nenhuma trivialidade à frente ou à direita.
A FullSpan propriedade é a extensão de texto que inclui a extensão normal do nó, mais a extensão de qualquer trivialidade à esquerda ou à direita.
Por exemplo:
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}
O nó da instrução dentro do bloco tem uma extensão indicada pelas barras verticais únicas (|). Inclui os personagens throw new Exception("Not right.");
. O vão completo é indicado pelas barras verticais duplas (||). Ele inclui os mesmos personagens que o span e os personagens associados com as curiosidades principais e finais.
Tipos
Cada nó, token ou trívia tem uma SyntaxNode.RawKind propriedade, do tipo System.Int32, que identifica o elemento de sintaxe exato representado. Esse valor pode ser convertido para 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 trivia na gramática. Esta conversão pode ser feita automaticamente acessando os CSharpExtensions.Kind métodos ou VisualBasicExtensions.Kind extensão.
A RawKind propriedade permite a desambiguação 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 Kind propriedade distingue se é um AddExpression, SubtractExpression, ou MultiplyExpression tipo de nó de sintaxe.
Gorjeta
É recomendável verificar os tipos usando IsKind métodos de extensão (para C#) ou IsKind (para VB).
Erros
Mesmo quando o texto de origem contém erros de sintaxe, uma árvore de sintaxe completa que pode ser triplada em relação à fonte é exposta. Quando o analisador encontra um código que não está de acordo 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 em que o token era esperado. Um token ausente representa o token real que era esperado, mas tem uma extensão vazia e sua SyntaxNode.IsMissing propriedade retorna
true
.O analisador pode pular tokens até encontrar um onde possa continuar analisando. Neste caso, os tokens ignorados são anexados como um nó trivia com o tipo SkippedTokensTrivia.