Tutorial : Création d’un analyseur de code (Roslyn / .NET)

Temps de lecture : 8 minutes

Comme le titre l’indique, on va parler aujourd’hui des analyseurs de code et plus précisément des analyseurs Roslyn, le fameux compilateur .NET.

L’idée derrière cet article est de creuser un peu et voir ce qu’il y a sous le capot d’une des fonctionnalités les plus utilisées de Visual Studio : la fameuse lampe jaune.

Bien évidemment je ne parle pas de l’ampoule de votre salon, mais de celle qu’on voit régulièrement sur la barre gauche de Visual Studio.

«Action Rapide » ou « Quick Action » est une des fonctionnalités les plus utilisées sur cet IDE et on ne peut pas vivre sans. Ce raccourci vous permet de faire plusieurs opérations sur votre code (générer des méthodes ou des classes, supprimer des propriétés non utilisées, alerter sur une optimisation de code, proposer un fix…).

Cette ampoule est généralement accompagnée par des lignes rouges sur le code pour signaler à l’utilisateur qu’il y a quelque chose qui cloche avec ce bout de code.

Pour avoir toutes ces fonctionnalités, dans les coulisses l’équipe Visual Studio a mis en place un mécanisme qui se déclenche pour analyser le code et propose par la suite à l’utilisateur des améliorations potentielles et fixes via des actions rapides.

Et on adore ça…

Dans cet article on va créer notre premier analyseur Roslyn qui va nous signaler une amélioration sur notre code. Le but est de souligner une opération de retour qui inclut une création de tableau vide avec la syntaxe new int[0] et proposer à l’utilisateur d’utiliser la syntaxe Array.Empty .

Avant qu’on commence, je vais vous présenter les différents outils et API qu’on va utiliser tout au long de notre tutoriel.

Le compilateur .NET, Roslyn, inclut des API et mécanismes qui permettent de faire plusieurs opérations sur notre code :

  • Analyser la structure de notre code
  • Construire des arbres syntaxiques
  • Récupérer les différentes opérations et metadata associées
  • ect.

Dans ce mini tutoriel on va utiliser principalement 3 API pour construire notre Analyzer et proposer par la suite une amélioration sur le code utilisateur :

  • Syntax Tree
  • Sementic Model
  • IOperation

Ne vous inquiétez pas, je vais vous donner quelques définitions avant de commencer pour partir sur des bases solides.

Syntax Tree API

La Syntax Tree API nous permet de construire une arborescence de syntaxe qui est une représentation fidèle de notre code C#. Cette structure de données est utilisée par le compilateur pour comprendre notre code et différencier les différents symboles utilisés (déclarations, opérateurs, ponctuation, espace vide…).

Voici un exemple d’un arbre syntaxique du code new int[0]

  • Les nodes sont de type SyntaxNode, ils peuvent représenter des déclarations, des instructions, des clauses et des expressions.
  • Les tokens sont de type SyntaxToken, ils peuvent représenter un mot clé, un identificateur, un opérateur ou un signe de ponctuation individuel.
  • Les trivias sont de type SyntaxTrivia, ils peuvent représenter les éléments d’information non significatifs comme les espaces et les commentaires.

Visual Studio propose une fenêtre qui nous permet de visualiser cet arbre et d’avoir plus de détails sur chaque nœud. View > Other Windows > Syntax Visualizer

Semantic Model API

Le Semantic Model est l’API qui peut répondre à toutes nos questions concernant les nœuds de la Syntax Tree évoquée précédemment. Cette API expose toutes les informations connues par le compilateur et peut répondre par exemple à ces questions :

  • Où est-ce que cet élément est déclaré dans le code source ?
  • Dans quel espace de nom et quel est le type de ce symbole ?
  • Quelle est la dimension d’un tableau ? …

IOperation API

L’API IOperation est une des évolutions du compilateur Roslyn qui expose des méthodes pour récupérer et cibler des opérations bien précises dans notre code.

On peut citer par exemple des opérations de création de tableau, un switch case, une déclaration d’une fonction locale…
Pour plus de détails vous pouvez trouver la liste complète des opérations supportées par le compilateur ICI

Allez on a trop parlé, on passe à l’action maintenant.

Etape 1 : Installation de l’environnement

Pour cet article on va utiliser Visual 2019 avec le kit “Visual studio extension development”

1 – Pour ajouter/supprimer des fonctionnalités dans votre Visual Studio, il faut ouvrir votre installer Visual Studio (vous pouvez utiliser la barre de recherche Windows).

