Partager via


Tutoriel : Écrire votre premier analyseur et correctif de code

Le SDK .NET Compiler Platform fournit les outils dont vous avez besoin pour créer des diagnostics personnalisés (analyseurs), des correctifs de code, une refactorisation du code et des suppresseurs de diagnostic qui ciblent le code C# ou Visual Basic. Un analyseur contient le code qui reconnaît les violations de votre règle. Votre correctif de code contient le code qui résout la violation. Les règles que vous implémentez peuvent aller de la structure du code au style de codage et aux conventions d’affectation de noms, et bien plus encore. .NET Compiler Platform fournit le framework permettant d’exécuter l’analyse alors que les développeurs écrivent du code, et toutes les fonctionnalités de l’IU Visual Studio pour corriger le code : afficher des tildes dans l’éditeur, renseigner la liste d’erreurs Visual Studio, créer des suggestions « ampoule » et afficher un aperçu détaillé des corrections suggérées.

Dans ce tutoriel, vous allez explorer la création d’un analyseur et d’un correctif de code associé à l’aide des API Roslyn. Un analyseur consiste à effectuer une analyse du code source et signaler un problème à l’utilisateur. Si vous le souhaitez, un correctif de code peut être associé à l’analyseur pour représenter une modification du code source de l’utilisateur. Ce tutoriel crée un analyseur qui recherche des déclarations de variables locales qui pourraient être déclarées à l’aide du modificateur const mais qui ne le sont pas. Le correctif de code associé modifie ces déclarations pour ajouter le modificateur const.

Prérequis

Vous devez installer le SDK .NET Compiler Platform avec Visual Studio Installer :

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.

Il existe plusieurs étapes pour créer et valider votre analyseur :

  1. Créez la solution.
  2. Enregistrez le nom de l’analyseur et sa description.
  3. Créez un rapport sur les avertissements et les recommandations de l’analyseur.
  4. Implémentez le correctif de code pour accepter les recommandations.
  5. Améliorez l’analyse par le biais de tests unitaires.

Créez la solution

  • Dans Visual Studio, choisissez Fichier > Nouveau > Projet... pour afficher la boîte de dialogue Nouveau projet.
  • Sous Visual C# > Extensibilité, choisissez Analyseur avec correctif de code (.NET Standard).
  • Nommez votre projet « MakeConst », puis cliquez sur OK.

Notes

Vous pouvez obtenir une erreur de compilation (MSB4062 : Impossible de charger la tâche « CompareBuildTaskVersion »). Pour la corriger, mettez à jour les packages NuGet dans la solution avec le Gestionnaire de package NuGet ou utilisez Update-Package dans la fenêtre Console du Gestionnaire de package.

Explorer le modèle d’analyseur

Le modèle d’analyseur avec correctif de code crée cinq projets :

  • MakeConst, qui contient l’analyseur.
  • MakeConst.CodeFixes, qui contient le correctif de code.
  • MakeConst.Package, qui est utilisé pour produire un package NuGet pour l’analyseur et le correctif de code.
  • MakeConst.Test, qui est un projet de test unitaire.
  • MakeConst.Vsix, qui est le projet de démarrage par défaut qui démarre une deuxième instance de Visual Studio qui a chargé votre nouvel analyseur. Appuyez sur F5 pour démarrer le projet VSIX.

Notes

Les analyseurs doivent cibler .NET Standard 2.0, car ils peuvent s’exécuter dans les environnements .NET Core (builds en ligne de commande) et .NET Framework (Visual Studio).

Conseil

Lorsque vous exécutez votre analyseur, vous démarrez une deuxième copie de Visual Studio. Cette deuxième copie utilise un hive de Registre différent pour stocker les paramètres. Cela vous permet de différencier les paramètres Visual dans les deux copies de Visual Studio. Vous pouvez choisir un autre thème pour l’exécution expérimentale de Visual Studio. En outre, ne rendez pas vos paramètres itinérants et ne vous connectez pas à votre compte Visual Studio à l’aide de l’exécution expérimentale de Visual Studio. Cela permet de conserver les paramètres différents.

