Partilhar via


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

O SDK da Plataforma de Compilador .NET fornece as ferramentas de que precisa para criar diagnósticos personalizados (analisadores), correções de código, refatorização de código e supressores de diagnóstico que visam O C# ou o código do Visual Basic. Um analisador contém código que reconhece violações da sua regra. A correção do código contém o código que corrige a violação. As regras que implementar podem ser tudo, desde a estrutura de código até ao estilo de codificação, a convenções de nomenclatura e muito mais. A Plataforma de Compilador do .NET fornece a arquitetura para executar a análise à medida que os programadores estão a escrever código e todas as funcionalidades da IU do Visual Studio para corrigir código: mostrar alternâncias no editor, preencher a Lista de Erros do Visual Studio, criar as sugestões de "lâmpada" e mostrar a pré-visualização avançada das correções sugeridas.

Neste tutorial, irá explorar a criação de um analisador e uma correção de código que o acompanha com as APIs Roslyn. Um analisador é uma forma de executar a análise do código fonte e comunicar um problema ao utilizador. Opcionalmente, uma correção de código pode ser associada ao analisador para representar uma modificação ao código fonte do utilizador. Este tutorial cria um analisador que localiza declarações de variáveis locais que podem ser declaradas com o const modificador, mas não o são. A correção do código que o acompanha modifica essas declarações para adicionar o const modificador.

Pré-requisitos

Terá de instalar o SDK da Plataforma do Compilador de .NET através do Instalador do Visual Studio:

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

Existem duas formas diferentes de encontrar o SDK de Plataforma do Compilador .NET no Instalador do Visual Studio:

Instalar com a vista Instalador do Visual Studio – Cargas de trabalho

O SDK da Plataforma de Compilador .NET não é selecionado automaticamente como parte da carga de trabalho de desenvolvimento da extensão do Visual Studio. Tem de selecioná-lo como um componente opcional.

  1. Executar o Instalador do Visual Studio
  2. Selecione Modificar
  3. Verifique a carga de trabalho de desenvolvimento da extensão do Visual Studio .
  4. Abra o nó de desenvolvimento da extensão do Visual Studio na árvore de resumo.
  5. Selecione a caixa do SDK de Plataforma do Compilador .NET. Irá encontrá-lo em último nos componentes opcionais.

Opcionalmente, também vai querer que o editor de DGML apresente gráficos no visualizador:

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

Instalar com o Instalador do Visual Studio – separador Componentes individuais

  1. Executar o Instalador do Visual Studio
  2. Selecione Modificar
  3. Selecione o separador Componentes individuais
  4. Selecione a caixa do SDK de Plataforma do Compilador .NET. Irá encontrá-la na parte superior na secção Compiladores, ferramentas de compilação e runtimes .

Opcionalmente, também vai querer que o editor de DGML apresente gráficos no visualizador:

  1. Selecione a caixa do editor DGML. Irá encontrá-lo na secção Ferramentas de código .

Existem vários passos para criar e validar o seu analisador:

  1. Crie a solução.
  2. Registe 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 de unidades.

Criar a solução

  • No Visual Studio, selecione Ficheiro > Novo > Projeto... para apresentar a caixa de diálogo Novo Projeto.
  • Em Extensibilidade do Visual C#>, selecione Analisador com correção de código (.NET Standard).
  • Dê o nome "MakeConst" ao projeto e clique em OK.

Nota

Poderá obter um erro de compilação (MSB4062: Não foi possível carregar a tarefa "CompareBuildTaskVersion"). Para corrigir esta situação, atualize os pacotes NuGet na solução com o Gestor de Pacotes NuGet ou utilize Update-Package na janela Consola do Gestor 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 é utilizado para produzir o pacote NuGet para o analisador e correção de código.
  • MakeConst.Test, que é um projeto de teste de unidades.
  • MakeConst.Vsix, que é o projeto de arranque predefinido que inicia uma segunda instância do Visual Studio que carregou o seu novo analisador. Prima F5 para iniciar o projeto VSIX.

Nota

