Partager via


Bien démarrer avec la transformation de la syntaxe

Ce tutoriel s’appuie sur les concepts et techniques décrits dans les démarrages rapides Bien démarrer avec l’analyse de la syntaxe et Bien démarrer avec l’analyse sémantique. Si vous ne l’avez pas déjà fait, vous devez terminer ces démarrages rapides avant de commencer celui-ci.

Dans ce démarrage rapide, vous découvrez les techniques de création et de transformation des arborescences de syntaxe. En association avec les techniques que vous avez apprises dans les démarrages rapides précédents, vous créez votre première refactorisation de ligne de commande !

Instructions d’installation - Visual Studio Installer

Vous pouvez rechercher SDK .NET Compiler Platform dans Visual Studio Installer de deux façons :

Vue Installation avec Visual Studio Installer - Charges de travail

SDK .NET Compiler Platform n’est pas sélectionné automatiquement dans le cadre de la charge de travail Développement d’extensions Visual Studio. Vous devez le sélectionner comme composant facultatif.

  1. Exécutez Visual Studio Installer.
  2. Sélectionnez Modifier
  3. Choisissez la charge de travail Développement d’extensions Visual Studio.
  4. Ouvrez le nœud Développement d’extensions Visual Studio dans l’arborescence résumée.
  5. Cochez la case pour SDK .NET Compiler Platform. Il se trouve en dernier sous les composants facultatifs.

Si vous le souhaitez, vous pouvez ajouter l’éditeur DGML pour afficher des graphes dans le visualiseur :

  1. Ouvrez le nœud Composants individuels dans l’arborescence résumée.
  2. Cochez la case pour Éditeur DGML.

Onglet Installation avec Visual Studio Installer - Composants individuels

  1. Exécutez Visual Studio Installer.
  2. Sélectionnez Modifier
  3. Sélectionnez l’onglet Composants individuels.
  4. Cochez la case pour SDK .NET Compiler Platform. Il se trouve en haut de la liste, sous la section Compilateurs, outils de génération et runtimes.

Si vous le souhaitez, vous pouvez ajouter l’éditeur DGML pour afficher des graphes dans le visualiseur :

  1. Cochez la case pour Éditeur DGML. Il se trouve sous la section Outils de code.

Immuabilité et plateforme de compilateur .NET

L’immuabilité est un principe fondamental de la plateforme de compilateur .NET. Une fois créées, les structures de données immuables ne peuvent plus être modifiées. Les structures de données immuables peuvent être partagées et analysées simultanément par plusieurs consommateurs, en toute sécurité. Il n’existe aucun risque qu’un consommateur n’en perturbe un autre de manière imprévisible. Votre analyseur n’a pas besoin de verrous ou d’autres mesures de concurrence. Cette règle s’applique aux arborescences de syntaxe, compilations, symboles, modèles sémantiques et toute autre structure de données que vous rencontrez. Au lieu de modifier des structures existantes, les API créent de nouveaux objets en fonction des différences spécifiés dans les anciens objets. Vous appliquez ce concept aux arborescences de syntaxe pour créer de nouvelles arborescences à l’aide de transformations.

Créer et transformer des arborescences

Vous choisissez une des deux stratégies pour les transformations de syntaxe. Les méthodes de fabrique sont particulièrement utiles lorsque vous recherchez des nœuds spécifiques à remplacer, ou des emplacements spécifiques dans lesquelles vous souhaitez insérer le nouveau code. Les modules de réécriture sont recommandés lorsque vous souhaitez analyser un projet complet et y rechercher des modèles de code à remplacer.

Créer des nœuds avec des méthodes de fabrique

La première transformation de syntaxe montre les méthodes de fabrique. Vous allez remplacer une instruction using System.Collections; par une instruction using System.Collections.Generic;. Cet exemple montre comment créer des objets Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode à l’aide des méthodes de fabrique Microsoft.CodeAnalysis.CSharp.SyntaxFactory. Pour chaque type de nœud, jeton ou trivia, il existe une méthode de fabrique qui crée une instance de ce type. Vous créez des arborescences de syntaxe en composant hiérarchiquement des nœuds de façon ascendante. Puis vous transformerez le programme existant en remplaçant les nœuds existants par la nouvelle arborescence que vous avez créée.