La ruche inclut non seulement l’analyseur en cours de développement, mais aussi tous les analyseurs précédents ouverts. Pour réinitialiser la ruche Roslyn, vous devez la supprimer manuellement depuis %LocalAppData%\Microsoft\VisualStudio. Le nom de dossier de la ruche Roslyn se termine par Roslyn, par exemple, 16.0_9ae182f9Roslyn. Notez que vous devrez peut-être nettoyer la solution et la reconstruire après la suppression de la ruche.

Dans la deuxième instance de Visual Studio que vous venez de démarrer, créez un projet d’application console C# (toutes les versions cibles de .NET Framework iront, car les analyseurs fonctionnent au niveau de la source). Pointez sur le jeton avec un soulignement ondulé et le texte d’avertissement fourni par un analyseur apparaît.

Le modèle crée un analyseur qui émet un avertissement sur chaque déclaration de type dont le nom de type contient des lettres minuscules, comme indiqué dans la figure suivante :

Avertissement de l’analyseur

Le modèle fournit également un correctif de code qui modifie n’importe quel nom de type contenant des caractères minuscules en majuscules. Vous pouvez cliquer sur l’ampoule affichée avec l’avertissement pour voir les modifications suggérées. Le fait d’accepter les modifications suggérées met à jour le nom de type et toutes les références à ce type dans la solution. Maintenant que vous avez vu l’analyseur initial en action, fermez la deuxième instance de Visual Studio et revenez à votre projet d’analyseur.

Vous n’êtes pas obligé de démarrer une deuxième copie de Visual Studio et de créer un nouveau code pour tester chaque modification de votre analyseur. Le modèle crée également un projet de test unitaire pour vous. Ce projet contient deux tests. TestMethod1 montre le format classique d’un test qui analyse le code sans déclencher un diagnostic. TestMethod2 montre le format d’un test qui déclenche un diagnostic, puis applique un correctif de code proposé. À mesure que vous générez votre analyseur et votre correctif de code, vous écrirez des tests pour des structures de codes différentes afin de vérifier votre travail. Les tests unitaires pour les analyseurs sont beaucoup plus rapides que de les tester de manière interactive avec Visual Studio.

Conseil

Les tests unitaires d’analyseur sont un excellent outil lorsque vous savez quelles constructions de code doivent et ne doivent pas déclencher votre analyseur. Le chargement de votre analyseur dans une autre copie de Visual Studio est un excellent outil pour explorer et rechercher des constructions auxquelles vous n’avez peut-être pas encore pensé.

Dans ce tutoriel, vous écrivez un analyseur qui signale à l’utilisateur les déclarations de variables locales qui peuvent être converties en constantes locales. Considérons par exemple le code suivant :

int x = 0;
Console.WriteLine(x);

Dans le code ci-dessus, une valeur constante est attribuée à x et n’est jamais modifiée. Elle peut être déclarée à l’aide du modificateur const :

const int x = 0;
Console.WriteLine(x);

L’analyse pour déterminer si une variable peut être déclarée constante est impliquée, nécessitant une analyse syntaxique, une analyse constante de l’expression de l’initialiseur et une analyse du flux de données pour s’assurer que la variable n’est jamais accessible en écriture. .NET Compiler Platform fournit les API qui simplifient l’exécution de cette analyse.

Créer des enregistrements d’analyseur

Le modèle crée la classe DiagnosticAnalyzer initiale, dans le fichier MakeConstAnalyzer.cs. Cet analyseur initial montre deux propriétés importantes de chaque analyseur.

  • Chaque analyseur de diagnostic doit fournir un attribut [DiagnosticAnalyzer] qui décrit le langage sur lequel il opère.
  • Chaque analyseur de diagnostic doit dériver (directement ou indirectement) de la classe DiagnosticAnalyzer.

Le modèle montre également les fonctionnalités de base qui font partie de n’importe quel analyseur :

  1. Enregistrer les actions. Les actions représentent les modifications de code qui doivent déclencher votre analyseur pour qu’il examine les violations dans le code. Lorsque Visual Studio détecte des modifications de code qui correspondent à une action enregistré, il appelle la méthode enregistrée de votre analyseur.
  2. Créer des diagnostics. Quand votre analyseur détecte une violation, il crée un objet de diagnostic que Visual Studio utilise pour informer l’utilisateur de la violation.

