Tutorial: escrever seu primeiro analisador e correção de código

O SDK do .NET Compiler Platform 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ódigos C# ou Visual Basic. Um analisador contém código que reconhece violações às suas regras. Sua correção de código contém o código que corrige a violação. As regras que você implementar podem ser qualquer coisa, incluindo estrutura do código, estilo de codificação, convenções de nomenclatura e muito mais. O .NET Compiler Platform fornece a estrutura para executar análise conforme os desenvolvedores escrevem o código, bem como todos os recursos de interface do usuário do Visual Studio para corrigir o código: mostrar rabiscos no editor, popular a Lista de Erros do Visual Studio, criar as sugestões da "lâmpada" e mostrar 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 executar a análise de código-fonte e relatar um problema para o 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 as declarações de variável local que poderiam ser declaradas usando o modificador const, mas não o são. A correção de código anexa modifica essas declarações para adicionar o modificador const.

Pré-requisitos

Será necessário instalar o SDK do .NET Compiler Platform por meio do Instalador do Visual Studio:

Instruções de instalação – Instalador do Visual Studio

Há duas maneiras diferentes de encontrar o SDK da .NET Compiler Platform no Instalador do Visual Studio:

Instalar usando o Instalador do Visual Studio – exibição de cargas de trabalho

O SDK da .NET Compiler Platform não é selecionado automaticamente como parte da carga de trabalho de desenvolvimento da extensão do Visual Studio. É necessário selecioná-lo como um componente opcional.

  1. Execute o Instalador do Visual Studio
  2. Escolha Mudar
  3. Marque a carga de trabalho de Desenvolvimento de extensão do Visual Studio.
  4. Abra o nó Desenvolvimento de extensão do Visual Studio na árvore de resumo.
  5. Marque a caixa do SDK da .NET Compiler Platform. Você a encontrará por último nos componentes opcionais.

Opcionalmente, você também poderá fazer o Editor DGML exibir gráficos no visualizador:

  1. Abra o nó Componentes individuais na árvore de resumo.
  2. Marque a caixa do Editor DGML

Instalar usando o Instalador do Visual Studio – guia Componentes individuais

  1. Execute o Instalador do Visual Studio
  2. Escolha Mudar
  3. Selecione a guia Componentes individuais
  4. Marque a caixa do SDK da .NET Compiler Platform. Você a encontrará na parte superior, na seção Compiladores, ferramentas de compilação e runtimes.

Opcionalmente, você também poderá fazer o Editor DGML exibir gráficos no visualizador:

  1. Marque a caixa do Editor DGML. Você a encontrará na seção Ferramentas de código.

Há várias etapas para criar e validar o analisador:

  1. Crie a solução.
  2. Registre o nome e a descrição do analisador.
  3. Relate os avisos e recomendações do analisador.
  4. Implemente a correção de código para aceitar as recomendações.
  5. Melhore a análise por meio de testes de unidade.

Criar a solução

  • No Visual Studio, escolha Arquivo > Novo > Projeto... para exibir o diálogo Novo Projeto.
  • Em Visual C# > Extensibilidade, escolha Analisador com correção de código (.NET Standard).
  • Nomeie seu projeto como "MakeConst" e clique em OK.

Observação

Você pode receber um erro de compilação (MSB4062: não foi possível carregar a tarefa "CompareBuildTaskVersion"). Para corrigir isso, atualize os pacotes NuGet na solução com o Gerenciador de Pacotes NuGet ou use Update-Package na janela do console do Gerenciador de Pacotes.

Explorar o modelo do analisador

O analisador com o 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 carrega uma segunda instância do Visual Studio com seu novo analisador. Pressione F5 para iniciar o projeto VSIX.

Observação

Os analisadores devem ter como destino o .NET Standard 2.0 porque podem ser executados nos ambientes do .NET Core (builds de linha de comando) e do .NET Framework (Visual Studio).

Dica

Quando você executa seu analisador, você pode iniciar uma segunda cópia do Visual Studio. Essa segunda cópia usa um hive do Registro diferente para armazenar configurações. Isso lhe permite diferenciar as configurações visuais em 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 use perfil móvel de suas configurações nem faça logon na conta do Visual Studio usando a execução experimental do Visual Studio. Isso mantém as diferenças entre as configurações.

