Criando um analisador Roslyn usando o teste de encapsulamento como exemplo

O que é Roslyn?


Roslyn é um conjunto de APIs de análise de código e compiladores de código aberto para as linguagens C # e VisualBasic .NET da Microsoft.


O analisador Roslyn é uma ferramenta poderosa para analisar código, encontrar erros e corrigi-los.


Árvore de sintaxe e modelo semântico


Para analisar o código, você precisa entender a árvore da sintaxe e o modelo semântico, pois esses são os dois principais componentes da análise estática.


Uma árvore de sintaxe é um elemento criado com base no código fonte do programa e é necessário para a análise do código. Durante a análise do código, ele se move ao longo dele.


Cada código possui uma árvore de sintaxe. Para o próximo objeto de classe


class A { void Method() { } } 

a árvore de sintaxe ficará assim:


Árvore


Um objeto do tipo SyntaxTree é uma árvore de sintaxe. Três elementos principais podem ser distinguidos na árvore: SyntaxNodes, SyntaxTokens, SyntaxTrivia.


Os nós de sintaxe descrevem construções de sintaxe, ou seja, declarações, operadores, expressões etc. No C #, as construções de sintaxe representam uma classe do tipo SyntaxNode.


Tokens de sintaxe descrevem elementos como: identificadores, palavras-chave, caracteres especiais. Em C #, é um tipo de classe SyntaxToken.


O Syntaxtrivia descreve elementos que não serão compilados, como espaços, feeds de linha, comentários, diretivas de pré-processador. Em C #, é definido por uma classe do tipo SyntaxTrivia.


O modelo semântico representa informações sobre objetos e seus tipos. Graças a esta ferramenta, você pode realizar uma análise profunda e complexa. Em C #, é definido por uma classe do tipo SemanticModel.


Criando um analisador


Para criar um analisador estático, você precisa instalar o seguinte componente .NETCompilerPlatformSDK.


As principais funções que compõem qualquer analisador incluem:


  1. Registro de ações.
    Ações são alterações de código que o analisador deve iniciar para verificar se há violações no código. Quando o VisualStudio detecta alterações de código que correspondem à ação registrada, ele chama o método analisador registrado.
  2. Crie diagnósticos.
    Quando uma violação é detectada, o analisador cria um objeto de diagnóstico usado pelo VisualStudio para notificar o usuário sobre a violação.

Existem várias etapas para criar e testar um analisador:


  1. Crie uma solução.
  2. Registre o nome e a descrição do analisador.
  3. Avisos e recomendações do analisador de relatórios.
  4. Execute uma correção de código para aceitar as recomendações.
  5. Melhorando a análise com testes de unidade.

As ações são registradas em uma substituição do método DiagnosticAnalyzer.Initialize (AnalysisContext), em que AnalysisContext é o método no qual a pesquisa do objeto analisado é corrigida.


O analisador pode fornecer uma ou mais correções de código. Um patch de código identifica alterações que abordam o problema relatado. O usuário escolhe as alterações na interface do usuário (lâmpadas no editor) e o VisualStudio altera o código. O método RegisterCodeFixesAsync descreve como alterar o código.


Exemplo


Por exemplo, escreveremos o analisador de campos públicos. Esse aplicativo deve avisar o usuário sobre campos públicos e fornecer a capacidade de encapsular o campo com uma propriedade.


Aqui está o que você deve obter:


exemplo de trabalho


Vamos descobrir o que precisa ser feito para isso.


Primeiro você precisa criar uma solução.


tomada de decisão


Depois de criar a solução, vemos que já existem três projetos.


árvore de decisão


Precisamos de duas classes:


1) Class AnalyzerPublicFieldsAnalyzer, no qual especificamos os critérios para analisar o código para localizar campos públicos e uma descrição do aviso para o usuário.


Indicamos as seguintes propriedades:


 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); } } 

Depois disso, indicamos por quais critérios a análise de campos públicos será realizada.


 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); } } 

Obtemos um campo de um objeto do tipo IFieldSymbol, que possui propriedades para definir modificadores de campo, seu nome e localização. O que precisamos para o diagnóstico.


Resta inicializar o analisador especificando no método substituído


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

2) Agora passamos a alterar o código proposto pelo usuário com base na análise do código. Isso acontece na classe AnalyzerPublicFieldsCodeFixProvider.


Para fazer isso, indique o seguinte:


 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); } 

E determinamos a capacidade de encapsular o campo com uma propriedade no método 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; } 

Para fazer isso, crie um campo privado.


 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)); } 

Em seguida, crie uma propriedade pública que retorne e receba esse campo particular.


 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))); } 

Ao mesmo tempo, salvamos o tipo e o nome do campo de origem. O nome do campo é construído da seguinte forma "_name" e o nome da propriedade "Name".


Referências


  1. Fontes do GitHub
  2. O SDK da plataforma do compilador .NET

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


All Articles