Vous enregistrez des actions dans votre substitution de la méthode DiagnosticAnalyzer.Initialize(AnalysisContext). Dans ce tutoriel, vous allez consulter des nœuds de syntaxe pour rechercher des déclarations locales et voir lesquelles ont des valeurs constantes. Si une déclaration peut être une constante, votre analyseur crée et signale un diagnostic.

La première étape consiste à mettre à jour les constantes d’inscription et la méthode Initialize pour que ces constantes indiquent votre analyseur « Make Const ». La plupart des constantes de chaîne est définie dans le fichier de ressources de chaîne. Cette pratique facilite la localisation. Ouvrez le fichier Resources.resx pour le projet d’analyseur MakeConst. Cela affiche l’éditeur de ressources. Mettez à jour les ressources de chaîne comme suit :

  • Remplacez AnalyzerDescription par « Variables that are not modified should be made constants. ».
  • Remplacez AnalyzerMessageFormat par « Variable '{0}' can be made constant ».
  • Remplacez AnalyzerTitle par « Variable can be made constant ».

Lorsque vous avez terminé, l’éditeur de ressources doit apparaître tel que montré dans l’illustration suivante :

Mettre à jour les ressources de chaîne

Les modifications restantes ont lieu dans le fichier de l’analyseur. Ouvrez MakeConstAnalyzer.cs dans Visual Studio. Modifiez l’action enregistrée d’une action qui agit sur les symboles à une action qui agit sur la syntaxe. Dans la méthode MakeConstAnalyzerAnalyzer.Initialize, recherchez la ligne qui enregistre l’action sur les symboles :

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Remplacez-la par la ligne suivante :

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Après cette modification, vous pouvez supprimer la méthode AnalyzeSymbol. Cet outil examine SyntaxKind.LocalDeclarationStatement, et non les instructions SymbolKind.NamedType. Notez que AnalyzeNode est souligné par des traits ondulés rouges. Le code que vous venez d’ajouter référence une méthode AnalyzeNode qui n’a pas été déclarée. Déclarez cette méthode en utilisant le code suivant :

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Remplacez Category par « Usage » dans MakeConstAnalyzer.cs, comme montré dans le code suivant :

private const string Category = "Usage";

Rechercher des déclarations locales qui pourraient être constantes

Il est temps d’écrire la première version de la méthode AnalyzeNode. Elle doit rechercher une déclaration locale unique qui peut être const mais ne l’est pas, comme le code suivant :

int x = 0;
Console.WriteLine(x);

La première étape consiste à rechercher les déclarations locales. Ajoutez le code suivant à AnalyzeNode dans MakeConstAnalyzer.cs :

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Ce cast réussit toujours, car votre analyseur a enregistré les modifications apportées aux déclarations locales, et uniquement les déclarations locales. Aucun autre type de nœud ne déclenche un appel à votre méthode AnalyzeNode. Ensuite, vérifiez la présence de modificateurs const dans la déclaration. Si vous en trouvez, retournez immédiatement. Le code suivant recherche les modificateurs const sur la déclaration locale :

// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
    return;
}

Enfin, vous devez vérifier que la variable peut être const. Cela signifie de s’assurer qu’elle n’est jamais assignée après son initialisation.

Vous allez effectuer une analyse sémantique à l’aide de SyntaxNodeAnalysisContext. Vous utilisez l’argument context pour déterminer si la déclaration de variable locale peut être rendue const. Microsoft.CodeAnalysis.SemanticModel représente toutes les informations sémantiques dans un seul fichier source. Vous pouvez en apprendre plus dans l’article qui traite des modèles sémantiques. Vous allez utiliser Microsoft.CodeAnalysis.SemanticModel pour effectuer une analyse de flux de données sur l’instruction de déclaration locale. Ensuite, vous utilisez les résultats de cette analyse de flux de données pour vous assurer que la variable locale n’est pas écrite avec une nouvelle valeur ailleurs. Appelez la méthode d’extension GetDeclaredSymbol pour récupérer ILocalSymbol pour la variable et vérifiez qu’il n’est pas contenu avec la collection DataFlowAnalysis.WrittenOutside de l’analyse de flux de données. Ajoutez le code suivant à la fin de la méthode AnalyzeNode :

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