O hive inclui não apenas o analisador em desenvolvimento, mas também todos os analisadores anteriores abertos. Para redefinir o hive da Roslyn, você precisa excluí-lo manualmente de %LocalAppData%\Microsoft\VisualStudio. O nome da pasta do hive da Roslyn terminará em Roslyn, por exemplo, 16.0_9ae182f9Roslyn. Observe que talvez seja necessário limpar a solução e compilar novamente após excluir o hive.

Na segunda instância do Visual Studio que você acabou de iniciar, crie um projeto de Aplicativo de Console do C# (qualquer estrutura de destino funcionará – os analisadores funcionam no nível de origem). Passe o mouse sobre o token com um sublinhado ondulado e o texto de aviso fornecido por um analisador será exibido.

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:

Analisador de aviso de relatórios

O modelo também fornece uma correção de código que altera qualquer nome de tipo que contenha caracteres de letras minúsculas, deixando-o com todas as letras maiúsculas. Você pode clicar na lâmpada exibida com o aviso para ver as alterações sugeridas. Aceitar as alterações sugeridas atualiza o nome do tipo e todas as referências para esse tipo na solução. Agora que você já viu o analisador inicial em ação, feche a segunda instância do Visual Studio e retorne ao projeto do analisador.

Você não precisa iniciar uma segunda cópia do Visual Studio e criar um novo código para testar cada alteração em seu analisador. O modelo também cria um projeto de teste de unidade para você. Esse projeto contém dois testes. TestMethod1 mostra o formato típico de um teste que analisa o código sem disparar 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. Conforme você cria o analisador e a correção de código, você escreve testes para estruturas de código diferentes para verificar seu trabalho. Testes de unidade para os analisadores são muito mais rápidos do que testá-los de forma interativa com o Visual Studio.

Dica

Testes de unidade de analisador são uma excelente ferramenta quando você sabe quais constructos de código devem e não devem disparar seu analisador. Carregar o analisador em outra cópia do Visual Studio é uma excelente ferramenta para explorar e encontrar constructos nos quais você talvez não tenha pensado ainda.

Neste tutorial, você grava um analisador que relata ao usuário qualquer declaração de variável local que possa ser convertida em constante local. Por exemplo, considere o seguinte código:

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

No código acima, um valor constante é atribuído a x, que nunca é modificado. Ele pode ser declarado usando o modificador const:

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 de inicializador e também análise de fluxo de dados, para garantir que nunca ocorram gravações na variável. O .NET Compiler Platform fornece APIs que facilitam essa análise.

Criar registros do analisador

O modelo cria a classe inicial DiagnosticAnalyzer, no arquivo MakeConstAnalyzer.cs. Esse analisador inicial mostra duas propriedades importantes de cada analisador.

  • Cada analisador de diagnóstico deve fornecer um atributo [DiagnosticAnalyzer] que descreve a linguagem em que opera.
  • Cada analisador de diagnóstico deve derivar (direta ou indiretamente) da classe DiagnosticAnalyzer.

O modelo também mostra os recursos básicos que fazem parte de qualquer analisador:

  1. Registrar ações. As ações representam alterações de código que devem disparar o analisador para examinar se há violações de código. Quando o Visual Studio detecta as edições de código que correspondem a uma ação registrada, ele chama o método registrado do analisador.
  2. Criar diagnósticos. Quando o analisador detecta uma violação, ele cria um objeto de diagnóstico que o Visual Studio usa para notificar o usuário sobre a violação.

Registrar ações na substituição do método DiagnosticAnalyzer.Initialize(AnalysisContext). Neste tutorial, você visitará nós de sintaxe em busca de declarações locais e verá quais delas têm valores constantes. Se houver possibilidade de uma declaração ser constante, seu analisador criará e relatará um diagnóstico.

A primeira etapa é atualizar as constantes de registro e o método Initialize, de modo que essas constantes indiquem seu analisador "Make Const". A maioria das constantes de cadeia de caracteres é definida no arquivo de recurso de cadeia de caracteres. Você deve seguir essa prática para uma localização mais fácil. 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:

  • Altere AnalyzerDescription para "Variables that are not modified should be made constants.".
  • Altere AnalyzerMessageFormat para "Variable '{0}' can be made constant".
  • Altere AnalyzerTitle para "Variable can be made constant".

Quando você terminar, o editor de recursos deverá aparecer conforme mostrado na seguinte figura:

Atualizar recursos de cadeia de caracteres

As alterações restantes estão no arquivo do analisador. Abra MakeConstAnalyzer.cs no Visual Studio. Altere a ação registrada de uma que age em símbolos para uma que age sobre a sintaxe. No método MakeConstAnalyzerAnalyzer.Initialize, localize a linha que registra a ação em símbolos:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Substitua-a com a seguinte linha:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Após essa alteração, você poderá excluir o método AnalyzeSymbol. Este analisador examina SyntaxKind.LocalDeclarationStatement, e não instruções SymbolKind.NamedType. Observe que AnalyzeNode tem rabiscos vermelhos sob ele. O código apenas que você acaba de adicionar referencia um método AnalyzeNode que não foi declarado. Declare esse método usando o seguinte código:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Altere Category para "Usage" em MakeConstAnalyzer.cs, conforme mostrado no seguinte código:

private const string Category = "Usage";

Localize as declarações locais que podem ser constantes

É hora de escrever a primeira versão do método AnalyzeNode. Ele deve procurar uma única declaração local que poderia ser const mas não é, algo semelhante ao seguinte código:

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

A primeira etapa é encontrar declarações locais. Adicione o seguinte código a AnalyzeNode em MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Essa conversão sempre terá êxito porque seu analisador fez o registro para alterações unicamente a declarações locais. Nenhum outro tipo de nó dispara uma chamada para seu método AnalyzeNode. Em seguida, verifique a declaração para quaisquer modificadores const. Se você encontrá-los, retorne imediatamente. O código a seguir procura por quaisquer modificadores const na declaração local:

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

Por fim, você precisa verificar que a variável pode ser const. Isso significa assegurar que ela nunca seja atribuída após ser inicializada.

Você executará alguma análise semântica usando o SyntaxNodeAnalysisContext. Você usa o argumento context para determinar se a declaração de variável local pode ser tornada const. Um Microsoft.CodeAnalysis.SemanticModel representa todas as informações semânticas em apenas um arquivo de origem. Você pode aprender mais no artigo que aborda modelos semânticos. Você usará o Microsoft.CodeAnalysis.SemanticModel para realizar a análise de fluxo de dados na instrução de declaração local. Em seguida, você usa 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 método de extensão GetDeclaredSymbol para recuperar o ILocalSymbol para a variável e verifique se ele não está contido na coleção DataFlowAnalysis.WrittenOutside da análise de fluxo de dados. Adicione o seguinte código ao final do método 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;
}

O código recém-adicionado garante que a variável não seja modificada e pode, portanto, ser tornada const. É hora de gerar o diagnóstico. Adicione o código a seguir como a última linha em AnalyzeNode:

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

Você pode verificar seu andamento pressionando F5 para executar o analisador. Você pode carregar o aplicativo de console que você criou anteriormente e, em seguida, adicionar o seguinte código de teste:

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

A lâmpada deve aparecer e o analisador deve relatar um diagnóstico. No entanto, dependendo da sua versão do Visual Studio, você verá:

  • A lâmpada, que ainda usa a correção de código gerada por modelo e dirá a você que ela pode ser colocada em letras maiúsculas.
  • Uma mensagem na barra de notificações, na parte superior do editor, informando que o "MakeConstCodeFixProvider" encontrou um erro e foi desabilitado. Isso ocorre porque o provedor de correção de código ainda não foi alterado e espera encontrar TypeDeclarationSyntax elementos em vez de LocalDeclarationStatementSyntax.

A próxima seção explica como escrever a correção de código.

Escrever 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 usuário escolhe-a da lâmpada da interface do usuário no editor e do Visual Studio altera o código.

Abra o arquivo CodeFixResources.resx e altere 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 ela ainda não implementa a transformação de código correta.

Em seguida, exclua o método MakeUppercaseAsync. Ele não se aplica mais.

Todos os provedores de correção de código derivam de CodeFixProvider. Todos eles substituem CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) para relatar as correções de código disponíveis. Em RegisterCodeFixesAsync, altere o tipo de nó ancestral pelo qual você está pesquisando 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. A correção criará um novo documento resultante da adição do modificador const para 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ê observará rabiscos vermelhos no código que você acabou de adicionar no símbolo MakeConstAsync. Adicione uma declaração para MakeConstAsync semelhante ao seguinte código:

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

