Partilhar via


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

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 .NET Compiler Platform fornece a estrutura para executar análises enquanto os programadores escrevem código, bem como todas as funcionalidades da interface Visual Studio para corrigir código: mostrar rabiscos no editor, preencher a Lista de Erros do Visual Studio, criar as sugestões de "lâmpada" e mostrar a pré-visualização rica das correções sugeridas.

Neste tutorial, explora a criação de um analisador e uma correção de código associada 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, pode associar uma correção de código ao analisador para representar uma modificação ao código-fonte do utilizador. Este tutorial cria um analisador que encontra declarações locais de variáveis que poderiam 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

Precisa de instalar o SDK da Plataforma de Compilação .NET usando o Visual Studio Installer:

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.

  1. Executar o instalador do Visual Studio
  2. Selecione Modificar
  3. Verifique a tarefa de desenvolvimento de extensões do Visual Studio.
  4. Abra o nó de desenvolvimento de extensões do Visual Studio na estrutura de resumo.
  5. Certifique-se de que a caixa de seleção para .NET Compiler Platform SDK está assinalada.
  6. Selecione Modificar.

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

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

Instalar usando o Visual Studio Installer - separador Componentes Individuais

  1. Executar o instalador do Visual Studio
  2. Selecione Modificar
  3. Selecione a guia Componentes individuais
  4. 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 .
  5. Selecione Modificar.

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

  1. Assinale a caixa do editor DGML. Você vai encontrá-lo na seção Ferramentas de código .

Criar e validar o seu analisador envolve vários passos:

  1. Crie a solução.
  2. Registre o nome e a descrição do analisador.
  3. Avisos e recomendações do analisador de relatórios.
  4. Implemente a correção de código para aceitar recomendações.
  5. Melhorar a análise através de testes unitários.

Criar a solução

  1. No Visual Studio, selecione Ficheiro>Novo>Projeto/Solução para abrir o diálogo Novo Projeto.
  2. Na caixa de pesquisa, escreva Analyzer para encontrar o modelo Analyzer with Code Fix (.NET Standard ).
  3. Selecione Avançar.
  4. Nomeie o seu projeto como "MakeConst" e selecione Criar.

Observação

Podes receber um erro de compilação (MSB4062: A tarefa "CompareBuildTaskVersion" não conseguiu ser carregada). Para corrigir este erro, atualize os pacotes NuGet na solução usando o Gestor de Pacotes NuGet ou utilize Update-Package na janela da Consola do Gestor 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. Essa diferença permite-lhe diferenciar as definiçõ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 replique as suas definições nem inicie sessão na sua conta do Visual Studio durante a execuçã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 da colmeia Roslyn termina em Roslyn, por exemplo, 16.0_9ae182f9Roslyn. Pode ser necessário limpar a solução e reconstruí-la depois de eliminar 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:

Aviso do analisador de relatórios

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 selecionar a 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 vê o analisador inicial em ação, feche a segunda instância do Visual Studio e volte 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 ótima ferramenta para explorar e encontrar construções em que talvez ainda não tenha pensado.

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 anterior, x é-lhe atribuído um valor constante e nunca é modificado. Pode ser declarado usando o const modificador:

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

Está envolvida a análise para determinar se uma variável pode ser tornada constante, requerendo análise sintática, análise constante da expressão inicializadora e análise de fluxo de dados para garantir que a variável nunca é escrita. 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 ficheiro 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:

  1. 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.
  2. 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 seu analisador cria e reporta um 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. Segue essa prática para facilitar a localização. Abra o arquivo Resources.resx para o projeto do analisador MakeConst . Esta ação mostra o editor de recursos. Atualize os recursos de cadeia de caracteres da seguinte maneira:

  • Mude AnalyzerDescription para "Variables that aren't modified should be made constants.".
  • Mude AnalyzerMessageFormat para "Variable '{0}' can be made constant".
  • Mude AnalyzerTitle para "Variable can be made constant".

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

Atualizar recursos de cadeia de caracteres

Faça as alterações restantes no ficheiro 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 acabaste de adicionar faz referência a um AnalyzeNode método que não está 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

Agora está pronto para escrever a primeira versão do AnalyzeNode método. Deve procurar uma única declaração local que possa ser const mas não seja, 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. Essa verificação garante que a variável nunca é atribuída depois de inicializada.

