Compartilhar via


Tutorial: Escreva seu primeiro analisador e conserte o 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 direcionados ao código C# ou Visual Basic. Um analisador contém código que reconhece violações de 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 até estilo de codificação até 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 roslyn. Um analisador é uma maneira de executar 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 podem ser declaradas usando o const modificador, mas não são. A correção de código que acompanha modifica essas declarações para adicionar o const modificador.

Pré-requisitos

Você precisará 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 do .NET Compiler Platform no Instalador do Visual Studio:

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

O SDK do .NET Compiler Platform 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.

  1. Executar o Instalador do Visual Studio
  2. Escolha Mudar
  3. Verifique 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ê o encontrará por último nos componentes opcionais.

Opcionalmente, você também desejará que o editor DGML exiba grafos no visualizador:

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

Instalar usando a guia Instalador do Visual Studio – Componentes individuais

  1. Executar 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ê o encontrará na parte superior na seção Compiladores, ferramentas de build e runtimes .

Opcionalmente, você também desejará que o editor DGML exiba grafos no visualizador:

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

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

  1. Crie a solução.
  2. Registre o nome e a descrição do analisador.
  3. Avisos e recomendações do analisador de relatório.
  4. Implemente a correção de código para aceitar recomendações.
  5. Aprimore a análise por meio 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 Extensibilidade do Visual C#>, 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: a tarefa "CompareBuildTaskVersion" não pôde ser carregada". Para corrigir isso, atualize os pacotes NuGet na solução com o Gerenciador de Pacotes Do NuGet ou use Update-Package na janela 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 e inicia uma segunda instância do Visual Studio que tenha carregado 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 no ambiente do .NET Core (builds de linha de comando) e no ambiente do .NET Framework (Visual Studio).

Dica

Ao executar o analisador, você inicia uma segunda cópia do Visual Studio. Esta segunda cópia usa um hive de registro diferente para armazenar as 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 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 configurações diferentes.

O hive inclui não apenas o analisador em desenvolvimento, mas também todos os analisadores anteriores abertos. Para redefinir o Hive Roslyn, você deve excluí-lo manualmente de %LocalAppData%\Microsoft\VisualStudio. O nome da pasta do hive 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 novo projeto de Aplicativo de Console em 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 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 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 no 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 aplica uma correção de código sugerida. Ao criar seu analisador e correção de código, você escreverá testes para estruturas de código diferentes 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.

Dica

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 disparar o 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ê escreve um analisador que relata ao usuário quaisquer declarações de variáveis locais que possam 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. Ele 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 de inicializador e também análise de fluxo de dados, para garantir que nunca ocorram gravações na variável. A Plataforma do Compilador do .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 o idioma em que ele 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:

  1. Registrar ações. As ações representam alterações de código que devem disparar o analisador para examinar o código em busca de violações. Quando o Visual Studio detecta 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 procurando declarações locais e verá quais deles têm valores constantes. Se uma declaração puder ser constante, o analisador criará e emitirá um diagnóstico.

A primeira etapa é atualizar os constantes de registro e o método Initialize para que esses constantes indiquem seu analisador denominado "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:

  • 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 figura a seguir:

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 atua em símbolos para uma que age na sintaxe. MakeConstAnalyzerAnalyzer.Initialize No método, localize a linha que registra 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. Esse analisador examina SyntaxKind.LocalDeclarationStatement, não SymbolKind.NamedType declarações. Observe que AnalyzeNode tem rabiscos vermelhos embaixo dele. 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)
{
}

Mude "Category" para "Usage" em MakeConstAnalyzer.cs conforme mostrado no código a seguir:

private const string Category = "Usage";

Localize as declarações locais que podem ser constantes

É hora de escrever a primeira versão do AnalyzeNode método. 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 é localizar declarações locais. Adicione o seguinte código ao AnalyzeNodeMakeConstAnalyzer.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 em busca de modificadores const. Se você encontrá-los, retorne imediatamente. O código a seguir procura por 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 se a variável pode ser const. Isso significa garantir que ele nunca seja atribuído depois de inicializado.

Você executará 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. Um 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 realizar 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 gravada com um novo valor em qualquer outro lugar. Chame o membro de extensão GetDeclaredSymbol para recuperar a variável ILocalSymbol e verifique se ela não está contida na coleção DataFlowAnalysis.WrittenOutside da análise de 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 que acabou de ser adicionado garante que a variável não seja modificada e, portanto, pode 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 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.
  • Um aviso em banner na parte superior do editor dizendo que 'MakeConstCodeFixProvider' encontrou um erro e foi desabilitado. Isso ocorre porque o provedor de correção de código ainda não foi alterado e continua esperando encontrar elementos TypeDeclarationSyntax em vez de elementos LocalDeclarationStatementSyntax.

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 aborda 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 seu analisador de diagnóstico, mas ainda não implementa a transformação de código correta.