Os analisadores devem visar o .NET Standard 2.0 porque podem ser executados no ambiente .NET Core (compilações de linha de comandos) e .NET Framework ambiente (Visual Studio).

Dica

Quando executa o analisador, inicia uma segunda cópia do Visual Studio. Esta segunda cópia utiliza um ramo de registo diferente para armazenar as definições. Isto permite-lhe diferenciar as definições visuais nas duas cópias do Visual Studio. Pode escolher um tema diferente para a execução experimental do Visual Studio. Além disso, não utilize a execução experimental do Visual Studio para não percorrer as suas definições nem iniciar sessão na sua conta do Visual Studio. Isto mantém as definições diferentes.

O hive inclui não só o analisador em desenvolvimento, mas também quaisquer analisadores anteriores abertos. Para repor o ramo de registo roslyn, tem de eliminá-lo manualmente de %LocalAppData%\Microsoft\VisualStudio. O nome da pasta do ramo de registo Roslyn terminará em Roslyn, por exemplo, 16.0_9ae182f9Roslyn. Tenha em atenção que poderá ter de limpar a solução e recompilá-la depois de eliminar o ramo de registo.

Na segunda instância do Visual Studio que acabou de iniciar, crie um novo projeto de Aplicação de Consola C# (qualquer arquitetura de destino funcionará – os analisadores funcionam ao nível da origem.) Paire o cursor sobre o token com um sublinhado ondulado e é apresentado o texto de aviso fornecido por um analisador.

O modelo cria um analisador que comunica um aviso em cada declaração de tipo em que o nome do tipo contém letras minúsculas, conforme mostrado na figura seguinte:

Aviso de relatórios do Analisador

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

Não tem de iniciar uma segunda cópia do Visual Studio e criar um novo código para testar todas as alterações no seu analisador. O modelo também cria um projeto de teste de unidades para si. Esse projeto contém dois testes. TestMethod1 mostra o formato típico de um teste que analisa código sem acionar um diagnóstico. TestMethod2 mostra o formato de um teste que aciona um diagnóstico e, em seguida, aplica uma correção de código sugerida. À medida que cria o analisador e a correção de código, irá escrever testes para diferentes estruturas de código para verificar o seu trabalho. Os testes de unidades para analisadores são muito mais rápidos do que testá-los interativamente com o Visual Studio.

Dica

Os testes de unidades do Analisador são uma ótima ferramenta quando sabe que construções de código devem e não devem acionar o analisador. Carregar o seu analisador noutra cópia do Visual Studio é uma ótima ferramenta para explorar e encontrar construções em que possa ainda não ter pensado.

Neste tutorial, vai escrever um analisador que reporta ao utilizador 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. Pode ser declarado com o const modificador:

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

A análise para determinar se uma variável pode ser feita constante está envolvida, 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 desta análise.

Criar registos 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ósticos tem de fornecer um [DiagnosticAnalyzer] atributo que descreva o idioma em que opera.
  • Cada analisador de diagnósticos tem de derivar (direta ou indiretamente) da DiagnosticAnalyzer classe .

O modelo também mostra as funcionalidades básicas que fazem parte de qualquer analisador:

  1. Registar ações. As ações representam alterações de código que devem acionar o analisador para examinar o código relativamente a violações. Quando o Visual Studio deteta edições de código que correspondem a uma ação registada, chama o método registado do analisador.
  2. Criar diagnósticos. Quando o analisador deteta uma violação, cria um objeto de diagnóstico que o Visual Studio utiliza para notificar o utilizador da violação.

Regista ações na substituição do DiagnosticAnalyzer.Initialize(AnalysisContext) método . Neste tutorial, irá visitar nós de sintaxe à procura de declarações locais e ver quais deles têm valores constantes. Se uma declaração puder ser constante, o analisador irá criar e comunicar um diagnóstico.

O primeiro passo é atualizar as constantes de registo e Initialize o método para que estas constantes indiquem o analisador "Make Const". A maioria das constantes de cadeia são definidas no ficheiro de recurso de cadeia. Deve seguir essa prática para facilitar a localização. Abra o ficheiro Resources.resx para o projeto makeConst analyzer. Esta ação apresenta o editor de recursos. Atualize os recursos da cadeia da seguinte forma:

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

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