2 – On sélectionne l’option “Modifier” dans le popup de l’installateur 

3 – On coche “Visual Studio extension development” et surtout il ne faut pas oublier l’option “.NET Compiler Platfom SDK”

Et voilà tout est prêt !!!!

Etape 2 : Création de notre analyseur

Création d’un nouveau projet analyseur

Pour commencer on va créer un nouveau projet de type “Analyzer with Code Fix”. On peut utiliser la barre de recherche pour trouver le bon template. Pour cet article on va utiliser C# comme langage mais garder en tête qu’il est possible aussi de faire le même analyseur en VB.NET (il n’y a pas une grande différence dans la syntaxe).

Vous allez avoir une nouvelle solution Visual Studio avec cette structure.

Comme tout template Visual Studio le projet embarque un petit exemple et pour une fois je suis sûr que ce dernier peut gagner l’oscar de l’analyseur le plus stupide au monde 😀

Cet exemple souligne la class Program et vous propose de changer ça en PROGRAM (tout en majuscule) !!!

Oui oui je sais très bien c’est stupide comme exemple mais ne vous inquiétez pas on va virer tout ça avant de commencer.

Vous pouvez zapper cette étape de cleanup et cloner directement ce repo github.
Il faut faire un checkout sur la branche Develop ; j’ai fait le cleanup pour vous. 😉
La branche feature/add-empty-array-analyzer est la version finale de l’analyseur qu’on va construire tout au long de cet article.

ahdbk/roslyn-analyzer-sample at develop (github.com)

git clone https://github.com/ahdbk/roslyn-analyzer-sample
git checkout develop

Si vous voulez faire le cleanup vous-même. On va supprimer tout le code dans les méthodes suivantes :

– Dans le projet analyzer == EmptyArrayAnalyzer.cs == méthode…
– Dans le projet CodeFix == EmptyArrayCodeFixProvider == méthode…

et enfin on va modifier le fichier Resources.resx pour avoir les bons messages dans notre console d’alerte ou lorsqu’on survole les petites dents de scies que l’on va avoir à la fin de notre diagnostic de code.

Pour la partie code fix du projet on va la traiter dans un article séparé pour proposer un fix à l’utilisateur. Pour le moment, on va se concentrer sur la partie analyseur.

Allez Show time !!!! On va enfin écrire un peu de code

Notre super Analyseur :
Comme on l’a déjà mentionné au début de notre article, l’analyseur qu’on va développer aujourd’hui va nous notifier en cas d’opération de retour avec un tableau vide et nous proposer d’utiliser la syntaxe Array.Empty.

Si on analyse les actions que notre petit programme va faire, on peut énumérer cette liste :
Détecter si l’utilisateur a déclaré une opération de retour (return…)
Vérifier si cette opération inclut une création de tableau
Vérifier si ce tableau est de taille zéro
Créer un diagnostic et reporter le contexte actuel pour voir les petites lignes rouges sous notre code.

Simple non ?!!!

On va procéder point par point.

1 – Détecter si l’utilisateur a déclaré une opération de retour et déclencher une action

La méthode Initialize() dans notre fichier EmptyArrayAnalyzer.cs  nous permet d’initialiser notre analyseur.
Dans cette étape d’initialisation on peut s’abonner à plusieurs opérations/actions utilisateur et déclencher un diagnostic pour vérifier des conditions ou des metadata sur notre code.

Pour notre exemple, on va s’abonner à chaque déclaration, que l’utilisateur fait, d’une opération de retour (return).
Pour chaque opération détectée, on va lancer une méthode “AnalyzeReturnStatment” qui va inclure tous les check qu’on va faire pour notre diagnostic.
Notre méthode Initialize ressemblera à ça :

public override void Initialize(AnalysisContext context)
 {
     context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
     context.EnableConcurrentExecution();

   // On enregistre une action qui va se déclancher pour chaque operation de "return" détectée
     context.RegisterOperationAction(AnalyzeReturnStatement, OperationKind.Return);
 }

private void AnalyzeReturnStatement(OperationAnalysisContext context)
 {
    // ETAPE 2
 }

Il est possible aussi de s’abonner à des nodes bien spécifiques dans le code ou à des types bien précis. Je vous laisse découvrir toutes les actions auxquelles on peut s’abonner dans notre analyseur.

Lise des opérations
Liste des actions

2 – Vérifier si cette opération inclut une création de tableau