Em seguida, exclua o MakeUppercaseAsync método. 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. 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ê observará os rabiscos vermelhos no código que acabou de adicionar no símbolo MakeConstAsync. Adicione uma declaração para MakeConstAsync como o código a seguir:

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

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

Você cria um novo const token de palavra-chave para inserir no início da 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 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 suas 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;

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

  1. Obter um identificador para o documento existente.
  2. Crie um novo documento substituindo a declaração existente pela nova declaração.
  3. Retorne 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);

Sua correção de código está pronta para teste. 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 do C# e adicione algumas declarações de variável local inicializadas com valores constantes ao método Main. Você verá que eles são relatados como avisos conforme mostrado abaixo.

Pode fazer avisos sobre constantes

Você fez muito progresso. Há rabiscos sob as declarações que podem ser tornados const. Mas ainda há trabalho a fazer. Isso funcionará bem se você adicionar const às declarações que começam com i, então j e por fim k. Mas, se você adicionar o modificador const em uma ordem diferente, começando com k, o analisador criará erros: k não poderá ser declarado const, a menos que i e j ambos já estejam 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á inúmeras declarações possíveis em que essa implementaçã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 o teste de unidade de correção de código. TestMethod1 mostra o padrão de um teste que garante que o analisador não relate 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 teste 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 é informado 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 foi relatado para text.

Substitua os testes de modelo na MakeConstUnitTest classe 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 seja aprovado. No Visual Studio, abra o Explorador de Testes selecionando Testar>Windows>Explorador de Testes. 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 o código que não deverão gerar o diagnóstico. O analisador já realiza 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);
    }
}
");
        }

Estes testes passam porque o analisador já lida com essas condições.

  • As variáveis atribuídas após a inicialização são detectadas pela análise de fluxo de dados.
  • Declarações que já const são excluídas ao verificar a palavra-chave const.
  • As declarações sem inicializador são tratadas pela análise de fluxo de dados que detecta atribuições fora da declaração.

Em seguida, adicione métodos de teste para condições que ainda não foram tratadas.

  • Declarações em que o inicializador não é uma constante, pois 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);
        }
    }
    ");
            }
    

Pode ser ainda mais complicado porque o C# permite várias declarações como uma instrução. Considere a seguinte constante de string para o 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 feita constante, mas a variável j não pode. Portanto, essa declaração não pode ser uma declaração const.

Execute seus testes novamente e você verá esses dois últimos casos de teste falharem.

Atualize o analisador para ignorar 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 alterações a seguir em AnalyzeNode:

  • Sua análise semântica examinou uma única declaração de variável. Esse código precisa estar em um foreach loop que examine todas as variáveis declaradas na mesma instrução.
  • Cada variável declarada precisa ter um inicializador.
  • Cada inicializador de variável declarada deve ser uma constante de tempo de compilação.

Em 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 a análise sintactica. 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 ela tem um impacto maior no desempenho. Execute seus testes novamente e você verá todos eles passarem.

Adicionar o final polonês

Você está quase lá. Há mais algumas condições com as quais o analisador deve lidar. O Visual Studio chama analisadores enquanto o usuário está escrevendo código. 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. Portanto, a implementação atual converterá alegremente uma declaração incorreta, 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 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 completo, você precisa adicionar outro teste para garantir que você possa criar uma declaração constante para uma cadeia de caracteres. O snippet 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"";
    }
}
");
        }

Por fim, se uma variável for declarada com a var palavra-chave, a correção de código fará a coisa errada e gerará uma const var declaração, que não tem suporte na linguagem C#. Para corrigir esse bug, a correção de código deve substituir a var palavra-chave 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 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 em que 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, 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 certificar-se de que ele é conversível para o tipo de variável. Adicione a seguinte verificação após garantir que o inicializador seja 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 se baseia 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ê deve escrever um pouco mais de código em seu provedor de correção de código para substituir a var palavra-chave pelo nome de tipo correto. Volte para MakeConstCodeFixProvider.cs. O código que você adicionará executa as seguintes etapas:

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

Isso soa como um monte de código. Não, não é. Substitua a linha que declara e inicializa newLocal pelo código a seguir. 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. 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 Roslyn Preview carregada.

  • Na segunda instância do Visual Studio, crie um novo projeto de Aplicativo de Console em C# 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 erro, não deve ser gerado nenhum aviso.
  • Por fim, adicione outra variável local que use a var palavra-chave. Você verá que um aviso é exibido e uma sugestão aparece na parte inferior esquerda.
  • Mova o cursor do editor sobre o sublinhado ondulado e pressione Ctrl+.. para exibir a correção de código sugerida. Ao selecionar sua correção de código, observe que a var palavra-chave agora é tratada 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 a análise de código on-the-fly 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 Roslyn). Você pode verificar seu trabalho em relação ao exemplo concluído em nosso repositório GitHub de exemplos.

Outros recursos