Le code qui vient d’être ajouté garantit que la variable n’est pas modifiée et peut par conséquent devenir const. Il est temps de générer le diagnostic. Ajoutez le code suivant comme dernière ligne dans AnalyzeNode :

context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));

Vous pouvez vérifier votre progression en appuyant sur F5 pour exécuter votre analyseur. Vous pouvez charger l’application console que vous avez créée précédemment et ajouter le code de test suivant :

int x = 0;
Console.WriteLine(x);

L’ampoule doit apparaître et votre analyseur doit signaler un diagnostic. Toutefois, en fonction de votre version de Visual Studio, vous verrez :

  • L’ampoule, qui utilise toujours le correctif de code généré par le modèle et vous indique qu’il peut être converti en majuscules.
  • Un message de bannière en haut de l’éditeur indiquant que « MakeConstCodeFixProvider a rencontré une erreur et a été désactivé ». C’est parce que le fournisseur de correctifs de code n’a pas encore été modifié et s’attend toujours à trouver les éléments TypeDeclarationSyntax au lieu des éléments LocalDeclarationStatementSyntax.

La section suivante explique comment écrire le correctif de code.

Écrire le correctif de code

Un analyseur peut fournir un ou plusieurs correctifs de code. Un correctif de code définit une modification qui traite le problème signalé. Pour l’analyseur que vous avez créé, vous pouvez fournir un correctif de code qui insère le mot clé const :

- int x = 0;
+ const int x = 0;
Console.WriteLine(x);

L’utilisateur le choisit à partir de l’IU ampoule dans l’éditeur et Visual Studio modifie le code.

Ouvrez le fichier CodeFixResources.resx et remplacez CodeFixTitle par « Make constant ».

Ouvrez le fichier MakeConstCodeFixProvider.cs ajouté par le modèle. Ce correctif de code est déjà associé à l’ID de diagnostic produit par votre analyseur de diagnostic, mais il n’implémente pas encore la transformation de code adéquate.

Ensuite, supprimez la méthode MakeUppercaseAsync. Elle n’est plus applicable.

Tous les fournisseurs de correctif de code dérivent de CodeFixProvider. Ils substituent tous CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) pour signaler les correctifs de code disponibles. Dans RegisterCodeFixesAsync, remplacez le type de nœud ancêtre que vous recherchez par LocalDeclarationStatementSyntax pour faire correspondre le diagnostic :

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

Ensuite, modifiez la dernière ligne pour enregistrer un correctif de code. Votre correctif crée un nouveau document obtenu en ajoutant le modificateur const à une déclaration existante :

// Register a code action that will invoke the fix.
context.RegisterCodeFix(
    CodeAction.Create(
        title: CodeFixResources.CodeFixTitle,
        createChangedDocument: c => MakeConstAsync(context.Document, declaration, c),
        equivalenceKey: nameof(CodeFixResources.CodeFixTitle)),
    diagnostic);

Notez les traits de soulignement ondulés rouges dans le code que vous venez d’ajouter sur le symbole MakeConstAsync. Ajoutez une déclaration pour MakeConstAsync comme le code suivant :

private static async Task<Document> MakeConstAsync(Document document,
    LocalDeclarationStatementSyntax localDeclaration,
    CancellationToken cancellationToken)
{
}

Votre nouvelle méthode MakeConstAsync transformera Document qui représente le fichier source de l’utilisateur en un nouveau Document qui contient à présent une déclaration const.

Vous créez un nouveau jeton de mot clé const à insérer au début de l’instruction de déclaration. Veillez tout d’abord à supprimer les trivia de début dans le premier jeton de l’instruction de déclaration et à les associer au jeton const. Ajoutez le code suivant à la méthode MakeConstAsync :

