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:

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:
- 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. - 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:
- Crie uma solução.
- Registre o nome e a descrição do analisador.
- Avisos e recomendações do analisador de relatórios.
- Execute uma correção de código para aceitar as recomendações.
- 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:

Vamos descobrir o que precisa ser feito para isso.
Primeiro você precisa criar uma solução.

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

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
- Fontes do GitHub
- O SDK da plataforma do compilador .NET