Démarrez Visual Studio, puis créez un nouveau projet C# Outil d’analyse du code autonome. Dans Visual Studio, choisissez Fichier>Nouveau>Projet pour afficher la boîte de dialogue Nouveau projet. Sous Visual C#>Extensibilité, choisissez Outil d’analyse du code autonome. Ce démarrage rapide inclut deux exemples de projets, par conséquent, nommez la solution SyntaxTransformationQuickStart, puis nommez le projet ConstructionCS. Cliquez sur OK.

Ce projet utilise les méthodes de classe Microsoft.CodeAnalysis.CSharp.SyntaxFactory pour construire un élément Microsoft.CodeAnalysis.CSharp.Syntax.NameSyntax représentant l’espace de noms System.Collections.Generic.

Ajoutez la directive using suivante en début de Program.cs.

using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
using static System.Console;

Vous allez créer des nœuds de syntaxe du nom pour générer l’arborescence qui représente l’instruction using System.Collections.Generic;. NameSyntax est la classe de base pour les quatre types de noms qui s’affichent en C#. Vous composez ces quatre types de noms à la fois pour créer n’importe quel nom pouvant s’afficher en langage C# :

Vous utilisez la méthode IdentifierName(String) pour créer un nœud NameSyntax. Ajoutez le code suivant à votre méthode Main, dans Program.cs :

NameSyntax name = IdentifierName("System");
WriteLine($"\tCreated the identifier {name}");

Le code précédent crée un objet IdentifierNameSyntax et l’assigne à la variable name. La plupart des API Roslyn renvoient des classes de base pour les rendre plus faciles à utiliser avec les types associés. La variable name, une NameSyntax, peut être réutilisée lorsque vous générez la QualifiedNameSyntax. N’utilisez pas l’inférence de type lorsque vous générez l’exemple. Vous allez automatiser cette étape dans ce projet.

Vous avez créé le nom. Maintenant, il est temps de créer d’autres nœuds dans l’arborescence en générant une QualifiedNameSyntax. La nouvelle arborescence utilise name comme partie gauche du nom et une nouvelle IdentifierNameSyntax pour l’espace de noms Collections comme partie droite de la QualifiedNameSyntax. Ajoutez le code suivant à program.cs :

name = QualifiedName(name, IdentifierName("Collections"));
WriteLine(name.ToString());

Exécutez à nouveau le code et affichez les résultats. Vous créez une arborescence de nœuds qui représente le code. Vous allez continuer à utiliser ce modèle pour générer la QualifiedNameSyntax pour l’espace de noms System.Collections.Generic. Ajoutez le code suivant à Program.cs :

name = QualifiedName(name, IdentifierName("Generic"));
WriteLine(name.ToString());

Exécutez à nouveau le programme pour constater que vous avez généré l’arborescence du code à ajouter.

Créer une arborescence modifiée

Vous avez créé petite arborescence de syntaxe contenant une instruction. Les API permettant de créer de nouveaux nœuds représentent la solution idéale pour créer des instructions uniques ou d’autres petits blocs de code. Toutefois, pour générer de plus grands blocs de code, vous devez utiliser des méthodes qui remplacent les nœuds ou insèrent des nœuds dans une arborescence existante. N’oubliez pas que les arborescences de syntaxe sont immuables. L’API Syntaxe ne fournit aucun mécanisme permettant de modifier une arborescence de syntaxe existante après la construction. Au lieu de cela, elle fournit des méthodes qui génèrent de nouvelles arborescences en fonction des modifications apportées aux arborescences existantes. Les méthodes With* sont définies dans des classes concrètes qui dérivent de SyntaxNode ou dans des méthodes d’extension déclarées dans la classe SyntaxNodeExtensions. Ces méthodes créent un nouveau nœud en appliquant les modifications apportées aux propriétés enfants d’un nœud existant. En outre, la méthode d’extension ReplaceNode peut être utilisée pour remplacer un nœud descendant dans une sous-arborescence. Cette méthode met également à jour le parent pour pointer vers l’enfant qui vient d’être créé, et répète ce processus pour l’arborescence complète. Ce processus est appelé « re-spining » d’arborescence.