// Remove the leading trivia from the local declaration.
SyntaxToken firstToken = localDeclaration.GetFirstToken();
SyntaxTriviaList leadingTrivia = firstToken.LeadingTrivia;
LocalDeclarationStatementSyntax trimmedLocal = localDeclaration.ReplaceToken(
    firstToken, firstToken.WithLeadingTrivia(SyntaxTriviaList.Empty));

// Create a const token with the leading trivia.
SyntaxToken constToken = SyntaxFactory.Token(leadingTrivia, SyntaxKind.ConstKeyword, SyntaxFactory.TriviaList(SyntaxFactory.ElasticMarker));

Ensuite, ajoutez le jeton const dans la déclaration à l’aide du code suivant :

// Insert the const token into the modifiers list, creating a new modifiers list.
SyntaxTokenList newModifiers = trimmedLocal.Modifiers.Insert(0, constToken);
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal
    .WithModifiers(newModifiers)
    .WithDeclaration(localDeclaration.Declaration);

Ensuite, mettez en forme la nouvelle déclaration en fonction des règles de mise en forme en C#. La mise en forme de vos modifications pour correspondre au code existant crée une meilleure expérience. Ajoutez l’instruction suivante immédiatement après le code existant :

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

Un nouvel espace de noms est requis pour ce code. Ajoutez la directive using suivante en haut du fichier :

using Microsoft.CodeAnalysis.Formatting;

L’étape finale consiste à apporter votre modification. Ce processus comprend trois étapes :

  1. Obtenir un handle pour le document existant.
  2. Créer un document en remplaçant la déclaration existante par la nouvelle déclaration.
  3. Retourner le nouveau document.

Ajoutez le code suivant à la fin de la méthode MakeConstAsync :

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

Votre correctif de code est prêt à être testé. Appuyez sur F5 pour exécuter le projet d’analyseur dans une deuxième instance de Visual Studio. Dans la deuxième instance de Visual Studio, créez un projet Application console C# et ajoutez quelques déclarations de variables locales initialisées avec des valeurs constantes à la méthode Main. Vous verrez que ces éléments sont signalés en tant qu’avertissements comme indiqué ci-dessous.

Avertissements Can make const

Vous avez bien progressé. Des traits de soulignement ondulés s’affichent sous les déclarations qui peuvent devenir const. Mais il reste encore du travail. Cela fonctionne si vous ajoutez const aux déclarations qui commencent par i, puis j et enfin k. Mais, si le modificateur const est ajouté dans un autre ordre, en commençant par k, l’analyseur crée des erreurs : k ne peut pas être déclaré const, sauf si i et j sont tous deux déjà const. Vous devez effectuer d’autres analyses afin de vous assurer que vous gérez les différentes façons dont les variables peuvent être déclarées et initialisées.

Créer des tests unitaires

Votre analyseur et votre correctif de code travaillent sur un cas simple d’une déclaration unique qui peut être devenir constante. Il existe de nombreuses instructions de déclaration possibles où cette implémentation commet des erreurs. Vous allez traiter ces cas en travaillant avec la bibliothèque de tests unitaires écrite par le modèle. C’est beaucoup plus rapide que d’ouvrir plusieurs fois une deuxième copie de Visual Studio.

Ouvrez le fichier MakeConstUnitTests.cs dans le projet de test unitaire. Le modèle a créé deux tests qui suivent les deux modèles courants pour un test unitaire d’analyseur et de correctif de code. TestMethod1 montre le modèle pour un test qui garantit que l’analyseur ne signale pas un diagnostic lorsqu’il ne le devrait pas. TestMethod2 montre le modèle pour créer un rapport de diagnostic et exécuter le correctif de code.

Le modèle utilise les packages Microsoft.CodeAnalysis.Testing pour les tests unitaires.

Conseil

La bibliothèque de tests prend en charge une syntaxe de balisage spéciale, notamment :

  • [|text|] : indique qu’un diagnostic est signalé pour text. Par défaut, ce formulaire peut être utilisé seulement pour tester des analyseurs avec exactement un DiagnosticDescriptor fourni par DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|} : indique qu’un diagnostic avec IdExpectedDiagnosticId est signalé pour text.