Atualizar recursos de cadeias de carateres

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

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Substitua-o pela seguinte linha:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Após essa alteração, pode eliminar o AnalyzeSymbol método . Este analisador examina SyntaxKind.LocalDeclarationStatement, não SymbolKind.NamedType as instruções. Repare que AnalyzeNode tem ondulantes vermelhos por baixo do mesmo. O código que acabou de adicionar referencia um AnalyzeNode método que não foi declarado. Declare esse método com o seguinte código:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

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

private const string Category = "Usage";

Localizar declarações locais que podem ser constantes

Está na altura de escrever a primeira versão do AnalyzeNode método. Deve procurar uma única declaração local que possa ser const , mas não é, como o seguinte código:

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

O primeiro passo é encontrar declarações locais. Adicione o seguinte código a AnalyzeNode em MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Este elenco é sempre bem-sucedido porque o analisador registou alterações nas declarações locais e apenas nas declarações locais. Nenhum outro tipo de nó aciona uma chamada para o seu AnalyzeNode método. Em seguida, verifique se existem modificadores na declaração const . Se as encontrar, regresse imediatamente. O código seguinte procura quaisquer const modificadores na declaração local:

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

Por fim, tem de verificar se a variável pode ser const. Isto significa certificar-se de que nunca é atribuído depois de ser inicializado.

Irá efetuar alguma análise semântica com o SyntaxNodeAnalysisContext. Utilize o context argumento para determinar se a declaração de variável local pode ser efetuada const. A Microsoft.CodeAnalysis.SemanticModel representa todas as informações semânticas num único ficheiro de origem. Pode saber mais no artigo que abrange modelos semânticos. Irá utilizar o Microsoft.CodeAnalysis.SemanticModel para efetuar a análise do fluxo de dados na declaração local. Em seguida, utilize 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 método de extensão para obter o ILocalSymbol para a variável e verifique se não está contido 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 adicionado garante que a variável não é modificada e, por conseguinte, pode ser efetuada const. Está na altura de criar 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));

Pode verificar o progresso ao premir F5 para executar o analisador. Pode carregar a aplicação de consola que 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 comunicar um diagnóstico. No entanto, dependendo da sua versão do Visual Studio, verá:

  • A lâmpada, que ainda utiliza a correção de código gerada pelo modelo, irá indicar-lhe que pode ser feita em maiúsculas.
  • Uma mensagem de faixa na parte superior do editor a dizer que "MakeConstCodeFixProvider" encontrou um erro e foi desativada.". Isto acontece porque o fornecedor de correção de código ainda não foi alterado e ainda espera encontrar TypeDeclarationSyntax elementos em vez de LocalDeclarationStatementSyntax elementos.

A secção seguinte 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 comunicado. Para o analisador que criou, pode fornecer uma correção de código que insira a palavra-chave de configuração:

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

O utilizador escolhe-a na IU da lâmpada no editor e o Visual Studio altera o código.

Abra o ficheiro CodeFixResources.resx e mude CodeFixTitle para "Make constant".

Abra o ficheiro MakeConstCodeFixProvider.cs adicionado pelo modelo. Esta correção de código já está ligada ao ID de Diagnóstico produzido pelo analisador de diagnóstico, mas ainda não implementa a transformação de código correta.

Em seguida, elimine o MakeUppercaseAsync método . Já não se aplica.

Todos os fornecedores de correção de código derivam do CodeFixProvider. Todas elas substituem CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) o relatório de correções de código disponíveis. Em RegisterCodeFixesAsync, altere o tipo de nó predecessora que está a procurar 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 registar uma correção de código. A correção irá 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);

Irá reparar em ondulantes vermelhos no código que acabou de adicionar ao símbolo MakeConstAsync. Adicione uma declaração para MakeConstAsync o seguinte código:

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

O seu novo MakeConstAsync método transformará o ficheiro de origem Document do utilizador que representa um novo Document que agora contém uma const declaração.