L’étape suivante consiste à créer une arborescence qui représente un (petit) programme complet puis à le modifier. Ajoutez le code suivant au début de la classe Program :

        private const string sampleCode =
@"using System;
using System.Collections;
using System.Linq;
using System.Text;

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

Notes

L’exemple de code utilise l’espace de noms System.Collections et non pas l’espace de noms System.Collections.Generic.

Ensuite, ajoutez le code suivant en bas de la méthode Main pour analyser le texte et créer une arborescence :

SyntaxTree tree = CSharpSyntaxTree.ParseText(sampleCode);
var root = (CompilationUnitSyntax)tree.GetRoot();

Cet exemple utilise la méthode WithName(NameSyntax) pour remplacer le nom dans un nœud UsingDirectiveSyntax par celui construit dans le code précédent.

Créez un nouveau nœud UsingDirectiveSyntax à l’aide de la méthode WithName(NameSyntax) pour mettre à jour le nom System.Collections avec le nom que vous avez créé dans le code précédent. Ajoutez le code suivant dans le bas de la méthode Main :

var oldUsing = root.Usings[1];
var newUsing = oldUsing.WithName(name);
WriteLine(root.ToString());

Exécutez le programme et examinez attentivement la sortie. newUsing n’a pas été placé dans l’arborescence racine. L’arborescence d’origine n’a pas été modifiée.

Ajoutez le code suivant à l’aide de la méthode d'extension ReplaceNode pour créer une nouvelle arborescence. La nouvelle arborescence est le résultat du remplacement de l’importation existante par le nœud newUsing mis à jour. Vous affectez cette nouvelle arborescence à la variable root existante :

root = root.ReplaceNode(oldUsing, newUsing);
WriteLine(root.ToString());

Réexécutez le programme. Cette fois, l’arborescence importe correctement l’espace de noms System.Collections.Generic.

Transformer des arborescences avec SyntaxRewriters

Les méthodes With* et ReplaceNode constituent une solution pratique pour transformer des branches individuelles en une arborescence de syntaxe. La classe Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter effectue plusieurs transformations sur une arborescence de syntaxe. La classe Microsoft.CodeAnalysis.CSharp.CSharpSyntaxRewriter est une sous-classe de la classe Microsoft.CodeAnalysis.CSharp.CSharpSyntaxVisitor<TResult>. CSharpSyntaxRewriter applique une transformation à un type spécifique de SyntaxNode. Vous pouvez appliquer des transformations à plusieurs types d’objets SyntaxNode là où elles apparaissent dans une arborescence de syntaxe. Le second projet de ce démarrage rapide crée une refactorisation de ligne de commande qui supprime des types explicites dans des déclarations de variables locales partout où une inférence de type peut être utilisée.

Créez un projet C# Outil d’analyse du code autonome. Dans Visual Studio, cliquez avec le bouton droit sur le nœud de la solution SyntaxTransformationQuickStart. Choisissez Ajouter>Nouveau projet pour afficher la boîte de dialogue Nouveau projet. Sous Visual C#>Extensibilité, choisissez Outil d’analyse du code autonome. Nommez votre projet TransformationCS, puis cliquez sur OK.

La première étape consiste à créer une classe dérivée de CSharpSyntaxRewriter pour effectuer vos transformations. Ajoutez un nouveau fichier de classe au projet. Dans Visual Studio, choisissez Projet>Ajouter une classe.... Dans la boîte de dialogue Ajouter un nouvel élément, tapez TypeInferenceRewriter.cs pour le nom de fichier.

Ajoutez le code suivant à l’aide de directives dans le fichier TypeInferenceRewriter.cs :

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

Ensuite, étendez la classe TypeInferenceRewriter à la classe CSharpSyntaxRewriter :

public class TypeInferenceRewriter : CSharpSyntaxRewriter

Ajoutez le code suivant pour déclarer un champ en lecture seule privé qui contiendra une SemanticModel et initialisez-le dans le constructeur. Vous aurez besoin de ce champ ultérieurement pour déterminer où l’inférence de type peut être utilisée :