Remplacez les tests de modèle dans la classe MakeConstUnitTest par la méthode de test suivante :

        [TestMethod]
        public async Task LocalIntCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|int i = 0;|]
        Console.WriteLine(i);
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int i = 0;
        Console.WriteLine(i);
    }
}
");
        }

Exécutez ce test pour vous assurer qu’il réussit. Dans Visual Studio, ouvrez l’Explorateur de tests en sélectionnant Test>Windows>Explorateur de tests. Ensuite, sélectionnez Exécuter tout.

Créer des tests pour les déclarations valides

En règle générale, les analyseurs doivent s’arrêter aussi rapidement que possible, en effectuant un minimum de travail. Visual Studio appelle les analyseurs enregistrés lorsque l’utilisateur modifie le code. La réactivité est essentielle. Il existe plusieurs cas de test pour le code qui ne doivent pas déclencher votre diagnostic. Votre analyseur a déjà géré l’un de ces tests, le cas où une variable est assignée après avoir été initialisée. Ajoutez la méthode de test suivante pour représenter ce cas :

        [TestMethod]
        public async Task VariableIsAssigned_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0;
        Console.WriteLine(i++);
    }
}
");
        }

Ce test réussit également. Ensuite, ajoutez des méthodes de test pour les conditions que vous n’avez pas encore gérées :

  • Déclarations qui sont déjà const, car elles sont déjà constantes :

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Déclarations sans initialiseur, car il n’existe aucune valeur à utiliser :

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Déclarations où l’initialiseur n’est pas une constante, car elles ne peuvent pas être des constantes de compilation :

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

Cela peut être encore plus compliqué, car C# accepte que plusieurs déclarations forment une seule instruction. Prenez en compte la constante de chaîne de cas de test suivante :

        [TestMethod]
        public async Task MultipleInitializers_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int i = 0, j = DateTime.Now.DayOfYear;
        Console.WriteLine(i);
        Console.WriteLine(j);
    }
}
");
        }

La variable i peut devenir constante, mais la variable j ne le peut pas. Par conséquent, cette instruction ne peut pas devenir une déclaration constante.

Réexécutez vos tests, et vous verrez ces nouveaux cas de test échouent.

Mettre à jour votre analyseur afin d’ignorer les déclarations correctes

Vous avez besoin de certaines améliorations dans la méthode AnalyzeNode de votre analyseur pour éliminer le code correspondant à ces conditions. Ce sont toutes des conditions associées, donc des modifications similaires résoudront toutes ces conditions. Dans AnalyzeNode, effectuez les changements suivants :

  • Votre analyse sémantique a examiné une déclaration de variable unique. Ce code doit se trouver dans une boucle foreach qui examine toutes les variables déclarées dans la même instruction.
  • Chaque variable déclarée doit avoir un initialiseur.
  • L’initialiseur de chaque variable déclarée doit être une constante de compilation.

Dans votre méthode AnalyzeNode, remplacez l’analyse sémantique d’origine :

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

par l’extrait de code suivant :

// Ensure that all variables in the local declaration have initializers that
// are assigned with constant values.
foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    EqualsValueClauseSyntax initializer = variable.Initializer;
    if (initializer == null)
    {
        return;
    }

    Optional<object> constantValue = context.SemanticModel.GetConstantValue(initializer.Value, context.CancellationToken);
    if (!constantValue.HasValue)
    {
        return;
    }
}

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

foreach (VariableDeclaratorSyntax variable in localDeclaration.Declaration.Variables)
{
    // Retrieve the local symbol for each variable in the local declaration
    // and ensure that it is not written outside of the data flow analysis region.
    ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
    if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
    {
        return;
    }
}

La première boucle foreach examine chaque déclaration de variable à l’aide de l’analyse syntaxique. La première vérification garantit que la variable a un initialiseur. La deuxième vérification garantit que l’initialiseur est une constante. La deuxième boucle contient l’analyse sémantique d’origine. Les vérifications sémantiques sont dans une boucle distincte, car elles ont un impact plus important sur les performances. Réexécutez vos tests. Ils doivent tous réussir.