Crie um novo const token de palavra-chave para inserir na parte frontal da instrução de declaração. Tenha cuidado para remover primeiro qualquer trivialidade à esquerda do primeiro token da instrução de declaração e anexe-a 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 com 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 C#. Formatar as alterações para corresponder ao código existente cria uma melhor experiência. 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);

É necessário um novo espaço de nomes para este código. Adicione a seguinte using diretiva à parte superior do ficheiro:

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 ao substituir a declaração existente pela nova declaração.
  3. Devolver 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 código está pronta a tentar. Prima F5 para executar o projeto de analisador numa segunda instância do Visual Studio. Na segunda instância do Visual Studio, crie um novo projeto de Aplicação de Consola C# e adicione algumas declarações de variáveis locais inicializadas com valores constantes ao método Main. Verá que são comunicados como avisos como abaixo.

Pode fazer avisos simultâneos

Fez muitos progressos. Existem alternâncias nas declarações que podem ser efetuadas const. Mas ainda há trabalho a fazer. Isto funciona bem se adicionar const às declarações a ipartir de , em seguida j , e, por fim, k. No entanto, se adicionar o const modificador por uma ordem diferente, começando com k, o analisador cria erros: k não pode ser declarado const, a menos que i e jconstsejam ambos . Tem de fazer mais análises para garantir que lida com as diferentes formas como as variáveis podem ser declaradas e inicializadas.

Compilar testes de unidades

O analisador e a correção de código funcionam num caso simples de uma única declaração que pode ser efetuada. Existem inúmeras declarações possíveis em que esta implementação comete erros. Irá resolver estes casos ao trabalhar com a biblioteca de teste de unidades escrita pelo modelo. É muito mais rápido do que abrir repetidamente uma segunda cópia do Visual Studio.

Abra o ficheiro MakeConstUnitTests.cs no projeto de teste de unidades. 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 comunica um diagnóstico quando não deve. TestMethod2 mostra o padrão para comunicar um diagnóstico e executar a correção de código.

O modelo utiliza pacotes Microsoft.CodeAnalysis.Testing para testes de unidades.

Dica

A biblioteca de testes suporta uma sintaxe de marcação especial, incluindo o seguinte:

  • [|text|]: indica que foi comunicado um diagnóstico para text. Por predefinição, este formulário só pode ser utilizado para testar analisadores com exatamente um DiagnosticDescriptor fornecido pelo DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: indica que é comunicado um diagnóstico com IdExpectedDiagnosticId para text.

Substitua os testes de modelo na classe pelo MakeConstUnitTest 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 passa. No Visual Studio, abra o Explorador de Testes ao selecionar Testar>Explorador de Testes doWindows>. Em seguida, selecione Executar Tudo.

Criar testes para declarações válidas

Regra geral, os analisadores devem sair o mais rapidamente possível, fazendo um trabalho mínimo. O Visual Studio chama analisadores registados à medida que o utilizador edita o código. A capacidade de resposta é um requisito fundamental. Existem vários casos de teste de código que não devem aumentar o diagnóstico. O analisador já processa um desses testes, o caso em que uma variável é atribuída depois de 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++);
    }
}
");
        }

