Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O SDK da plataforma de compilador .NET fornece as ferramentas necessárias para criar diagnósticos personalizados (analisadores), correções de código, refatoração de código e supressores de diagnóstico destinados a código C# ou Visual Basic. Um analisador contém código que reconhece violações da sua regra. Sua correção de código contém o código que corrige a violação. As regras que você implementa podem ser qualquer coisa, desde estrutura de código a estilo de codificação, convenções de nomenclatura e muito mais. A Plataforma de Compilador .NET fornece a estrutura para executar a análise à medida que os desenvolvedores estão escrevendo código e todos os recursos da interface do usuário do Visual Studio para corrigir o código: mostrando rabiscos no editor, preenchendo a Lista de Erros do Visual Studio, criando as sugestões de "lâmpada" e mostrando a visualização avançada das correções sugeridas.
Neste tutorial, você explorará a criação de um analisador e uma correção de código que o acompanha usando as APIs do Roslyn. Um analisador é uma maneira de realizar a análise do código-fonte e relatar um problema ao usuário. Opcionalmente, uma correção de código pode ser associada ao analisador para representar uma modificação no código-fonte do usuário. Este tutorial cria um analisador que localiza declarações de variáveis locais que poderiam ser declaradas usando o modificador, const mas não são. A correção de código que acompanha modifica essas declarações para adicionar o const modificador.
Pré-requisitos
- Visual Studio 2019 versão 16.8 ou posterior
Você precisará instalar o SDK da plataforma de compilador .NET por meio do instalador do Visual Studio:
Instruções de instalação - Visual Studio Installer
Há duas maneiras diferentes de localizar o SDK da plataforma de compilador .NET no instalador do Visual Studio:
Instalar usando o Visual Studio Installer - modo de exibição Cargas de trabalho
O SDK da plataforma de compilador .NET não é selecionado automaticamente como parte da carga de trabalho de desenvolvimento de extensão do Visual Studio. Você deve selecioná-lo como um componente opcional.
- Executar o instalador do Visual Studio
- Selecione Modificar
- Verifique a tarefa de desenvolvimento de extensões do Visual Studio.
- Abra o nó de desenvolvimento de extensões do Visual Studio na estrutura de resumo.
- Marque a caixa de seleção para .NET Compiler Platform SDK. Você vai encontrá-lo por último sob os componentes opcionais.
Opcionalmente, você também desejará que o editor DGML exiba gráficos no visualizador:
- Abra o nó Componentes individuais na árvore de resumo.
- Marque a caixa para o editor DGML
Instalar usando o Visual Studio Installer - separador Componentes Individuais
- Executar o instalador do Visual Studio
- Selecione Modificar
- Selecione a guia Componentes individuais
- Marque a caixa de seleção para .NET Compiler Platform SDK. Você o encontrará na parte superior na seção Compiladores, ferramentas de compilação e tempos de execução .
Opcionalmente, você também desejará que o editor DGML exiba gráficos no visualizador:
- Assinale a caixa do editor DGML. Você vai encontrá-lo na seção Ferramentas de código .
Existem várias etapas para criar e validar seu analisador:
- Crie a solução.
- Registre o nome e a descrição do analisador.
- Avisos e recomendações do analisador de relatórios.
- Implemente a correção de código para aceitar recomendações.
- Melhorar a análise através de testes unitários.
Criar a solução
- No Visual Studio, escolha Arquivo > Novo > Projeto... para exibir a caixa de diálogo Novo Projeto.
- Em Visual C# > Extensibility, escolha Analyzer with code fix (.NET Standard).
- Nomeie seu projeto como "MakeConst" e clique em OK.
Observação
Você pode obter um erro de compilação (MSB4062: A tarefa "CompareBuildTaskVersion" não pôde ser carregada"). Para corrigir isso, atualize os pacotes NuGet na solução com o Gerenciador de Pacotes NuGet ou use Update-Package na janela Console do Gerenciador de Pacotes.
Explore o modelo do analisador
O analisador com modelo de correção de código cria cinco projetos:
- MakeConst, que contém o analisador.
- MakeConst.CodeFixes, que contém a correção de código.
- MakeConst.Package, que é usado para produzir o pacote NuGet para o analisador e a correção de código.
- MakeConst.Test, que é um projeto de teste de unidade.
- MakeConst.Vsix, que é o projeto de inicialização padrão que inicia uma segunda instância do Visual Studio que carregou seu novo analisador. Pressione F5 para iniciar o projeto VSIX.
Observação
Os analisadores devem ter como alvo o .NET Standard 2.0 porque podem ser executados no ambiente .NET Core (compilações de linha de comando) e no ambiente .NET Framework (Visual Studio).
Sugestão
Quando você executa o analisador, você inicia uma segunda cópia do Visual Studio. Esta segunda cópia usa uma seção de registro diferente para armazenar configurações. Isso permite que você diferencie as configurações visuais nas duas cópias do Visual Studio. Você pode escolher um tema diferente para a execução experimental do Visual Studio. Além disso, não faça roaming das suas definições nem inicie sessão na sua conta do Visual Studio ao utilizar a versão experimental do Visual Studio. Isso mantém as configurações diferentes.
A colmeia inclui não só o analisador em desenvolvimento, mas também quaisquer analisadores anteriores abertos. Para redefinir o hive Roslyn, deve excluí-lo manualmente do %LocalAppData%\Microsoft\VisualStudio. O nome da pasta Roslyn hive terminará em Roslyn, por exemplo, 16.0_9ae182f9Roslyn. Observe que talvez seja necessário limpar a solução e reconstruí-la depois de excluir a colmeia.
Na segunda instância do Visual Studio que acabaste de iniciar, cria um novo projeto de Aplicação de Consola em C# (qualquer framework será compatível — os analisadores funcionam ao nível do código-fonte.) Ao passar o cursor sobre o token com um sublinhado ondulado, aparece o texto de aviso fornecido por um analisador.
O modelo cria um analisador que relata um aviso em cada declaração de tipo em que o nome do tipo contém letras minúsculas, conforme mostrado na figura a seguir:
O modelo também fornece uma correção de código que altera qualquer nome de tipo que contenha caracteres minúsculos para todas as letras maiúsculas. Pode clicar na lâmpada apresentada com o aviso para ver as alterações sugeridas. Aceitar as alterações sugeridas atualiza o nome do tipo e todas as referências a esse tipo na solução. Agora que você viu o analisador inicial em ação, feche a segunda instância do Visual Studio e retorne ao seu projeto de analisador.
Você não precisa iniciar uma segunda cópia do Visual Studio e criar um novo código para testar todas as alterações no analisador. O modelo também cria um projeto de teste de unidade para você. Este projeto contém dois testes.
TestMethod1 Mostra o formato típico de um teste que analisa o código sem acionar um diagnóstico.
TestMethod2 Mostra o formato de um teste que dispara um diagnóstico e, em seguida, aplica uma correção de código sugerida. À medida que você cria seu analisador e correção de código, você escreverá testes para diferentes estruturas de código para verificar seu trabalho. Os testes de unidade para analisadores são muito mais rápidos do que testá-los interativamente com o Visual Studio.
Sugestão
Os testes de unidade do analisador são uma ótima ferramenta quando você sabe quais construções de código devem e não devem acionar seu analisador. Carregar o seu analisador noutra cópia do Visual Studio é uma excelente ferramenta para explorar e encontrar construções que talvez ainda não tenha considerado.
Neste tutorial, você escreve um analisador que relata ao usuário todas as declarações de variáveis locais que podem ser convertidas em constantes locais. Por exemplo, considere o seguinte código:
int x = 0;
Console.WriteLine(x);
No código acima, x é atribuído um valor constante e nunca é modificado. Pode ser declarado usando o const modificador:
const int x = 0;
Console.WriteLine(x);
A análise para determinar se uma variável pode ser tornada constante está envolvida, exigindo análise sintática, análise constante da expressão do inicializador e análise de fluxo de dados para garantir que a variável nunca seja gravada. A plataforma de compilador .NET fornece APIs que facilitam a execução dessa análise.
Criar registros do analisador
O modelo cria a classe inicial DiagnosticAnalyzer , no arquivo MakeConstAnalyzer.cs . Este analisador inicial mostra duas propriedades importantes de cada analisador.
- Cada analisador de diagnóstico deve fornecer um
[DiagnosticAnalyzer]atributo que descreva a linguagem em que opera. - Cada analisador de diagnóstico deve derivar (direta ou indiretamente) da DiagnosticAnalyzer classe.
O modelo também mostra os recursos básicos que fazem parte de qualquer analisador:
- Registre ações. As ações representam alterações de código que devem acionar seu analisador para examinar o código em busca de violações. Quando o Visual Studio deteta edições de código que correspondem a uma ação registrada, ele chama o método registrado do analisador.
- Crie diagnósticos. Quando o analisador deteta uma violação, ele cria um objeto de diagnóstico que o Visual Studio usa para notificar o usuário sobre a violação.
Na substituição do método DiagnosticAnalyzer.Initialize(AnalysisContext), regista ações. Neste tutorial, você visitará nós de sintaxe procurando declarações locais e verá quais delas têm valores constantes. Se uma declaração puder ser constante, o analisador criará e gerará um relatório de diagnóstico.
O primeiro passo é atualizar as constantes de registo e o método Initialize para que essas constantes indiquem o seu analisador "Make Const". A maioria das constantes de cadeia de caracteres são definidas no arquivo de recurso de cadeia de caracteres. Você deve seguir essa prática para facilitar a localização. Abra o arquivo Resources.resx para o projeto do analisador MakeConst . Isso exibe o editor de recursos. Atualize os recursos de cadeia de caracteres da seguinte maneira:
- Mude
AnalyzerDescriptionpara "Variables that are not modified should be made constants.". - Mude
AnalyzerMessageFormatpara "Variable '{0}' can be made constant". - Mude
AnalyzerTitlepara "Variable can be made constant".
Quando terminar, o editor de recursos deve aparecer como mostrado na figura a seguir:
As alterações restantes estão no arquivo do analisador. Abra MakeConstAnalyzer.cs no Visual Studio. Altere a ação registrada de uma que atua em símbolos para uma que atua em sintaxe. No método MakeConstAnalyzerAnalyzer.Initialize, encontre a linha que regista a ação em símbolos:
context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);
Substitua-o pela seguinte linha:
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);
Após essa alteração, você pode excluir o AnalyzeSymbol método. Este analisador examina SyntaxKind.LocalDeclarationStatement, não SymbolKind.NamedType declarações. Observe que AnalyzeNode tem rabiscos vermelhos sob ele. O código que você acabou de adicionar faz referência a um AnalyzeNode método que não foi declarado. Declare esse método usando o seguinte código:
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}
Altere o Category para "Usage" no MakeConstAnalyzer.cs conforme mostrado no código a seguir:
private const string Category = "Usage";
Encontre declarações locais que possam ser const
É hora de escrever a primeira versão do AnalyzeNode método. Deve procurar uma única declaração local que poderia ser const, mas não é, como o seguinte código:
int x = 0;
Console.WriteLine(x);
O primeiro passo é encontrar declarações locais. Adicione o seguinte código ao AnalyzeNodeMakeConstAnalyzer.cs:
var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;
Esse elenco sempre é bem-sucedido porque seu analisador se registrou para alterações em declarações locais e apenas declarações locais. Nenhum outro tipo de nó dispara uma chamada para o seu AnalyzeNode método. Em seguida, verifique na declaração se há algum modificador const. Se os encontrar, devolva imediatamente. O código a seguir busca quaisquer const modificadores na declaração local:
// make sure the declaration isn't already const:
if (localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
return;
}
Finalmente, você precisa verificar se a variável pode ser const. Isso significa garantir que ele nunca seja atribuído depois de inicializado.
Você realizará algumas análises semânticas usando o SyntaxNodeAnalysisContext. Use o context argumento para determinar se a declaração de variável local pode ser feita const. A Microsoft.CodeAnalysis.SemanticModel representa todas as informações semânticas em um único arquivo de origem. Saiba mais no artigo que aborda modelos semânticos. Você usará o Microsoft.CodeAnalysis.SemanticModel para executar a análise de fluxo de dados na instrução de declaração local. Em seguida, use os resultados dessa análise de fluxo de dados para garantir que a variável local não seja escrita com um novo valor em nenhum outro lugar. Chame o membro da extensão GetDeclaredSymbol para recuperar o ILocalSymbol da variável e verifique que não está contido na coleção DataFlowAnalysis.WrittenOutside da análise do fluxo de dados. Adicione o seguinte código ao final do AnalyzeNode método:
// 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;
}
O código adicionado garante que a variável não seja modificada e, portanto, possa ser feita const. É hora de levantar o diagnóstico. Adicione o seguinte código como a última linha em AnalyzeNode:
context.ReportDiagnostic(Diagnostic.Create(Rule, context.Node.GetLocation(), localDeclaration.Declaration.Variables.First().Identifier.ValueText));
Você pode verificar seu progresso pressionando F5 para executar o analisador. Você pode carregar o aplicativo de console criado anteriormente e, em seguida, adicionar o seguinte código de teste:
int x = 0;
Console.WriteLine(x);
A lâmpada deve aparecer, e o seu analisador deve fornecer um diagnóstico. No entanto, dependendo da sua versão do Visual Studio, você verá:
- A lâmpada, que ainda utiliza a correção de código gerada pelo modelo, indicará que pode ser convertida para maiúsculas.
- Uma mensagem de banner na parte superior do editor dizendo que o 'MakeConstCodeFixProvider' encontrou um erro e foi desativado.'. Isso ocorre porque a ferramenta de correção do código ainda não foi atualizada e portanto espera encontrar elementos
TypeDeclarationSyntaxem vez de elementosLocalDeclarationStatementSyntax.
A próxima seção explica como escrever a correção de código.
Escreva a correção de código
Um analisador pode fornecer uma ou mais correções de código. Uma correção de código define uma edição que resolve o problema relatado. Para o analisador que você criou, você pode fornecer uma correção de código que insere a palavra-chave const:
- int x = 0;
+ const int x = 0;
Console.WriteLine(x);
O utilizador escolhe-o a partir da interface de sugestões no editor e o Visual Studio altera automaticamente o código.
Abra o arquivo CodeFixResources.resx e mude CodeFixTitle para "Make constant".
Abra o arquivo MakeConstCodeFixProvider.cs adicionado pelo modelo. Essa correção de código já está conectada à ID de diagnóstico produzida pelo analisador de diagnóstico, mas ainda não implementa a transformação de código correta.
Em seguida, exclua o MakeUppercaseAsync método. Já não se aplica.
Todos os provedores de correção de código derivam de CodeFixProvider. Todos eles substituem CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) para relatar correções de código disponíveis. No RegisterCodeFixesAsync, altere o tipo de nó ancestral que você está procurando para um LocalDeclarationStatementSyntax para corresponder ao diagnóstico:
var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();
Em seguida, altere a última linha para registrar uma correção de código. Sua correção criará um novo documento que resulta da adição do const modificador a uma declaração existente:
// 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);
Você notará rabiscos vermelhos no código que você acabou de adicionar no símbolo MakeConstAsync. Adicione uma declaração para MakeConstAsync como no código a seguir:
private static async Task<Document> MakeConstAsync(Document document,
LocalDeclarationStatementSyntax localDeclaration,
CancellationToken cancellationToken)
{
}
Seu novo MakeConstAsync método transformará a Document representação do arquivo de origem do usuário em um novo Document que agora contém uma const declaração.
Você cria um novo token de palavra-chave const para inserir no início da declaração. Tenha cuidado para primeiro remover qualquer curiosidade principal do primeiro token da declaração e anexá-la ao const token. Adicione o seguinte código ao método 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));
Em seguida, adicione o const token à declaração usando o seguinte código:
// 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);
Em seguida, formate a nova declaração para corresponder às regras de formatação em C#. Formatar as alterações para corresponder ao código existente cria uma experiência melhor. Adicione a seguinte instrução imediatamente após o código existente:
// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);
Um novo namespace é necessário para esse código. Adicione a seguinte using diretiva à parte superior do arquivo:
using Microsoft.CodeAnalysis.Formatting;
O passo final é fazer a sua edição. Existem três passos para este processo:
- Obtenha um identificador para o documento existente.
- Crie um novo documento substituindo a declaração existente pela nova declaração.
- Retornar o novo documento.
Adicione o seguinte código ao final do MakeConstAsync método:
// 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);
A correção do seu código está pronta para você testar. Pressione F5 para executar o projeto do analisador em uma segunda instância do Visual Studio. Na segunda instância do Visual Studio, crie um novo projeto C# Console Application e adicione algumas declarações de variáveis locais inicializadas com valores constantes ao método Main. Você verá que eles são relatados como avisos conforme abaixo.
Fizeste muitos progressos. Há hesitações sob as declarações que podem ser feitas const. Mas ainda há trabalho a fazer. Isso funciona bem se você adicionar const às declarações começando com i, então j e finalmente k. Mas, se adicionar o modificador const numa ordem diferente, começando com k, o seu analisador cria erros: k não pode ser declarado const, a menos que i e j já sejam ambos const. Você precisa fazer mais análises para garantir que lida com as diferentes maneiras pelas quais as variáveis podem ser declaradas e inicializadas.
Testes de unidade de compilação
Seu analisador e correção de código funcionam em um caso simples de uma única declaração que pode ser feita const. Existem inúmeras declarações possíveis em que esta aplicação comete erros. Você abordará esses casos trabalhando com a biblioteca de teste de unidade escrita pelo modelo. É muito mais rápido do que abrir repetidamente uma segunda cópia do Visual Studio.
Abra o arquivo MakeConstUnitTests.cs no projeto de teste de unidade. O modelo criou dois testes que seguem os dois padrões comuns para um analisador e um teste de unidade de correção de código.
TestMethod1 mostra o padrão de um teste que garante que o analisador não relata um diagnóstico quando não deveria.
TestMethod2 mostra o padrão para relatar um diagnóstico e executar a correção de código.
O modelo usa pacotes Microsoft.CodeAnalysis.Testing para testes de unidade.
Sugestão
A biblioteca de teste suporta uma sintaxe de marcação especial, incluindo o seguinte:
-
[|text|]: indica que um diagnóstico é apresentado paratext. Por padrão, este formulário pode ser utilizado apenas para testar analisadores, com exatamente umDiagnosticDescriptorfornecido porDiagnosticAnalyzer.SupportedDiagnostics. -
{|ExpectedDiagnosticId:text|}: indica que um diagnóstico com IdExpectedDiagnosticIdé reportado paratext.
Substitua os testes do modelo na classe MakeConstUnitTest pelo seguinte método de teste:
[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);
}
}
");
}
Execute este teste para se certificar de que é aprovado. No Visual Studio, abra o Gerenciador de Testes selecionando Test>Windows>Test Explorer. Em seguida, selecione Executar tudo.
Criar testes para declarações válidas
Como regra geral, os analisadores devem sair o mais rápido possível, fazendo um trabalho mínimo. O Visual Studio chama analisadores registrados à medida que o usuário edita o código. A capacidade de resposta é um requisito fundamental. Há vários casos de teste para código que não devem aumentar seu diagnóstico. Seu analisador já lida com vários desses testes. Adicione os seguintes métodos de teste para representar esses casos:
[TestMethod]
public async Task VariableIsAssigned_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i = 0;
Console.WriteLine(i++);
}
}
");
}
[TestMethod]
public async Task VariableIsAlreadyConst_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
const int i = 0;
Console.WriteLine(i);
}
}
");
}
[TestMethod]
public async Task NoInitializer_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int i;
i = 0;
Console.WriteLine(i);
}
}
");
}
Esses testes são bem-sucedidos porque o analisador já lida com estas condições:
- As variáveis atribuídas após a inicialização são detetadas pela análise de fluxo de dados.
- As declarações já presentes em
constsão filtradas através da verificação da palavra-chaveconst. - As declarações sem inicializador são tratadas pela análise de fluxo de dados que deteta atribuições fora da declaração.
Em seguida, adicione métodos de teste para condições que você ainda não manipulou:
Declarações em que o inicializador não é uma constante, porque não podem ser constantes em tempo de compilação:
[TestMethod] public async Task InitializerIsNotConstant_NoDiagnostic() { await VerifyCS.VerifyAnalyzerAsync(@" using System; class Program { static void Main() { int i = DateTime.Now.DayOfYear; Console.WriteLine(i); } } "); }
Pode ser ainda mais complicado porque o C# permite várias declarações como uma instrução. Considere a seguinte constante de cadeia de caracteres de caso de teste:
[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);
}
}
");
}
A variável i pode ser tornada constante, mas a variável j não. Por conseguinte, esta declaração não pode ser feita como uma declaração const.
Execute os testes novamente e você verá esses dois últimos casos de teste falharem.
Atualize o analisador para ignorar as declarações corretas
Você precisa de alguns aprimoramentos no método do AnalyzeNode analisador para filtrar o código que corresponde a essas condições. Todas elas são condições relacionadas, portanto, alterações semelhantes corrigirão todas essas condições. Faça as seguintes alterações em AnalyzeNode:
- Sua análise semântica examinou uma única declaração de variável. Esse código precisa estar em um
foreachloop que examina todas as variáveis declaradas na mesma instrução. - Cada variável declarada precisa ter um inicializador.
- O inicializador de cada variável declarada deve ser uma constante de tempo de compilação.
No seu AnalyzeNode método, substitua a análise semântica original:
// 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;
}
com o seguinte trecho de código:
// 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;
}
}
O primeiro foreach loop examina cada declaração de variável usando análise sintática. A primeira verificação garante que a variável tenha um inicializador. A segunda verificação garante que o inicializador é uma constante. O segundo loop tem a análise semântica original. As verificações semânticas estão em um loop separado porque tem um impacto maior no desempenho. Execute novamente os seus testes, e deve ver que todos passam.
Dê os retoques finais
Está quase concluído. Há mais algumas condições para o seu analisador gerir. O Visual Studio chama analisadores enquanto o usuário está escrevendo código. Muitas vezes, o analisador é chamado para um código que não é compilado. O método do analisador diagnóstico AnalyzeNode não verifica a conversibilidade do valor constante para o tipo de variável. Assim, a implementação atual converterá facilmente uma declaração incorreta, como int i = "abc", para uma constante local. Adicione um método de teste para este caso:
[TestMethod]
public async Task DeclarationIsInvalid_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
int x = {|CS0029:""abc""|};
}
}
");
}
Além disso, os tipos de referência não são tratados corretamente. O único valor constante permitido para um tipo de referência é null, exceto no caso de System.String, que permite literais de string. Por outras palavras, const string s = "abc" é legal, mas const object s = "abc" não é. Este trecho de código verifica essa condição:
[TestMethod]
public async Task DeclarationIsNotString_NoDiagnostic()
{
await VerifyCS.VerifyAnalyzerAsync(@"
using System;
class Program
{
static void Main()
{
object s = ""abc"";
}
}
");
}
Para ser minucioso, você precisa adicionar outro teste para certificar-se de que você pode criar uma declaração constante para uma cadeia de caracteres. O trecho a seguir define o código que gera o diagnóstico e o código após a correção ter sido aplicada:
[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"";
}
}
");
}
Finalmente, se uma variável é declarada com a palavra-chave var , a correção de código faz a coisa errada e gera uma const var declaração, que não é suportada pela linguagem C#. Para corrigir esse bug, a correção de código deve substituir a var palavra-chave com o nome do tipo inferido:
[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"";
}
}
");
}
Felizmente, todos os bugs acima podem ser resolvidos usando as mesmas técnicas que você acabou de aprender.
Para corrigir o primeiro bug, primeiro abra MakeConstAnalyzer.cs e localize o loop foreach onde cada um dos inicializadores da declaração local é verificado para garantir que eles sejam atribuídos com valores constantes. Imediatamente antes do primeiro loop foreach, ligue context.SemanticModel.GetTypeInfo() para recuperar informações detalhadas sobre o tipo declarado da declaração local:
TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;
Em seguida, dentro do loop foreach , verifique cada inicializador para se certificar de que é conversível para o tipo de variável. Adicione a seguinte verificação após assegurar que o inicializador é uma 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;
}
A próxima alteração baseia-se na última. Antes da chave de fechamento do primeiro loop foreach, adicione o código a seguir para verificar o tipo da declaração local quando a constante for uma string ou 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;
}
Você deve escrever um pouco mais de código em seu provedor de correção de código para substituir a var palavra-chave com o nome de tipo correto. Volte para MakeConstCodeFixProvider.cs. O código que você adicionará executa as seguintes etapas:
- Verifique se a declaração é uma
vardeclaração e se é: - Crie um novo tipo para o tipo inferido.
- Verifique se a declaração de tipo não é um alias. Em caso afirmativo, é legal declarar
const var. - Certifique-se de que
varnão é um nome de tipo neste programa. (Se sim,const varé legal). - Simplifique o nome completo do tipo
Isso soa como um monte de código. Não é. Substitua a linha que declara e inicializa newLocal pelo código a seguir. Ele vai imediatamente após a inicialização 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);
Você precisará adicionar uma using diretiva para usar o Simplifier tipo:
using Microsoft.CodeAnalysis.Simplification;
Execute seus testes, e todos eles devem passar. Felicite-se ao executar o seu analisador concluído. Pressione Ctrl+F5 para executar o projeto do analisador em uma segunda instância do Visual Studio com a extensão Roslyn Preview carregada.
- Na segunda instância do Visual Studio, crie um novo projeto C# Console Application e adicione
int x = "abc";ao método Main. Graças à primeira correção de bug, nenhum aviso deve ser relatado para essa declaração de variável local (embora haja um erro do compilador conforme o esperado). - Em seguida, adicione
object s = "abc";ao método Main. Devido à segunda correção de bug, não deve ser relatado nenhum aviso. - Finalmente, adicione outra variável local que usa a
varpalavra-chave. Você verá que um aviso é relatado, e uma sugestão aparece abaixo, à esquerda. - Mova o cursor do editor sobre o sublinhado ondulado e pressione Ctrl+.. para exibir a correção de código sugerida. Ao selecionar a sua correção de código, observe que a palavra-chave
varagora é manipulada corretamente.
Finalmente, adicione o seguinte código:
int i = 2;
int j = 32;
int k = i + j;
Após essas alterações, você obtém rabiscos vermelhos apenas nas duas primeiras variáveis. Adicione const a ambos i e j, e você receberá um novo aviso porque k agora pode ser const.
Parabéns! Você criou sua primeira extensão .NET Compiler Platform que executa análise de código instantânea para detetar um problema e fornece uma correção rápida para corrigi-lo. Ao longo do caminho, você aprendeu muitas das APIs de código que fazem parte do .NET Compiler Platform SDK (Roslyn APIs). Você pode verificar seu trabalho em relação à amostra concluída em nosso repositório GitHub de amostras.