Ajouter la touche finale

Vous avez presque terminé. Il existe quelques conditions de plus que votre analyseur doit gérer. Visual Studio appelle les analyseurs lorsque l’utilisateur écrit du code. Souvent, votre analyseur sera appelé lorsque du code ne se compile pas. La méthode AnalyzeNode de votre analyseur de diagnostic ne vérifie pas si la valeur de constante est convertible en type de variable. Par conséquent, l’implémentation actuelle convertit sans problème une déclaration incorrecte comme int i = "abc" en constante locale. Ajoutez une méthode de test pour ce cas :

        [TestMethod]
        public async Task DeclarationIsInvalid_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

En outre, les types de référence ne sont pas gérés correctement. La seule valeur de constante autorisée pour un type de référence est null, sauf dans le cas de System.String, qui autorise des littéraux de chaîne. En d’autres termes, const string s = "abc" est légal, mais pas const object s = "abc". Cet extrait de code vérifie cette condition :

        [TestMethod]
        public async Task DeclarationIsNotString_NoDiagnostic()
        {
            await VerifyCS.VerifyAnalyzerAsync(@"
using System;

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

Pour être exhaustif, vous devez ajouter un autre test afin de vous assurer que vous pouvez créer une déclaration de constante pour une chaîne. L’extrait de code suivant définit le code qui déclenche le diagnostic et le code une fois que le correctif a été appliqué :

        [TestMethod]
        public async Task StringCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|string s = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string s = ""abc"";
    }
}
");
        }

Enfin, si une variable est déclarée avec le mot clé var, le correctif de code fait la mauvaise chose et génère une déclaration const var, qui n’est pas prise en charge par le langage C#. Pour corriger ce bogue, le correctif de code doit remplacer le mot clé var par le nom du type déduit :

        [TestMethod]
        public async Task VarIntDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = 4;|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const int item = 4;
    }
}
");
        }

        [TestMethod]
        public async Task VarStringDeclarationCouldBeConstant_Diagnostic()
        {
            await VerifyCS.VerifyCodeFixAsync(@"
using System;

class Program
{
    static void Main()
    {
        [|var item = ""abc"";|]
    }
}
", @"
using System;

class Program
{
    static void Main()
    {
        const string item = ""abc"";
    }
}
");
        }

Heureusement, tous les bogues ci-dessus peuvent être gérés à l’aide des mêmes techniques que celles que vous venez d’apprendre.

Pour corriger le premier bogue, commencez par ouvrir MakeConstAnalyzer.cs et recherchez la boucle foreach où chacun des initialiseurs de la déclaration locale est vérifié pour s’assurer que des valeurs constantes leur sont attribués. Immédiatement avant la première boucle foreach, appelez context.SemanticModel.GetTypeInfo() pour récupérer des informations détaillées sur le type déclaré de la déclaration locale :

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

Ensuite, dans votre boucle foreach, vérifiez chaque initialiseur pour vous assurer qu’il peut être converti dans le type de variable. Ajoutez la vérification suivante après avoir vérifié que l’initialiseur est une constante :

// Ensure that the initializer value can be converted to the type of the
// local declaration without a user-defined conversion.
Conversion conversion = context.SemanticModel.ClassifyConversion(initializer.Value, variableType);
if (!conversion.Exists || conversion.IsUserDefined)
{
    return;
}

La modification suivante s’appuie sur la précédente. Avant l’accolade fermante de la première boucle foreach, ajoutez le code suivant pour vérifier le type de la déclaration locale si la constante est une chaîne ou une valeur null.

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

Vous devez écrire un peu plus de code dans votre fournisseur de correctif de code pour remplacer le mot clé var par le nom de type correct. Revenez à MakeConstCodeFixProvider.cs. Le code que vous ajouterez effectue les étapes suivantes :

  • Vérifiez si la déclaration est une déclaration var, et dans ce cas :
  • Créez un nouveau type pour le type déduit.
  • Assurez-vous que la déclaration de type n’est pas un alias. Le cas échéant, il est permis de déclarer const var.
  • Assurez-vous que var n’est pas un nom de type dans ce programme. (Dans ce cas, const var est autorisé).
  • Simplifier le nom de type complet

On dirait que beaucoup de code est nécessaire pour cela. Ce n’est pas le cas. Remplacez la ligne qui déclare et initialise newLocal par le code suivant. Il se place immédiatement après l’initialisation de newModifiers :

// If the type of the declaration is 'var', create a new type name
// for the inferred type.
VariableDeclarationSyntax variableDeclaration = localDeclaration.Declaration;
TypeSyntax variableTypeName = variableDeclaration.Type;
if (variableTypeName.IsVar)
{
    SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);

    // Special case: Ensure that 'var' isn't actually an alias to another type
    // (e.g. using var = System.String).
    IAliasSymbol aliasInfo = semanticModel.GetAliasInfo(variableTypeName, cancellationToken);
    if (aliasInfo == null)
    {
        // Retrieve the type inferred for var.
        ITypeSymbol type = semanticModel.GetTypeInfo(variableTypeName, cancellationToken).ConvertedType;

        // Special case: Ensure that 'var' isn't actually a type named 'var'.
        if (type.Name != "var")
        {
            // Create a new TypeSyntax for the inferred type. Be careful
            // to keep any leading and trailing trivia from the var keyword.
            TypeSyntax typeName = SyntaxFactory.ParseTypeName(type.ToDisplayString())
                .WithLeadingTrivia(variableTypeName.GetLeadingTrivia())
                .WithTrailingTrivia(variableTypeName.GetTrailingTrivia());

            // Add an annotation to simplify the type name.
            TypeSyntax simplifiedTypeName = typeName.WithAdditionalAnnotations(Simplifier.Annotation);

            // Replace the type in the variable declaration.
            variableDeclaration = variableDeclaration.WithType(simplifiedTypeName);
        }
    }
}
// Produce the new local declaration.
LocalDeclarationStatementSyntax newLocal = trimmedLocal.WithModifiers(newModifiers)
                           .WithDeclaration(variableDeclaration);