Realize alguma análise semântica usando o SyntaxNodeAnalysisContext. Use o context argumento para determinar se a declaração local da variável 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. Use o Microsoft.CodeAnalysis.SemanticModel para realizar análise de fluxo de dados na declaração local. Depois, use os resultados desta análise de fluxo de dados para garantir que a variável local não é escrita com um novo valor em qualquer outro lugar. Chame o GetDeclaredSymbol membro da extensão para recuperar o ILocalSymbol da variável e verifique se não está incluída na DataFlowAnalysis.WrittenOutside coleção 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 que acabou de adicionar garante que a variável não é modificada e, portanto, pode ser criada 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 em banner no topo do editor a dizer que o 'MakeConstCodeFixProvider' encontrou um erro e foi desativado. Este erro ocorre porque o fornecedor de correção de código ainda não foi alterado e continua a esperar 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 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. A sua correção cria 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)
{
}

O seu novo método MakeConstAsync transforma o Document ficheiro de origem do utilizador num novo Document que agora contém uma declaração const.

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:

  1. Obtenha um identificador para o documento existente.
  2. Crie 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 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. Vê que são reportados como avisos, como mostrado na imagem seguinte.

Pode gerar avisos de constantes

Fizeste muitos progressos. Há hesitações sob as declarações que podem ser feitas const. Mas ainda há trabalho a fazer. Isto funciona bem se adicionares const às declarações que começam por i, depois 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. Precisas de fazer mais análise para garantir que lidas com as diferentes formas como as variáveis podem ser declaradas e inicializadas.

Testes de unidade de compilação

O seu analisador e correção de código funcionam num caso simples de uma única declaração que pode ser tornado const. Existem inúmeras declarações possíveis onde esta 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 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 para text. Por padrão, este formulário pode ser utilizado apenas para testar analisadores, com exatamente um DiagnosticDescriptor fornecido por DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: indica que um diagnóstico com IdExpectedDiagnosticId é reportado para text.

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. Existem vários casos de teste para código que não deveriam aumentar o 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 const são filtradas através da verificação da palavra-chave const.
  • 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 onde o inicializador não é 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 constante, mas a variável j não pode. Portanto, esta afirmação não pode ser uma declaração const.

Faz os teus testes novamente e vês que estes dois últimos casos de teste falham.

Atualize o analisador para ignorar as declarações corretas

Precisa de melhorar o método AnalyzeNode do seu analisador para filtrar o código que cumpra estas condições. Estas condições estão todas relacionadas, por isso mudanças semelhantes corrigem todas estas 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 foreach loop 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 ciclo examina cada declaração de variável utilizando 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 num ciclo separado porque têm um impacto maior no desempenho. Execute novamente os seus testes, e deve ver que todos passam.

Dê os retoques finais

Estás quase a terminar. 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 seu analisador é chamado para código que não compila. O método do AnalyzeNode analisador de diagnóstico não verifica se o valor constante é convertível para o tipo de variável. Assim, a implementação atual converte felizmente uma declaração incorreta, como por exemplo int i = "abc" , numa 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 snippet seguinte define tanto o código que levanta o diagnóstico como 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 for declarada com a var palavra-chave, a correção do código faz a coisa errada e gera uma const var declaração, que a linguagem C# não suporta. 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, pode resolver todos estes bugs usando as mesmas técnicas que acabou de aprender.

Para corrigir o primeiro bug, abra MakeConstAnalyzer.cs e localize o foreach ciclo onde cada um dos inicializadores da declaração local são verificados para garantir que lhes são atribuídos valores constantes. Imediatamente antes do primeiro foreach ciclo, ligue context.SemanticModel.GetTypeInfo() para obter 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 do encerramento do primeiro foreach ciclo, adicione o seguinte código para verificar o tipo da declaração local quando a constante é uma cadeia ou nula.

// 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 var declaraçã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 var nã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 bugs, não é reportado nenhum aviso para esta declaração local de variável (embora haja um erro do compilador, como esperado).
  • Em seguida, adicione object s = "abc"; ao método Main. Devido à segunda correção, não é reportado nenhum aviso.
  • Finalmente, adicione outra variável local que usa a var palavra-chave. Vê-se que é reportado um aviso e uma sugestão aparece à esquerda, por baixo.
  • 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 var agora é 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! Criaste a tua primeira extensão da .NET Compiler Platform que faz análises de código em tempo real para detetar um problema e fornece uma solução rápida para o corrigir. Pelo caminho, aprendeu muitas das APIs de código que fazem parte do SDK da Plataforma do Compilador .NET (APIs Roslyn). Pode verificar o seu trabalho em relação à amostra concluída no repositório de exemplos no GitHub.

Outros recursos