A partir de cette étape tout notre travail sera dans la méthode AnalyzerReturnStatment créée précédemment.

Pour commencer notre diagnostic, il faut bien sûr, préparer nos outils de travail.

private void AnalyzeReturnStatement(OperationAnalysisContext context)
 {
     // On recupere notre operation de retour depuis le context
     var operation = context.Operation as IReturnOperation;

     // on récupere l'arbre syntaxique et le modele sementique de cette operation
     var syntaxTree = context.Operation.Syntax;
     var sementicModel = context.Operation.SemanticModel;
 }

Et là on peut interroger notre arbre syntaxique pour voir s’il y a une opération de création de tableau dans les nœuds de cette opération de retour.

private void AnalyzeReturnStatement(OperationAnalysisContext context)
 {
     // On recupere notre operation de retour depuis le context
     var operation = context.Operation as IReturnOperation;

     // on récupere l'arbre syntaxique et le modele sementique de cette operation
     var syntaxTree = context.Operation.Syntax;
     var sementicModel = context.Operation.SemanticModel;

     // on parcoure touts les noeuds de notre operation pour voir
     // si il y a une operation de creation de tableau dans notre operation de retour
     var arrayCreationExpression = syntaxTree.ChildNodes()
             .SingleOrDefault(el => el.Kind() == SyntaxKind.ArrayCreationExpression);

    // si on arrive a trouver une operation de creation de tableau dans notre "return"
       if (arrayCreationExpression != null)
        {
            // ETAPE 3
        }
 }

3 – Vérifier si ce tableau est de taille zéro 

Pour vérifier que notre tableau est de taille 0, on va créer une fonction locale IsArrayEmpty qui va faire le check pour nous.
Mais avant ça il faut récupérer plus d’informations sur cette opération de création de tableau. Comme on l’a indiqué dans la première partie de l’article la “Syntax Tree” est une représentation syntaxique de notre code qui présente une succession de symbole connu par le compilateur, mais pour avoir plus d’informations sur un symbole il faut interroger une autre API qui est le “Sementic Model API” comme ça on peut récupérer par exemple la dimension du tableau ou le type d’éléments de celui-ci.

// si on arrive a trouver une operation de creation de tableau dans notre "return"
if (arrayCreationExpression != null)
 {
   // on demande a notre sementicModel plus d'information sur notre operation
   // comme ça on peut avoir la dimension du tableau
   var arrayCreationOp = (IArrayCreationOperation)sementicModel.GetOperation(arrayCreationExpression);

   if (IsArrayEmpty(arrayCreationOp))
    {
          // ETAPE 4
    }
 }

    bool IsArrayEmpty(IArrayCreationOperation arrayCreationOp)
     {
        return arrayCreationOp.DimensionSizes.First().ConstantValue.HasValue
            && (int)arrayCreationOp.DimensionSizes.First().ConstantValue.Value == 0;
     }

4 – Créer un diagnostic et reporter le contexte actuel

if (IsArrayEmpty(arrayCreationOp))
 {
     // si notre tableau est vide on crée un diagnostique et le signale
     var diagnostic = Diagnostic.Create(Rule, syntaxTree.GetLocation());

     context.ReportDiagnostic(diagnostic)
 }


N’oubliez pas de supprimer les commentaires :D. Il faut suivre les bonnes pratiques si vous êtes en train de créer un analyseur de code.

Étape 3 :  Exécuter et tester notre code

Pour tester notre code il suffit de lancer le projet d’extension .VIX

Visual Studio va installer notre analyseur en tant qu’extension dans une nouvelle instance Visual Studio et la lancer pour nous. Vous pouvez sélectionner n’importe quel projet existant ou en créer un nouveau.

Et voilaaaaaa !!!!!!

Happy Grand Slam GIF by Roland-Garros


Peut-on proposer un code fix directement pour l’utilisateur ?
La réponse est oui !!!

Dans cet article on a vu ensemble comment alerter notre développeur sur une potentielle amélioration mais c’est possible aussi de passer à l’action et proposer un code fix

Ca sera le sujet de notre deuxième article. Donc Stay tuned !!!

Tout le code du projet est sur ce repo


J’espère que cet article vous a apporté un plus

Lien utils: 

Analyse du code à l’aide d’analyseurs Roslyn – Visual Studio | Microsoft Docs

GitHub – dotnet/roslyn-analyzers

Source Browser

Nombre de vue : 142

AJOUTER UN COMMENTAIRE