Vous devez ajouter une directive using pour utiliser le type Simplifier :

using Microsoft.CodeAnalysis.Simplification;

Exécutez vos tests. Ils doivent tous réussir. Félicitez-vous en exécutant votre analyseur terminé. Appuyez sur Ctrl+F5 pour exécuter le projet d’analyseur dans une deuxième instance de Visual Studio avec l’extension Roslyn Preview chargée.

  • Dans la deuxième instance de Visual Studio, créez un projet Application console C# et ajoutez int x = "abc"; à la méthode Main. Grâce à la correction du premier bogue, aucun avertissement ne doit être signalé pour cette déclaration de variable locale (même s’il existe une erreur du compilateur comme prévu).
  • Ensuite, ajoutez object s = "abc"; à la méthode Main. En raison de la correction du deuxième bogue, aucun avertissement ne doit être signalé.
  • Enfin, ajoutez une autre variable locale qui utilise le mot clé var. Un avertissement est signalé et une suggestion apparaît en dessous à gauche.
  • Déplacez le signe d’insertion de l’éditeur sur le trait de soulignement ondulé et appuyez sur Ctrl+.. pour afficher le correctif de code suggéré. Lors de la sélection de votre correctif de code, notez que le mot clé var est désormais géré correctement.

Enfin, ajoutez le code suivant :

int i = 2;
int j = 32;
int k = i + j;

Après ces modifications, vous obtenez des traits de soulignement ondulés rouges uniquement sur les deux premières variables. Ajoutez const à i et à j, et vous obtenez un nouvel avertissement sur k, car cet élément peut désormais devenir const.

Félicitations ! Vous avez créé votre première extension .NET Compiler Platform qui effectue une analyse de code à la volée pour détecter un problème et fournit une solution rapide pour résoudre ce problème. Tout au long du processus, vous avez découvert la plupart des API de code qui font partie du SDK .NET Compiler Platform (API Roslyn). Vous pouvez comparer votre travail avec l’exemple terminé dans notre référentiel GitHub d’exemples.

Autres ressources