private readonly SemanticModel SemanticModel;

public TypeInferenceRewriter(SemanticModel semanticModel) => SemanticModel = semanticModel;

Remplacez la méthode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) :

public override SyntaxNode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax node)
{

}

Notes

De nombreuses API Roslyn déclarent des types de retour représentant des classes de base des types Runtime réels retournés. Dans de nombreux scénarios, il est possible de remplacer un type de nœud par un autre, voire de le supprimer. Dans cet exemple, la méthode VisitLocalDeclarationStatement(LocalDeclarationStatementSyntax) retourne un SyntaxNode, au lieu du type dérivé de LocalDeclarationStatementSyntax. Ce module de réécriture retourne un nouveau nœud LocalDeclarationStatementSyntax en fonction du nœud existant.

Ce démarrage rapide gère les déclarations de variables locales. Vous pouvez l’étendre à d’autres déclarations telles que des boucles foreach, des boucles for, des expressions LINQ et des expressions lambda. En outre, ce module de réécriture transformera les déclarations dans la forme la plus simple :

Type variable = expression;

Si vous souhaitez explorer votre propre déclaration, vous pouvez étendre l’exemple terminé pour ces types de déclarations de variables :

// Multiple variables in a single declaration.
Type variable1 = expression1,
     variable2 = expression2;
// No initializer.
Type variable;

Ajoutez le code suivant au corps de la méthode VisitLocalDeclarationStatement pour ignorer la réécriture de ces formes de déclarations :

if (node.Declaration.Variables.Count > 1)
{
    return node;
}
if (node.Declaration.Variables[0].Initializer == null)
{
    return node;
}

La méthode indique qu’aucune réécriture n’a lieu en retournant le paramètre node inchangé. Si aucune de ces expressions if n’est vraie, le nœud représente une déclaration possible avec initialisation. Ajoutez les instructions suivantes pour extraire le nom du type spécifié dans la déclaration et liez-le à l’aide du champ SemanticModel pour obtenir un symbole de type :

var declarator = node.Declaration.Variables.First();
var variableTypeName = node.Declaration.Type;

var variableType = (ITypeSymbol)SemanticModel
    .GetSymbolInfo(variableTypeName)
    .Symbol;

Ajoutez maintenant cette instruction pour lier l’expression de l’initialiseur :

var initializerInfo = SemanticModel.GetTypeInfo(declarator.Initializer.Value);

Enfin, ajoutez l’instruction if suivante pour remplacer le nom de type existant par le mot clé var si le type de l’expression de l’initialiseur correspond au type spécifié :

if (SymbolEqualityComparer.Default.Equals(variableType, initializerInfo.Type))
{
    TypeSyntax varTypeName = SyntaxFactory.IdentifierName("var")
        .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
        .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

    return node.ReplaceNode(variableTypeName, varTypeName);
}
else
{
    return node;
}

Cette condition est nécessaire car la déclaration peut effectuer un cast de l’expression de l’initialiseur dans une classe de base ou une interface. Si vous le souhaitez, les types à gauche et à droite de l’affectation ne correspondent pas. Dans ce cas, la suppression du type explicite modifierait la sémantique d’un programme. var est spécifié comme un identificateur plutôt qu’un mot clé car var est un mot clé contextuel. Les trivias de début et de fin (espaces blancs) sont transférés de l’ancien nom de type vers le mot clé var afin de conserver les espaces blancs verticaux et l’indentation. Il est plus simple d’utiliser ReplaceNode plutôt que With* pour transformer la LocalDeclarationStatementSyntax, car le nom de type est en fait le petit-enfant de l’instruction de déclaration.

Vous avez terminé le TypeInferenceRewriter. Revenez maintenant à votre fichier Program.cs pour terminer l’exemple. Créez un test Compilation et obtenez le SemanticModel à partir de celui-ci. Utilisez SemanticModel pour tester votre TypeInferenceRewriter. Vous effectuerez cette étape en dernier. En attendant, déclarez une variable d’espace réservé représentant votre compilation de test :

Compilation test = CreateTestCompilation();