Este teste também é aprovado. Em seguida, adicione métodos de teste para as condições que ainda não tratou:

  • Declarações que já constsão , porque já são constantes:

            [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 inicializador, porque não há valor para utilizar:

            [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 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 C# permite múltiplas declarações como uma instrução. Considere a seguinte constante de cadeia 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 tornar-se constante, mas a variável j não pode. Por conseguinte, esta declaração não pode ser efetuada numa declaração de contrassobilidade.

Execute os testes novamente e verá estes novos casos de teste falharem.

Atualizar o analisador para ignorar as declarações corretas

Precisa de algumas melhorias ao método do AnalyzeNode analisador para filtrar o código que corresponde a estas condições. São todas condições relacionadas, pelo que alterações semelhantes irão corrigir todas estas condições. Efetue as seguintes alterações a AnalyzeNode:

  • A análise semântica examinou uma única declaração de variável. Este código tem de estar num foreach ciclo que examine todas as variáveis declaradas na mesma instrução.
  • Cada variável declarada precisa de ter um inicializador.
  • O inicializador de cada variável declarada tem de 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 fragmento 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 através da análise sintática. A primeira verificação garante que a variável tem um inicializador. A segunda verificação garante que o inicializador é uma constante. O segundo ciclo tem a análise semântica original. As verificações semânticas estão num ciclo separado porque tem um maior impacto no desempenho. Execute os testes novamente e deverá vê-los todos passar.

Adicionar o polimento final

Está quase concluído. Existem mais algumas condições para o seu analisador processar. O Visual Studio chama analisadores enquanto o utilizador está a escrever código. Muitas vezes, o analisador será chamado para obter código que não é compilado. O método do analisador de AnalyzeNode diagnóstico não verifica se o valor constante é convertível para o tipo de variável. Assim, a implementação atual converterá alegremente uma declaração incorreta, como 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 processados corretamente. O único valor constante permitido para um tipo de referência é null, exceto no caso de , que permite literais de System.Stringcadeia. Por outras palavras, const string s = "abc" é legal, mas const object s = "abc" não é. Este fragmento 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, tem de adicionar outro teste para se certificar de que pode criar uma declaração constante para uma cadeia. O fragmento seguinte 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 faz a coisa errada e gera uma const var declaração, que não é suportada pela linguagem C#. Para corrigir este erro, a correção de código tem de 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 erros acima podem ser resolvidos com as mesmas técnicas que acabou de aprender.

Para corrigir o primeiro erro, abra primeiro MakeConstAnalyzer.cs e localize o ciclo foreach onde cada um dos inicializadores da declaração local é verificado para garantir que são atribuídos com valores constantes. Imediatamente antes do primeiro ciclo foreach, chame 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 ciclo foreach , verifique cada inicializador para se certificar de que é convertível para o 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 baseia-se na última. Antes da chaveta de fecho do primeiro ciclo foreach, adicione o seguinte código para verificar o tipo de declaração local quando a constante for 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;
}

Tem de escrever um pouco mais de código no seu fornecedor de correção de código para substituir a var palavra-chave pelo nome do tipo correto. Regresse a MakeConstCodeFixProvider.cs. O código que irá adicionar faz os seguintes passos:

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

Parece um monte de código. Não é. Substitua a linha que declara e inicializa newLocal pelo seguinte código. É 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);

Terá de adicionar uma using diretiva para utilizar o Simplifier tipo:

using Microsoft.CodeAnalysis.Simplification;

Execute os testes e todos devem passar. Dê os parabéns por ter executado o seu analisador concluído. Prima Ctrl+F5 para executar o projeto de analisador numa 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 Aplicação de Consola C# e adicione int x = "abc"; ao método Main. Graças à primeira correção do erro, não deve ser comunicado nenhum aviso para esta declaração de variável local (embora exista um erro de compilador conforme esperado).
  • Em seguida, adicione object s = "abc"; ao método Main. Devido à segunda correção do erro, não deve ser comunicado nenhum aviso.
  • Por fim, adicione outra variável local que utilize a var palavra-chave. Verá que é comunicado um aviso e é apresentada uma sugestão abaixo da esquerda.
  • Mova o cursor do editor sobre o sublinhado ondulado e prima Ctrl+.. para apresentar a correção de código sugerida. Ao selecionar a correção do código, tenha em atenção que a var palavra-chave é processada corretamente.

Por fim, adicione o seguinte código:

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

Após estas alterações, obtém apenas alterações vermelhas nas duas primeiras variáveis. Adicione const a i e j, e receberá um novo aviso, k porque agora pode ser const.

Parabéns! Criou a sua primeira extensão de Plataforma de Compilador .NET que efetua uma análise de código no momento para detetar um problema e fornece uma correção rápida para corrigi-lo. Ao longo do percurso, aprendeu muitas das APIs de código que fazem parte do SDK de Plataforma de Compilador de .NET (APIs Roslyn). Pode verificar o seu trabalho relativamente ao exemplo concluído no nosso repositório do GitHub de exemplo.

Outros recursos