Seu novo método MakeConstAsync transformará o Document que representa o arquivo de origem do usuário em um novo Document que agora contém uma declaração const.

Você cria um novo token de palavra-chave const a ser inserido no início da instrução de declaração. Tenha cuidado para remover qualquer desafio à esquerda do primeiro token de instrução de declaração e anexe-o ao token const. 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 token const à 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 de acordo com regras de formatação de C#. Formatar de suas alterações para corresponderem ao código existente cria uma experiência melhor. Adicione a instrução a seguir 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 diretiva using para a parte superior do arquivo:

using Microsoft.CodeAnalysis.Formatting;

A etapa final é fazer a edição. Há três etapas para esse processo:

  1. Obter um identificador para o documento existente.
  2. Criar um novo documento, substituindo a declaração existente pela nova declaração.
  3. Retornar o novo documento.

Adicione o seguinte código ao final do método 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);

A correção de código está pronta para ser experimentada. 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 de Aplicativo de Console em C# e adicione algumas declarações de variável local inicializadas com valores constantes para o método Main. Você verá que elas são relatadas como avisos, conforme mostrado a seguir.

Pode fazer avisos constantes

Você fez muito progresso. Há rabiscos sob as declarações que podem ser tornados const. Mas ainda há trabalho a fazer. Isso funciona bem se você adicionar const às declarações começando com i, depois j e, por fim, k. Mas se você adicionar o modificador const em uma ordem diferente, começando com k, seu analisador criará erros: k não pode ser declarado como const, a menos que i e j já sejam ambos const. Você tem que fazer mais análise para assegurar que lida com as diferentes maneiras em que variáveis podem ser declaradas e inicializadas.

Criar testes de unidade

Seu analisador e correção de código trabalham em um caso simples de uma única declaração que pode ser tornada const. Há várias instruções de declaração possíveis em que essa implementação comete erros. Você tratará desses casos trabalhando com a biblioteca de teste de unidade gravada pelo modelo. Isso é 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 o teste de unidade de correção de código. TestMethod1 mostra o padrão para um teste que garante que o analisador não relata um diagnóstico quando não deve fazê-lo. 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.

Dica

A biblioteca de testes dá suporte a uma sintaxe de marcação especial, incluindo o seguinte:

  • [|text|]: indica que um diagnóstico é relatado para text. Por padrão, esse formulário só pode ser usado para testar analisadores com exatamente um DiagnosticDescriptor fornecido por DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: indica que um diagnóstico com IdExpectedDiagnosticId é relatado para text.

Substitua os testes de 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 garantir que ele passa. No Visual Studio, abra o Gerenciador de Testes selecionando Teste>Windows>Gerenciador de Testes. Então selecione Executar tudo.

Criar testes para declarações válidas

Como regra geral, os analisadores devem sair assim que possível, fazendo o mínimo de trabalho. O Visual Studio chama analisadores registrados conforme o usuário edita o código. A capacidade de resposta é um requisito fundamental. Há vários casos de teste para o código que não deverão gerar o diagnóstico. O analisador já gerencia um desses testes, o caso em que uma variável é atribuída após ser inicializada. Adicione o seguinte método de teste para representar esse caso:

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

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

Esse teste é aprovado também. Em seguida, adicione métodos de teste para condições que você ainda não gerenciou:

  • Declarações que já são const, porque elas já são const:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Declarações que não têm nenhum inicializador, porque não há nenhum valor a ser usado:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Declarações em que o inicializador não é uma constante, porque elas não podem ser constantes de 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);
        }
    }
    ");
            }
    

Isso 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 o mesmo não se aplica à variável j. Portanto, essa instrução não pode ser tornada uma declaração const.

Execute os testes novamente e você verá esses novos casos de teste falharem.

Atualize seu analisador para ignorar as declarações corretas

Você precisa de algumas melhorias no método AnalyzeNode do analisador para filtrar o código que corresponde a essas condições. Elas são todas condições relacionadas, portanto, alterações semelhantes corrigirão todas essas condições. Faça as alterações a seguir em AnalyzeNode:

  • A análise semântica examinou uma única declaração de variável. Esse código deve estar em um loop de foreach 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 precisa ser uma constante de tempo de compilação.

No seu método AnalyzeNode, 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 snippet de código a seguir:

// 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 loop de foreach examina cada declaração de variável usando a análise sintática. A primeira verificação garante que a variável tenha um inicializador. A segunda verificação garante que o inicializador seja uma constante. O segundo loop tem a análise semântica original. As verificações semânticas estão em um loop separado porque ele tem um impacto maior no desempenho. Execute os testes novamente e você deverá ver todos eles serem aprovados.

Adicionar o final polonês

Você está quase lá. Há mais algumas condições com as quais o seu analisador deve lidar. Enquanto o usuário está escrevendo código, o Visual Studio chama os analisadores. Muitas vezes o analisador será chamado para código que não é compilado. O método AnalyzeNode do analisador de diagnóstico não verifica para ver se o valor da constante é conversível para o tipo de variável. Assim, a implementação atual converterá facilmente uma declaração incorreta, tal como int i = "abc", em 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 de constante permitido para um tipo de referência é null, exceto no caso de System.String, que permite literais de cadeia de caracteres. Em outras palavras, const string s = "abc" é legal, mas const object s = "abc" não é. Este snippet 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 criterioso, você precisará adicionar outro teste para verificar se pode criar uma declaração de constante para uma cadeia de caracteres. O snippet de código a seguir define o código que gera o diagnóstico e o código após a aplicação da correção:

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

Por fim, se uma variável é declarada com a palavra-chave var, a correção de código faz a coisa errada e gera uma declaração const var, que não é compatível com a linguagem C#. Para corrigir esse bug, a correção de código deve substituir a palavra-chave var pelo 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 erros acima podem ser resolvidos usando as mesmas técnicas que você acabou de aprender.

Para corrigir o primeiro bug, primeiro abra DiagnosticAnalyzer.cs e localize o loop foreach em que cada um dos inicializadores de declaração local é verificado para garantir que valores constantes sejam atribuídos a eles. Imediatamente antes do primeiro loop foreach, chame 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 garantir que ele pode ser convertido no tipo de variável. Adicione a seguinte verificação depois de garantir 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 é realizada com base na última. Antes da chave de fechamento do primeiro loop foreach, adicione o código a seguir para verificar o tipo da declaração de local quando a constante é uma cadeia de caracteres ou valor nulo.

// 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ê precisa escrever um pouco mais de código no seu provedor de correção de código para substituir a palavra-chave var pelo nome do tipo correto. Retorne para MakeConstCodeFixProvider.cs. O código que você adicionará realizará as seguintes etapas:

  • Verifique se a declaração é uma declaração var e se afirmativo:
  • Crie um novo tipo para o tipo inferido.
  • Certifique-se de que a declaração de tipo não é um alias. Em caso afirmativo, é legal declarar const var.
  • Certifique-se de que var não é um nome de tipo neste programa. (Em caso afirmativo, const var é legal).
  • Simplificar o nome completo do tipo

Isso soa como muito código. Mas não é. Substitua a linha que declara e inicializa newLocal com o código a seguir. Ele é colocado 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 diretiva using para usar o tipo Simplifier:

using Microsoft.CodeAnalysis.Simplification;

Execute seus testes, que devem todos ser aprovados. Dê parabéns a si mesmo, executando 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 de versão prévia da Roslyn carregada.

  • Na segunda instância do Visual Studio, crie um novo projeto de Aplicativo de Console de C# e adicione int x = "abc"; ao método Main. Graças à primeira correção de bug, nenhum aviso deve ser relatado para esta declaração de variável local (embora haja um erro do compilador, conforme esperado).
  • Em seguida, adicione object s = "abc"; ao método Main. Devido à segunda correção de bug, nenhum aviso deve ser relatado.
  • Por fim, adicione outra variável local que usa a palavra-chave var. Você verá que um aviso é relatado e uma sugestão é exibida abaixo e a 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 correção de código, observe que a palavra-chave var agora é gerenciada corretamente.

Por fim, adicione o seguinte código:

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

Após essas alterações, você obtém linhas onduladas vermelhas apenas nas duas primeiras variáveis. Adicione const para ambos i e j, e você receberá um novo aviso em k porque ele agora pode ser const.

Parabéns! Você criou sua primeira extensão do .NET Compiler Platform que executa análise de código com o sistema em funcionamento para detectar 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 SDK do .NET Compiler Platform (APIs do Roslyn). Você pode verificar seu trabalho comparando-o à amostra concluída em nosso repositório GitHub de exemplos.

Outros recursos