Utiliser la syntaxe
Une arborescence de syntaxe est une structure de données immuable de base qui est exposée par les API du compilateur. Ces arborescences représentent la structure lexicale et syntaxique du code source. Elles ont deux utilités principales :
- Permettre aux outils (comme un IDE, des compléments, des outils d’analyse de code et des refactorisations) d’afficher et de traiter la structure syntaxique du code source dans le projet d’un utilisateur.
- Activer des outils, tels que des refactorisations et un IDE, pour créer, modifier et réorganiser le code source de manière naturelle sans avoir à utiliser des modifications de texte directes. Par la création et la manipulation des arborescences, les outils peuvent facilement créer et réorganiser le code source.
Arborescences de syntaxe
Les arborescences de syntaxe sont la structure principale utilisée pour la compilation, l’analyse de code, la liaison, la refactorisation, les fonctionnalités de l’IDE et la génération de code. Aucune partie du code source ne peut être comprise sans avoir au préalable été identifiée et classée dans l’un des nombreux éléments de langage structurel connus.
Remarque
RoslynQuoter est un outil open source qui montre les appels d’API de fabrique de syntaxe utilisés pour construire l’arborescence de syntaxe d’un programme. Pour l’essayer en direct, voir http://roslynquoter.azurewebsites.net.
Les arborescences de syntaxe ont trois caractéristiques principales.
- Elles contiennent toutes les informations sources et les représentent en toute fidélité. La fidélité totale signifie que l’arborescence de syntaxe contient toutes les informations trouvées dans le texte source, chaque construction grammaticale, chaque jeton lexical et tout le contenu entre ces éléments, notamment les directives d’espace blanc, de commentaires et de préprocesseur. Par exemple, chaque littéral mentionné dans la source est représenté exactement comme il a été tapé. Les arborescences de syntaxe capturent également les erreurs dans le code source quand le programme est incomplet ou incorrect en affichant les jetons ignorés ou manquants.
- Elles peuvent produire le texte exact à partir duquel ils ont été analysés. À partir de n’importe quel nœud de syntaxe, il est possible d’obtenir la représentation texte de la sous-arborescence ayant ce nœud pour racine. Cette fonctionnalité signifie que vous pouvez utiliser les arborescences de syntaxe pour créer et modifier le texte source. La création d’une arborescence implique que vous avez créé le texte équivalent, et la création d’une autre arborescence à partir des modifications apportées à une arborescence existante, implique que vous avez modifié le texte.
- Ils sont immuables et thread-safe. Une fois qu’une arborescence a été obtenue, elle est un instantané de l’état actuel du code et elle ne peut plus être changée. Cela permet à plusieurs utilisateurs d’interagir simultanément avec la même arborescence de syntaxe dans différents threads sans verrouillage ou duplication. Étant donné que les arborescences sont immuables et qu’elles ne peuvent pas être modifiées directement, les méthodes de fabrique facilitent la création et la modification des arborescences de syntaxe en créant des instantanés supplémentaires de l’arborescence. Les arborescences réutilisent avantageusement les nœuds sous-jacents pour accélérer la génération d’une nouvelle version, sans consommer beaucoup plus de mémoire.
Une arborescence de syntaxe est littéralement une structure de données arborescente, où des éléments structurels non terminaux sont parents d’autres éléments. Chaque arborescence de syntaxe est constituée de nœuds, de jetons et de trivia.
Nœuds de syntaxe
Les nœuds de syntaxe figurent parmi les principaux éléments des arborescences de syntaxe. Ces nœuds représentent des constructions syntaxiques, telles que des déclarations, des instructions, des clauses et des expressions. Chaque catégorie de nœuds de syntaxe est représentée par une classe distincte dérivée de Microsoft.CodeAnalysis.SyntaxNode. L’ensemble de classes de nœud n’est pas extensible.
Tous les nœuds de syntaxe sont des nœuds non terminaux dans l’arborescence de syntaxe ; ils ont donc toujours d’autres nœuds et jetons comme enfants. Chaque nœud enfant d’un autre nœud a un nœud parent qui est accessible via la propriété SyntaxNode.Parent. Comme les nœuds et les arborescences sont immuables, le parent d’un nœud ne change jamais. La racine de l’arborescence a un parent Null.
Chaque nœud a une méthode SyntaxNode.ChildNodes(), qui retourne une liste des nœuds enfants dans l’ordre séquentiel déterminé par leur position dans le texte source. Cette liste ne contient pas de jetons. Chaque nœud a également des méthodes qui retournent les descendants. Ainsi, les méthodes DescendantNodes, DescendantTokens ou DescendantTrivia représentent une liste de tous les nœuds, jetons ou trivia qui existent dans la sous-arborescence ayant ce nœud comme racine.
En outre, chaque sous-classe de nœud de syntaxe expose les mêmes enfants par le biais de propriétés fortement typées. Par exemple, une classe de nœud BinaryExpressionSyntax a trois propriétés supplémentaires propres aux opérateurs binaires : Left, OperatorToken et Right. Le type de Left et Right est ExpressionSyntax, et le type de OperatorToken est SyntaxToken.
Certains nœuds de syntaxe ont des enfants facultatifs. Par exemple, IfStatementSyntax a un ElseClauseSyntax facultatif. Si l’enfant n’est pas trouvé, la propriété retourne la valeur Null.
Jetons de syntaxe
Les jetons de syntaxe sont les éléments terminaux dans la grammaire d’un langage ; ils représentent les plus petits fragments syntaxiques du code. Ils ne sont jamais parents d’autres nœuds ou jetons. Les jetons de syntaxe se composent de mots clés, d’identificateurs, de littéraux et de caractères de ponctuation.
Par souci d’efficacité, le type SyntaxToken est un type valeur CLR. Par conséquent, contrairement aux nœuds de syntaxe, il n’y a qu’une seule structure pour tous les genres de jetons, avec une combinaison de propriétés dont le sens dépend du genre du jeton représenté.
Par exemple, un jeton de littéral d’entier représente une valeur numérique. En plus du texte source brut sur lequel s’étend le jeton, le jeton de littéral a une propriété Value qui indique précisément la valeur entière décodée. Cette propriété est typée en Object, car elle peut être l’un des nombreux types primitifs.
La propriété ValueText fournit les mêmes informations que la propriété Value, mais elle est toujours typée en String. Dans du texte source C#, un identificateur peut inclure des caractères d’échappement Unicode, mais la syntaxe de la séquence d’échappement n’est pas considérée comme faisant partie du nom de l’identificateur. Même si le texte brut sur lequel s’étend le jeton inclut la séquence d’échappement, la propriété ValueText ne l’inclut pas. À la place, elle inclut les caractères Unicode identifiés par la séquence d’échappement. Par exemple, si le texte source contient un identificateur écrit sous la forme \u03C0
, la propriété ValueText pour ce jeton retourne π
.
Trivia de syntaxe
Les trivia de syntaxe représentent les parties du texte source qui n’ont pas une grande utilité pour la compréhension générale du code. Il s’agit notamment des espaces blancs, des commentaires et des directives de préprocesseur. Comme les jetons de syntaxe, les trivia sont des types valeur. Le type Microsoft.CodeAnalysis.SyntaxTrivia unique est utilisé pour décrire tous les genres de trivia.
Du fait que les trivia ne font pas partie de la syntaxe du langage à proprement parler et qu’ils peuvent se trouver n’importe où entre deux jetons, ils ne sont pas représentés dans l’arborescence de syntaxe comme enfants d’un nœud. Les trivia sont toutefois inclus dans l’arborescence de syntaxe, car ils sont importants lors de l’implémentation d’une fonctionnalité telle que la refactorisation, mais aussi pour garantir une haute fidélité avec le texte source.
Vous pouvez accéder aux trivia en inspectant les collections SyntaxToken.LeadingTrivia ou SyntaxToken.TrailingTrivia d’un jeton. Quand le texte source est analysé, les séquences de trivia sont associées aux jetons. En règle générale, un jeton est propriétaire de tous les trivia qui se trouvent après lui sur la même ligne. Les trivia situés après cette ligne dépendent du jeton suivant. Dans le fichier source, le premier jeton obtient tous les trivia initiaux, et la dernière séquence de trivia est associée au jeton de fin de fichier, qui a sinon une largeur égale à zéro.
Contrairement aux nœuds et jetons de syntaxe, les trivia de syntaxe n’ont pas de parents. Toutefois, comme les trivia font partie de l’arborescence et que chacun d’eux est associé à un jeton unique, vous pouvez accéder au jeton correspondant à l’aide de la propriété SyntaxTrivia.Token.
Étendues
Chaque nœud, jeton ou trivia connaît sa position dans le texte source et le nombre de caractères qui le composent. La position du texte est représentée par un entier 32 bits, qui est un index char
de base zéro. L’objet TextSpan représente la position de début et le nombre de caractères sous forme de deux entiers. Si TextSpan a une longueur nulle, il fait référence à une position entre deux caractères.
Chaque nœud a deux propriétés TextSpan : Span et FullSpan.
La propriété Span définit l’étendue de texte comprise entre le début du premier jeton dans la sous-arborescence du nœud et la fin du dernier jeton. Cette étendue n’inclut pas les trivia de début ou de fin.
La propriété FullSpan définit l’étendue de texte qui inclut l’étendue du nœud, plus l’étendue des trivia de début ou de fin.
Par exemple :
if (x > 3)
{
|| // this is bad
|throw new Exception("Not right.");| // better exception?||
}
L’étendue du nœud de l’instruction dans le bloc est délimitée par deux barres verticales simples (|). Elle inclut les caractères throw new Exception("Not right.");
. L’étendue complète est délimitée par les deux barres verticales doubles (||). Elle inclut les mêmes caractères que l’étendue du nœud, plus les caractères associés aux trivia de début et de fin.
Genres
Chaque nœud, jeton ou trivia a une propriété SyntaxNode.RawKind de type System.Int32, qui identifie précisément l’élément de syntaxe représenté. Cette valeur peut être convertie en énumération spécifique au langage. Chaque langage, C# ou Visual Basic, a une seule énumération SyntaxKind
(Microsoft.CodeAnalysis.CSharp.SyntaxKind et Microsoft.CodeAnalysis.VisualBasic.SyntaxKind, respectivement) qui répertorie tous les nœuds, jetons et éléments trivia possibles au niveau syntaxique. Cette conversion peut être effectuée automatiquement en accédant aux méthodes d’extension CSharpExtensions.Kind ou VisualBasicExtensions.Kind.
La propriété RawKind permet de lever facilement toute ambiguïté sur les types de nœud de syntaxe qui utilisent la même classe de nœud. Pour les jetons et les trivia, cette propriété est le seul moyen de différencier les types d’élément entre eux.
Prenons l’exemple d’une classe BinaryExpressionSyntax, qui a les enfants Left, OperatorToken et Right. La propriété Kind détermine si le nœud de syntaxe est du genre AddExpression, SubtractExpression ou MultiplyExpression.
Conseil
Il est recommandé de vérifier les types à l’aide de méthodes d’extension IsKind (pour C#) ou IsKind (pour VB).
Erreurs
Même si le texte source contient des erreurs de syntaxe, une arborescence de syntaxe complète avec aller-retour au code source est exposée. Quand l’analyseur rencontre du code qui n’est pas conforme à la syntaxe définie pour le langage, il crée une arborescence de syntaxe à l’aide d’une des deux techniques suivantes :
Quand l’analyseur ne trouve pas le genre de jeton attendu, il insère un jeton manquant dans l’arborescence de syntaxe, à l’emplacement où le jeton était attendu. Un jeton manquant représente le jeton qui était attendu, mais du fait qu’il a une étendue vide, sa propriété SyntaxNode.IsMissing retourne
true
.L’analyseur ignore les jetons jusqu’à ce qu’il en trouve un à partir duquel il peut poursuivre l’analyse. Dans ce cas, les jetons ignorés sont attachés en tant que nœud de trivia avec le genre SkippedTokensTrivia.