Création d'un analyseur Roslyn en utilisant les tests d'encapsulation comme exemple

Qu'est-ce que Roslyn?


Roslyn est un ensemble de compilateurs open source et d'API d'analyse de code pour les langages C # et VisualBasic .NET de Microsoft.


L'analyseur Roslyn est un outil puissant pour analyser le code, trouver des erreurs et les corriger.


Arbre de syntaxe et modèle sémantique


Pour analyser le code, vous devez avoir une compréhension de l'arbre de syntaxe et du modèle sémantique, car ce sont les deux principaux composants de l'analyse statique.


Un arbre de syntaxe est un élément qui est construit sur la base du code source du programme, et est nécessaire pour l'analyse de code. Lors de l'analyse du code, il se déplace le long de celui-ci.


Chaque code a un arbre de syntaxe. Pour l'objet de classe suivant


class A { void Method() { } } 

l'arbre de syntaxe ressemblera à ceci:


Arbre


Un objet de type SyntaxTree est un arbre de syntaxe. Trois éléments principaux peuvent être distingués dans l'arborescence: SyntaxNodes, SyntaxTokens, SyntaxTrivia.


Les nœuds de syntaxe décrivent des constructions syntaxiques, à savoir des déclarations, des opérateurs, des expressions, etc. En C #, les constructions syntaxiques représentent une classe de type SyntaxNode.


Syntaxtokens décrit des éléments tels que: identifiants, mots clés, caractères spéciaux. En C #, il s'agit d'un type de classe SyntaxToken.


Syntaxtrivia décrit les éléments qui ne seront pas compilés, à savoir les espaces, les sauts de ligne, les commentaires, les directives du préprocesseur. En C #, il est défini par une classe de type SyntaxTrivia.


Le modèle sémantique représente des informations sur les objets et leurs types. Grâce à cet outil, vous pouvez effectuer une analyse approfondie et complexe. En C #, il est défini par une classe de type SemanticModel.


Création d'un analyseur


Pour créer un analyseur statique, vous devez installer le composant .NETCompilerPlatformSDK suivant.


Les principales fonctions qui composent tout analyseur incluent:


  1. Enregistrement des actions.
    Les actions sont des modifications de code que l'analyseur doit initier afin de vérifier le code pour les violations. Lorsque VisualStudio détecte des modifications de code qui correspondent à l'action enregistrée, il appelle la méthode d'analyseur enregistrée.
  2. Créez des diagnostics.
    Lorsqu'une violation est détectée, l'analyseur crée un objet de diagnostic utilisé par VisualStudio pour informer l'utilisateur de la violation.

Il existe plusieurs étapes pour créer et tester un analyseur:


  1. Créez une solution.
  2. Enregistrez le nom et la description de l'analyseur.
  3. Avertissements et recommandations de l'analyseur de rapports.
  4. Effectuez une correction de code pour accepter les recommandations.
  5. Amélioration de l'analyse avec des tests unitaires.

Les actions sont enregistrées dans un remplacement de la méthode DiagnosticAnalyzer.Initialize (AnalysisContext), où AnalysisContext est la méthode dans laquelle la recherche de l'objet analysé est fixe.


L'analyseur peut fournir une ou plusieurs corrections de code. Un correctif de code identifie les modifications qui résolvent le problème signalé. L'utilisateur choisit les modifications dans l'interface utilisateur (ampoules dans l'éditeur) et VisualStudio modifie le code. La méthode RegisterCodeFixesAsync décrit comment modifier le code.


Exemple


Pour un exemple, nous allons écrire l'analyseur de champs publics. Cette application doit avertir l'utilisateur des champs publics et offrir la possibilité d'encapsuler le champ avec une propriété.


Voici ce que vous devriez obtenir:


exemple de travail


Voyons ce qui doit être fait pour cela.


Vous devez d'abord créer une solution.


prise de décision


Après avoir créé la solution, nous voyons qu'il y a déjà trois projets.


arbre de décision


Nous avons besoin de deux classes:


1) Classe AnalyzerPublicFieldsAnalyzer, dans laquelle nous spécifions les critères d'analyse du code pour trouver des champs publics et une description de l'avertissement pour l'utilisateur.


Nous indiquons les propriétés suivantes:


 public const string DiagnosticId = "PublicField"; private const string Title = "Filed is public"; private const string MessageFormat = "Field '{0}' is public"; private const string Category = "Syntax"; private static DiagnosticDescriptor Rule = new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: true); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } 