Après une pause, vous devriez voir apparaître une erreur sous forme de tilde, indiquant qu’il n’existe aucune méthode CreateTestCompilation. Appuyez sur Ctrl+point pour ouvrir l’ampoule, puis sur Entrée pour appeler la commande Générer un stub de méthode. Cette commande va générer un stub de méthode pour la méthode CreateTestCompilation dans la classe Program. Vous y reviendrez pour remplir cette méthode :

C# Generate method from usage

Écrivez le code suivant pour effectuer une itération sur chaque SyntaxTree dans le test de Compilation. Pour chacune d’elles, initialisez un nouveau TypeInferenceRewriter avec le SemanticModel pour cet arborescence :

foreach (SyntaxTree sourceTree in test.SyntaxTrees)
{
    SemanticModel model = test.GetSemanticModel(sourceTree);

    TypeInferenceRewriter rewriter = new TypeInferenceRewriter(model);

    SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

    if (newSource != sourceTree.GetRoot())
    {
        File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
    }
}

Dans l’instruction foreach que vous avez créée, ajoutez le code suivant pour effectuer la transformation sur chaque arborescence source. Ce code écrit de façon conditionnelle la nouvelle arborescence transformée si des modifications ont été effectuées. Votre module de réécriture doit uniquement modifier une arborescence si elle rencontre une ou plusieurs déclarations de variables locales qui pourraient être simplifiées à l’aide de l’inférence de type :

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());

if (newSource != sourceTree.GetRoot())
{
    File.WriteAllText(sourceTree.FilePath, newSource.ToFullString());
}

Vous devriez voir des tildes sous le code File.WriteAllText. Sélectionnez l’ampoule et ajoutez l’instruction using System.IO; nécessaire.

Vous avez presque terminé. Il ne reste plus qu’une étape : la création d’un Compilation test. Étant donné que vous n’avez pas utilisé du tout l’inférence de type pendant ce démarrage rapide, cela aurait été un cas de test parfait. Malheureusement, la création d’une compilation à partir d’un fichier de projet C# dépasse le cadre de cette procédure pas à pas. Heureusement, si vous avez suivi attentivement les instructions, il reste un espoir. Remplacez le contenu de la méthode CreateTestCompilation par le code suivant. Il crée une compilation de test correspondant par coïncidence au projet décrit dans ce démarrage rapide :

String programPath = @"..\..\..\Program.cs";
String programText = File.ReadAllText(programPath);
SyntaxTree programTree =
               CSharpSyntaxTree.ParseText(programText)
                               .WithFilePath(programPath);

String rewriterPath = @"..\..\..\TypeInferenceRewriter.cs";
String rewriterText = File.ReadAllText(rewriterPath);
SyntaxTree rewriterTree =
               CSharpSyntaxTree.ParseText(rewriterText)
                               .WithFilePath(rewriterPath);

SyntaxTree[] sourceTrees = { programTree, rewriterTree };

MetadataReference mscorlib =
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
MetadataReference codeAnalysis =
        MetadataReference.CreateFromFile(typeof(SyntaxTree).Assembly.Location);
MetadataReference csharpCodeAnalysis =
        MetadataReference.CreateFromFile(typeof(CSharpSyntaxTree).Assembly.Location);

MetadataReference[] references = { mscorlib, codeAnalysis, csharpCodeAnalysis };

return CSharpCompilation.Create("TransformationCS",
    sourceTrees,
    references,
    new CSharpCompilationOptions(OutputKind.ConsoleApplication));

Croisez les doigts et exécutez le projet. Dans Visual Studio, choisissez Déboguer>Démarrer le débogage. Visual Studio devrait vous avertir que les fichiers de votre projet ont été modifiés. Cliquez sur «Oui pour tout» pour recharger les fichiers modifiés. Examinez ces fichiers pour vérifier la qualité de votre travail. Remarquez à quel point le code serait plus propre sans tous ces spécificateurs de type explicites et redondants.

Félicitations ! Vous avez utilisé les API du compilateur pour écrire votre propre refactorisation qui recherche certains modèles syntaxiques dans tous les fichiers d’un projet C#, analyse la sémantique du code source correspondant à ces modèles puis la transforme. Vous avez officiellement réussi à créer une refactorisation !