Après cela, nous indiquons par quels critères l'analyse des domaines publics aura lieu.


 private static void AnalyzeSymbol(SymbolAnalysisContext context) { var fieldSymbol = context.Symbol as IFieldSymbol; if (fieldSymbol != null && fieldSymbol.DeclaredAccessibility == Accessibility.Public && !fieldSymbol.IsConst && !fieldSymbol.IsAbstract && !fieldSymbol.IsStatic && !fieldSymbol.IsVirtual && !fieldSymbol.IsOverride && !fieldSymbol.IsReadOnly && !fieldSymbol.IsSealed && !fieldSymbol.IsExtern) { var diagnostic = Diagnostic.Create(Rule, fieldSymbol.Locations[0], fieldSymbol.Name); context.ReportDiagnostic(diagnostic); } } 

Nous obtenons un champ d'un objet de type IFieldSymbol, qui a des propriétés pour définir des modificateurs de champ, son nom et son emplacement. Ce dont nous avons besoin pour le diagnostic.


Il reste à initialiser l'analyseur en spécifiant dans la méthode surchargée


 public override void Initialize(AnalysisContext context) { context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.Field); } 

2) Nous allons maintenant modifier le code proposé par l'utilisateur sur la base de l'analyse du code. Cela se produit dans la classe AnalyzerPublicFieldsCodeFixProvider.


Pour ce faire, indiquez les informations suivantes:


 private const string title = "Encapsulate field"; public sealed override ImmutableArray<string> FixableDiagnosticIds { get { return ImmutableArray.Create(AnalyzerPublicFieldsAnalyzer.DiagnosticId); } } public sealed override FixAllProvider GetFixAllProvider() { return WellKnownFixAllProviders.BatchFixer; } public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken) .ConfigureAwait(false); var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; var initialToken = root.FindToken(diagnosticSpan.Start); context.RegisterCodeFix( CodeAction.Create(title, c => EncapsulateFieldAsync(context.Document, initialToken, c), AnalyzerPublicFieldsAnalyzer.DiagnosticId), diagnostic); } 

Et nous déterminons la capacité d'encapsuler le champ avec une propriété dans la méthode EncapsulateFieldAsync.


 private async Task<Document> EncapsulateFieldAsync(Document document, SyntaxToken declaration, CancellationToken cancellationToken) { var field = FindAncestorOfType<FieldDeclarationSyntax>(declaration.Parent); var fieldType = field.Declaration.Type; ChangeNameFieldAndNameProperty(declaration.ValueText, out string fieldName, out string propertyName); var fieldDeclaration = CreateFieldDecaration(fieldName, fieldType); var propertyDeclaration = CreatePropertyDecaration(fieldName, propertyName, fieldType); var root = await document.GetSyntaxRootAsync(); var newRoot = root.ReplaceNode(field, new List<SyntaxNode> { fieldDeclaration, propertyDeclaration }); var newDocument = document.WithSyntaxRoot(newRoot); return newDocument; } 

Pour ce faire, créez un champ privé.


 private FieldDeclarationSyntax CreateFieldDecaration(string fieldName, TypeSyntax fieldType) { var variableDeclarationField = SyntaxFactory.VariableDeclaration(fieldType) .AddVariables(SyntaxFactory.VariableDeclarator(fieldName)); return SyntaxFactory.FieldDeclaration(variableDeclarationField) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PrivateKeyword)); } 

Créez ensuite une propriété publique qui renvoie et reçoit ce champ privé.


 private PropertyDeclarationSyntax CreatePropertyDecaration(string fieldName, string propertyName, TypeSyntax propertyType) { var syntaxGet = SyntaxFactory.ParseStatement($"return {fieldName};"); var syntaxSet = SyntaxFactory.ParseStatement($"{fieldName} = value;"); return SyntaxFactory.PropertyDeclaration(propertyType, propertyName) .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithBody(SyntaxFactory.Block(syntaxGet)), SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithBody(SyntaxFactory.Block(syntaxSet))); } 

Dans le même temps, nous enregistrons le type et le nom du champ source. Le nom du champ est construit comme suit "_name" et le nom de la propriété "Name".


Les références


  1. Sources GitHub
  2. SDK de la plate-forme du compilateur .NET

Source: https://habr.com/ru/post/fr455844/


All Articles