Esercitazione: compilare il primo analizzatore con correzione del codice

L'SDK di .NET Compiler Platform fornisce gli strumenti necessari per creare diagnostica personalizzata (analizzatori), correzioni del codice, refactoring del codice e soppressori di diagnostica destinati a C# o Codice Visual Basic. Un analizzatore contiene codice che riconosce le violazioni della regola. La correzione del codice contiene il codice che corregge la violazione. Le regole implementate possono essere di qualsiasi tipo: struttura del codice, stile di codifica, convenzioni di denominazione e altro ancora. .NET Compiler Platform fornisce il framework per l'esecuzione di analisi mentre gli sviluppatori scrivono il codice e tutte le funzionalità dell'interfaccia utente di Visual Studio per la correzione del codice: visualizzazione di linee ondulate nell'editor, informazioni dell'Elenco errori di Visual Studio, creazione di suggerimenti "lampadina" e visualizzazione di un'anteprima dettagliata delle correzioni suggerite.

In questa esercitazione verrà esaminata la creazione di un analizzatore e una correzione del codice associato usando le API Roslyn. Un analizzatore è un modo per eseguire analisi del codice sorgente e segnalare un problema all'utente. Facoltativamente, una correzione del codice può essere associata all'analizzatore per rappresentare una modifica al codice sorgente dell'utente. In questa esercitazione verrà creato un analizzatore per trovare le dichiarazioni di variabili locali che potrebbero essere dichiarate tramite il modificatore const ma che lo non sono. La correzione del codice associata modifica tali dichiarazioni per aggiungere il modificatore const.

Prerequisiti

È necessario installare l'SDK di .NET Compiler Platform tramite il Programma di installazione di Visual Studio:

Istruzioni di installazione: Programma di installazione di Visual Studio

Esistono due modi diversi per trovare .NET Compiler Platform SDK nel programma di installazione di Visual Studio:

Eseguire l'installazione con il Programma di installazione di Visual Studio: visualizzazione dei carichi di lavoro

.NET Compiler Platform SDK non viene selezionato automaticamente come parte del carico di lavoro Sviluppo di estensioni di Visual Studio. È necessario selezionarlo come componente facoltativo.

  1. Eseguire il programma di installazione di Visual Studio.
  2. Selezionare Modifica
  3. Selezionare il carico di lavoro Sviluppo di estensioni di Visual Studio.
  4. Aprire il nodo Sviluppo di estensioni di Visual Studio nell'albero di riepilogo.
  5. Selezionare la casella di controllo per .NET Compiler Platform SDK. È l'ultima voce dei componenti facoltativi.

Facoltativamente, è possibile installare anche l'editor DGML per visualizzare i grafici nel visualizzatore:

  1. Aprire il nodo Singoli componenti nell'albero di riepilogo.
  2. Selezionare la casella per l'editor DGML

Eseguire l'installazione usando il Programma di installazione di Visual Studio: scheda Singoli componenti

  1. Eseguire il programma di installazione di Visual Studio.
  2. Selezionare Modifica
  3. Selezionare la scheda Singoli componenti
  4. Selezionare la casella di controllo per .NET Compiler Platform SDK. È la prima voce nella sezione Compilatori, strumenti di compilazione e runtime.

Facoltativamente, è possibile installare anche l'editor DGML per visualizzare i grafici nel visualizzatore:

  1. Selezionare la casella di controllo per Editor DGML. La voce è disponibile nella sezione Strumenti per il codice.

La creazione e la convalida dell'analizzatore comprendono diversi passaggi:

  1. Creare la soluzione.
  2. Registrare il nome e la descrizione dell'analizzatore.
  3. Segnalare gli avvisi e i suggerimenti dell'analizzatore.
  4. Implementare la correzione del codice per accettare i suggerimenti.
  5. Migliorare l'analisi tramite unit test.

Creare la soluzione

  • In Visual Studio scegliere File > Nuovo > progetto per visualizzare la finestra di dialogo Nuovo progetto.
  • In Estendibilità visual C# >scegliere Analizzatore con correzione del codice (.NET Standard).
  • Assegnare al progetto il nome "MakeConst" e fare clic su OK.

Nota

È possibile che venga visualizzato un errore di compilazione (MSB4062: Impossibile caricare l'attività "CompareBuildTaskVersion"). Per risolvere questo problema, aggiornare i pacchetti NuGet nella soluzione con Gestione pacchetti NuGet o usare Update-Package nella finestra Console di Gestione pacchetti di Gestione pacchetti.

Esplorare il modello dell'analizzatore

L'analizzatore con il modello di correzione del codice crea cinque progetti:

  • MakeConst, che contiene l'analizzatore.
  • MakeConst.CodeFixes, che contiene la correzione del codice.
  • MakeConst.Package, usato per produrre il pacchetto NuGet per l'analizzatore e la correzione del codice.
  • MakeConst.Test, che è un progetto di unit test.
  • MakeConst.Vsix, ovvero il progetto di avvio predefinito che avvia una seconda istanza di Visual Studio che ha caricato il nuovo analizzatore. Premere F5 per avviare il progetto VSIX.

Nota

Gli analizzatori devono essere destinati a .NET Standard 2.0 perché possono essere eseguiti nell'ambiente .NET Core (compilazioni della riga di comando) e nell'ambiente .NET Framework (Visual Studio).

Suggerimento

Quando si esegue l'analizzatore, si avvia una seconda copia di Visual Studio. Questa seconda copia usa un diverso hive del Registro di sistema per archiviare le impostazioni. Questo consente di distinguere le impostazioni di visualizzazione nelle due copie di Visual Studio. È possibile scegliere un tema diverso per l'esecuzione sperimentale di Visual Studio. Inoltre, non eseguire il roaming delle impostazioni o accedere all'account di Visual Studio usando l'istanza sperimentale di Visual Studio. Ciò consente di mantenere diverse le impostazioni.

L'hive include non solo l'analizzatore in fase di sviluppo, ma anche tutti gli analizzatori precedenti aperti. Per reimpostare l'hive Roslyn, è necessario eliminarlo manualmente da %LocalAppData%\Microsoft\VisualStudio. Il nome della cartella di Roslyn hive termina in Roslyn, ad esempio . 16.0_9ae182f9Roslyn Si noti che potrebbe essere necessario pulire la soluzione e ricompilarla dopo l'eliminazione dell'hive.

Nella seconda istanza di Visual Studio appena avviata creare un nuovo progetto applicazione console C# (qualsiasi framework di destinazione funzionerà - gli analizzatori funzionano a livello di origine). Passare il puntatore del mouse sul token con una sottolineatura ondulata e viene visualizzato il testo di avviso fornito da un analizzatore.

Il modello crea un analizzatore che genera un avviso per ogni dichiarazione di tipo in cui il nome del tipo contiene lettere minuscole, come illustrato nella figura seguente:

Avviso di segnalazione dell'analizzatore

Il modello fornisce inoltre una correzione del codice che converte in lettere maiuscole qualsiasi nome di un tipo contenente caratteri minuscoli. È possibile fare clic sulla lampadina visualizzata con il messaggio di avviso per vedere le modifiche suggerite. Accettando le modifiche suggerite, vengono aggiornati il nome del tipo e tutti i riferimenti a tale tipo nella soluzione. Dopo aver osservato l'analizzatore iniziale in azione, chiudere la seconda istanza di Visual Studio e tornare al progetto dell'analizzatore.

Non è necessario avviare una seconda copia di Visual Studio e creare nuovo codice per testare ogni modifica nell'analizzatore. Il modello crea automaticamente anche un progetto unit test. Tale progetto contiene due test. TestMethod1 illustra il formato tipico di un test che analizza il codice senza attivare la diagnostica. TestMethod2 illustra il formato di un test che attiva la diagnostica e quindi applica una correzione del codice suggerita. Durante la creazione dell'analizzatore e della correzione del codice, si scriveranno i test per le diverse strutture di codice per verificare il proprio lavoro. Gli unit test per gli analizzatori sono molto più rapidi rispetto ai test in modalità interattiva con Visual Studio.

Suggerimento

Gli unit test per gli analizzatori sono uno strumento molto utile quando si sa quali costrutti di codice devono attivare o meno l'analizzatore. Il caricamento dell'analizzatore in un'altra copia di Visual Studio è una soluzione ideale per esplorare e trovare costrutti a cui ancora non si è pensato.

In questa esercitazione si scrive un analizzatore che segnala all'utente eventuali dichiarazioni di variabili locali che possono essere convertite in costanti locali. Si consideri il codice di esempio seguente:

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

Nel codice precedente, a x viene assegnato un valore costante che non viene mai modificato. Può essere dichiarato usando il modificatore const:

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

Per determinare se una variabile può essere resa una costante, è necessaria un'analisi che richiede un'analisi sintattica, l'analisi delle costanti dell'espressione dell'inizializzatore e l'analisi del flusso di dati per garantire che non vengano mai eseguite operazioni di scrittura nella variabile. .NET Compiler Platform fornisce API che rendono più semplice l'esecuzione di questa analisi.

Creare registrazioni per l'analizzatore

Il modello crea la classe DiagnosticAnalyzer iniziale nel file MakeConstAnalyzer.cs. Questo analizzatore iniziale mostra due importanti proprietà di ogni analizzatore.

  • Ogni analizzatore diagnostico deve fornire un attributo [DiagnosticAnalyzer] che descrive il linguaggio su cui opera.
  • Ogni analizzatore di diagnostica deve derivare (direttamente o indirettamente) dalla DiagnosticAnalyzer classe .

Il modello illustra anche le funzionalità di base che fanno parte di qualsiasi analizzatore:

  1. Registrare le azioni. Le azioni rappresentano le modifiche del codice che devono attivare l'analizzatore per esaminare le violazioni del codice. Quando Visual Studio rileva modifiche del codice che corrispondono a un'azione registrata, esegue una chiamata al metodo registrato dell'analizzatore.
  2. Creare la diagnostica. Quando l'analizzatore rileva una violazione, crea un oggetto di diagnostica usato da Visual Studio per notificare all'utente la violazione.

Le azioni vengono registrate nell'override del metodo DiagnosticAnalyzer.Initialize(AnalysisContext). In questa esercitazione verranno esaminati i nodi di sintassi alla ricerca delle dichiarazioni locali, per determinare quali di queste hanno valori costanti. Se una dichiarazione può essere costante, l'analizzatore crea e segnala una diagnostica.

Il primo passaggio consiste nell'aggiornare le costanti di registrazione e il metodo Initialize, in modo che queste costanti specifichino l'analizzatore "MakeConst". La maggior parte delle costanti di stringa è definita nel file della risorsa stringa. È consigliabile attenersi a tale pratica per semplificare l'individuazione. Aprire il file Resources.resx per il progetto dell'analizzatore MakeConst. Verrà visualizzato l'editor di risorse. Aggiornare le risorse stringa come segue:

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

Al termine, l'editor di risorse verrà visualizzato come illustrato nella figura seguente:

Aggiornare le risorse stringa

Le modifiche rimanenti sono da apportare al file dell'analizzatore. Aprire MakeConstAnalyzer.cs in Visual Studio. Modificare l'azione registrata da una che opera sui simboli in una che opera sulla sintassi. Nel metodo MakeConstAnalyzerAnalyzer.Initialize trovare la riga che esegue la registrazione dell'azione sui simboli:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Sostituirla con la riga seguente:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Dopo questa modifica, è possibile eliminare il metodo AnalyzeSymbol. Questo analizzatore esamina le istruzioni SyntaxKind.LocalDeclarationStatement, non le istruzioni SymbolKind.NamedType. È possibile notare che sotto AnalyzeNode sono visualizzate delle linee ondulate. Il codice appena aggiunto fa riferimento a un metodo AnalyzeNode che non è stato dichiarato. Dichiarare tale metodo usando il codice seguente:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Modificare in Category "Usage" in MakeConstAnalyzer.cs come illustrato nel codice seguente:

private const string Category = "Usage";

Trovare le dichiarazioni locali che potrebbero essere costanti

È ora possibile scrivere la prima versione del metodo AnalyzeNode. Deve cercare una singola dichiarazione locale che può essere const ma non lo è, come ad esempio il codice seguente:

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

Il primo passaggio è trovare le dichiarazioni locali. Aggiungere il codice seguente a AnalyzeNode in MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Questo cast ha sempre esito positivo, perché l'analizzatore ha eseguito la registrazione per le modifiche delle dichiarazioni locali e solo per le dichiarazioni locali. Nessun altro tipo di nodo attiva una chiamata al metodo AnalyzeNode. Controllare quindi se nella dichiarazione sono presenti modificatori const. Se vengono rilevati, restituire immediatamente un risultato. Il codice seguente cerca eventuali modificatori const nella dichiarazione locale:

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

Infine, è necessario verificare che la variabile possa essere const. In altre parole, occorre verificare che non venga mai assegnata dopo l'inizializzazione.

Si eseguiranno alcune analisi semantiche usando SyntaxNodeAnalysisContext. Verrà usato l'argomento context per determinare se la dichiarazione di variabile locale può essere resa const. Un Microsoft.CodeAnalysis.SemanticModel oggetto rappresenta tutte le informazioni semantiche in un singolo file di origine. Per altre informazioni, vedere l'articolo relativo ai modelli semantici. Si userà Microsoft.CodeAnalysis.SemanticModel per eseguire l'analisi del flusso dei dati nell'istruzione della dichiarazione locale. Sarà quindi possibile usare i risultati di questa analisi del flusso di dati per assicurarsi che non venga mai eseguita la scrittura di un nuovo valore nella variabile locale. Chiamare il metodo di estensione GetDeclaredSymbol per recuperare ILocalSymbol per la variabile e verificare che non sia contenuto nella raccolta DataFlowAnalysis.WrittenOutside dell'analisi del flusso di dati. Aggiungere il codice seguente alla fine del metodo AnalyzeNode:

// Perform data flow analysis on the local declaration.
DataFlowAnalysis dataFlowAnalysis = context.SemanticModel.AnalyzeDataFlow(localDeclaration);

// Retrieve the local symbol for each variable in the local declaration
// and ensure that it is not written outside of the data flow analysis region.
VariableDeclaratorSyntax variable = localDeclaration.Declaration.Variables.Single();
ISymbol variableSymbol = context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken);
if (dataFlowAnalysis.WrittenOutside.Contains(variableSymbol))
{
    return;
}

Il codice appena aggiunto garantisce che la variabile non viene modificata e quindi può essere resa const. È ora possibile generare la diagnostica. Aggiungere il codice seguente come ultima riga in AnalyzeNode:

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

È possibile controllare lo stato di avanzamento premendo F5 per eseguire l'analizzatore. È possibile caricare l'applicazione console creata in precedenza e quindi aggiungere il codice di test seguente:

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

Verrà visualizzata la lampadina e l'analizzatore segnalerà la diagnostica. Tuttavia, a seconda della versione di Visual Studio, si vedrà:

  • La lampadina, che usa ancora la correzione del codice generata dal modello, indicherà che può essere fatta in lettere maiuscole.
  • Messaggio banner nella parte superiore dell'editor che indica che "MakeConstCodeFixProvider" ha rilevato un errore ed è stato disabilitato. Ciò è dovuto al fatto che il provider di correzione del codice non è ancora stato modificato e prevede comunque di LocalDeclarationStatementSyntax trovare TypeDeclarationSyntax elementi anziché elementi.

Nella sezione successiva viene descritto come scrivere la correzione del codice.

Scrivere la correzione del codice

Un analizzatore può fornire una o più correzioni del codice. Una correzione del codice definisce una modifica in grado di risolvere il problema segnalato. Per l'analizzatore che è stato creato, è possibile fornire una correzione del codice che inserisce la parola chiave const:

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

L'utente la sceglie dall'interfaccia utente della lampadina dell'editor e Visual Studio modifica il codice.

Aprire il file CodeFixResources.resx e passare CodeFixTitle a "Make constant".

Aprire il file MakeConstCodeFixProvider.cs aggiunto dal modello. Questa correzione del codice è già collegata all'ID di diagnostica prodotto dall'analizzatore di diagnostica, ma non implementa ancora la trasformazione del codice corretta.

Eliminare quindi il metodo MakeUppercaseAsync. Tale metodo non è più applicabile.

Tutti i provider di correzione del codice derivano da CodeFixProvider e sostituiscono CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) per segnalare le correzioni del codice disponibili. In RegisterCodeFixesAsync modificare il tipo di nodo predecessore in cui eseguire la ricerca in LocalDeclarationStatementSyntax, in modo che corrisponda alla diagnostica:

var declaration = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<LocalDeclarationStatementSyntax>().First();

Modificare quindi l'ultima riga per registrare una correzione del codice. La correzione creerà un nuovo documento risultante dall'aggiunta del modificatore const a una dichiarazione esistente:

// 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);

Si noteranno delle sottolineature rosse ondulate nel codice appena aggiunto sul simbolo MakeConstAsync. Aggiungere una dichiarazione per MakeConstAsync, come il codice seguente:

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

Il nuovo metodo MakeConstAsync trasforma l'oggetto Document che rappresenta il file di origine dell'utente in un nuovo Document che ora contiene una dichiarazione const.

Creare un nuovo token di parola chiave const da inserire all'inizio dell'istruzione di dichiarazione. Prestare attenzione a rimuovere eventuali elementi semplici iniziali dal primo token dell'istruzione di dichiarazione e associarlo al token const. Aggiungere al metodo MakeConstAsync il codice seguente:

// 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));

Aggiungere quindi il token const alla dichiarazione mediante il codice seguente:

// 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);

Formattare la nuova dichiarazione in modo che corrisponda alle regole di formattazione di C#. La formattazione delle modifiche in modo che corrispondano al codice esistente consente di migliorare l'esperienza. Aggiungere l'istruzione seguente subito dopo il codice esistente:

// Add an annotation to format the new local declaration.
LocalDeclarationStatementSyntax formattedLocal = newLocal.WithAdditionalAnnotations(Formatter.Annotation);

È necessario un nuovo spazio dei nomi per il codice. Aggiungere la direttiva seguente using all'inizio del file:

using Microsoft.CodeAnalysis.Formatting;

Il passaggio finale consiste nell'apportare la modifica. Questo processo comprende tre passaggi:

  1. Ottenere un handle per il documento esistente.
  2. Creare un nuovo documento, sostituendo la dichiarazione esistente con la nuova dichiarazione.
  3. Restituire il nuovo documento.

Aggiungere il codice seguente alla fine del metodo MakeConstAsync:

// Replace the old local declaration with the new local declaration.
SyntaxNode oldRoot = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
SyntaxNode newRoot = oldRoot.ReplaceNode(localDeclaration, formattedLocal);

// Return document with transformed tree.
return document.WithSyntaxRoot(newRoot);

A questo punto, è possibile provare la correzione del codice. Premere F5 per eseguire il progetto analizzatore in una seconda istanza di Visual Studio. Nella seconda istanza di Visual Studio creare un nuovo progetto di applicazione console C# e aggiungere alcune dichiarazioni di variabili locali inizializzate con valori costanti al metodo Main. Si noterà che vengono segnalate come avvisi, come indicato di seguito.

Avvisi relativi alla possibilità di usare una costante

Sono stati compiuti notevoli progressi. Sono presenti linee ondulate sotto le dichiarazioni che possono essere rese const. Ma c'è ancora qualche operazione da eseguire. Questa soluzione funziona se si aggiunge const a dichiarazioni che iniziano con i, quindi con j e infine con k. Tuttavia, se si aggiunge il modificatore const in un ordine diverso, a partire da k, l'analizzatore genera errori: k non può essere dichiarato const, a meno che i e j non siano già entrambi const. È necessario eseguire altre analisi per poter gestire i diversi modi in cui possono essere dichiarate e inizializzate le variabili.

Unit test di compilazione

L'analizzatore e la correzione del codice funzionano nel caso semplice di una singola dichiarazione che può essere resa const. Vi sono numerose possibili istruzioni di dichiarazione in cui questa implementazione genera errori. Questi casi possono essere gestiti usando la libreria di unit test scritta dal modello. È molto più veloce che aprire ripetutamente una seconda copia di Visual Studio.

Aprire il file MakeConstUnitTests.cs nel progetto di unit test. Il modello ha creato due test che seguono i due modelli comuni per gli unit test di un analizzatore e una correzione del codice. TestMethod1 illustra il modello per un test che assicura l'analizzatore non segnali una diagnostica quando non deve. TestMethod2 illustra il modello per la segnalazione di una diagnostica e l'esecuzione della correzione del codice.

Il modello usa pacchetti Microsoft.CodeAnalysis.Testing per unit test.

Suggerimento

La libreria di test supporta una sintassi di markup speciale, inclusa la seguente:

  • [|text|]: indica che viene segnalata una diagnostica per text. Per impostazione predefinita, questo modulo può essere usato solo per gli analizzatori di test con esattamente uno DiagnosticDescriptor fornito da DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: indica che una diagnostica con IdExpectedDiagnosticId viene segnalata per text.

Sostituire i test del modello nella MakeConstUnitTest classe con il metodo di test seguente:

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

Eseguire questo test per assicurarsi che venga superato. In Visual Studio aprire Esplora test selezionando Test>Windows>Esplora test. Selezionare Quindi Esegui tutto.

Creare test per le dichiarazioni valide

Come regola generale, gli analizzatori devono essere chiusi appena possibile ed eseguire attività minime. Visual Studio esegue chiamate agli analizzatori registrati mentre l'utente modifica il codice. La velocità di risposta è un requisito essenziale. Esistono diversi casi di test per il codice che non devono generare la diagnostica. L'analizzatore gestisce già uno di tali test, il caso in cui una variabile viene assegnata dopo l'inizializzazione. Aggiungere il metodo di test seguente per rappresentare tale caso:

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

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

Anche questo test ha esito positivo. Aggiungere quindi metodi di test per le condizioni che non sono ancora stati gestiti:

  • Dichiarazioni che sono già const, perché sono già costanti:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Dichiarazioni senza alcun inizializzatore, perché non è presente alcun valore da usare:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Dichiarazioni in cui l'inizializzatore non è una costante, perché non possono essere costanti in fase di compilazione:

            [TestMethod]
            public async Task InitializerIsNotConstant_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i = DateTime.Now.DayOfYear;
            Console.WriteLine(i);
        }
    }
    ");
            }
    

Può essere ancora più complicato perché C# consente più dichiarazioni come un'unica istruzione. Prendere in considerazione la costante stringa di test case seguente:

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

La variabile i può essere resa costante, ma per la variabile j non è possibile. Questa istruzione non può quindi essere resa una dichiarazione const.

Eseguendo nuovamente i test, si noterà che i nuovi test case hanno esito negativo.

Aggiornare l'analizzatore per ignorare le dichiarazioni corrette

Sono necessari alcuni miglioramenti del metodo AnalyzeNode dell'analizzatore per filtrare le code che soddisfano queste condizioni. Sono tutte condizioni correlate, di conseguenza modifiche simili consentiranno di correggere tutte le condizioni. Modificare AnalyzeNode nel modo seguente:

  • L'analisi semantica ha esaminato una singola dichiarazione di variabile. Questo codice deve essere inserito in un ciclo foreach, che esamina tutte le variabili dichiarate nella stessa istruzione.
  • Ogni variabile dichiarata deve avere un inizializzatore.
  • L'inizializzatore di ogni variabile dichiarata deve essere una costante in fase di compilazione.

Nel metodo AnalyzeNode sostituire l'analisi semantica originale:

// 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;
}

con il frammento di codice seguente:

// 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;
    }
}

Il primo ciclo foreach esamina ogni dichiarazione di variabile tramite l'analisi sintattica. Il primo controllo garantisce che la variabile disponga di un inizializzatore. Il secondo controllo garantisce che l'inizializzatore sia una costante. Il secondo ciclo contiene l'analisi semantica originale. I controlli semantici sono in un ciclo distinto perché ha un maggiore impatto sulle prestazioni. Eseguendo nuovamente i test, dovrebbero tutti avere esito positivo.

Aggiungere le ultime modifiche

La procedura è quasi terminata. Esistono alcune altre condizioni che l'analizzatore deve gestire. Visual Studio esegue chiamate agli analizzatori mentre l'utente scrive il codice. Spesso l'analizzatore viene chiamato per codice che non viene compilato. Il metodo AnalyzeNode dell'analizzatore diagnostico non verifica se il valore costante può essere convertito nel tipo variabile. L'implementazione corrente convertirà quindi una dichiarazione non corretta, ad esempio int i = "abc" in una costante locale. Aggiungere un metodo di test per questo caso:

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

class Program
{
    static void Main()
    {
        int x = {|CS0029:""abc""|};
    }
}
");
        }

Inoltre, i tipi di riferimento non sono gestiti correttamente. L'unico valore costante consentito per un tipo di riferimento è null, tranne nel caso di System.String, che consente valori letterali stringa. In altre parole, const string s = "abc" è consentito, mentre const object s = "abc" non lo è. Tale condizione viene verificata da questo frammento di codice:

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

class Program
{
    static void Main()
    {
        object s = ""abc"";
    }
}
");
        }

Per completezza, è necessario aggiungere un altro test per assicurarsi che sia possibile creare una dichiarazione di costante per una stringa. Il frammento di codice seguente definisce sia il codice che genera il messaggio di diagnostica che il codice dopo l'applicazione della correzione:

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

Infine, se una variabile viene dichiarata con la parola chiave var, la correzione esegue l'operazione errata e genera una dichiarazione const var, che non è supportata dal linguaggio C#. Per correggere questo bug, la correzione del codice deve sostituire la parola chiave var con il nome del tipo dedotto:

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

Fortunatamente, tutti i bug precedenti possono essere corretti con le stesse tecniche appena descritte.

Per correggere il primo bug, aprire prima MakeConstAnalyzer.cs e individuare il ciclo foreach in cui vengono controllati gli inizializzatori della dichiarazione locale per assicurarsi che vengano assegnati con valori costanti. Subito prima del primo ciclo foreach, chiamare context.SemanticModel.GetTypeInfo() per recuperare informazioni dettagliate sul tipo dichiarato della dichiarazione locale:

TypeSyntax variableTypeName = localDeclaration.Declaration.Type;
ITypeSymbol variableType = context.SemanticModel.GetTypeInfo(variableTypeName, context.CancellationToken).ConvertedType;

Quindi, all'interno del ciclo foreach, controllare ogni inizializzatore per assicurarsi che possa essere convertito nel tipo di variabile. Aggiungere il controllo seguente dopo aver verificato che l'inizializzatore sia una costante:

// 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;
}

La modifica successiva si basa su quest'ultima. Prima della parentesi graffa di chiusura del primo ciclo foreach, aggiungere il codice seguente per verificare il tipo di dichiarazione locale quando la costante è una stringa o null.

// Special cases:
//  * If the constant value is a string, the type of the local declaration
//    must be System.String.
//  * If the constant value is null, the type of the local declaration must
//    be a reference type.
if (constantValue.Value is string)
{
    if (variableType.SpecialType != SpecialType.System_String)
    {
        return;
    }
}
else if (variableType.IsReferenceType && constantValue.Value != null)
{
    return;
}

È necessario scrivere un po' di codice nel provider di correzione del codice per sostituire la parola chiave con il var nome del tipo corretto. Tornare a MakeConstCodeFixProvider.cs. Il codice che verrà aggiunto esegue i passaggi seguenti:

  • Controllare se la dichiarazione è una dichiarazione var e in tal caso:
  • Creare un nuovo tipo per il tipo dedotto.
  • Assicurarsi che la dichiarazione del tipo non sia un alias. In tal caso, è consentito dichiarare const var.
  • Verificare che var non sia un nome di tipo in questo programma. In tal caso, const var è consentito.
  • Semplificare il nome completo del tipo

Può sembrare necessaria una notevole quantità di codice, ma non è così. Sostituire la riga che dichiara e inizializza newLocal con il codice seguente. Deve essere inserita subito dopo l'inizializzazione di 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);

È necessario aggiungere una using direttiva per usare il Simplifier tipo:

using Microsoft.CodeAnalysis.Simplification;

Eseguire i test. Avranno tutti esito positivo. Per concludere, è possibile eseguire l'analizzatore completato. Premere CTRL+F5 per eseguire il progetto analizzatore in una seconda istanza di Visual Studio con l'estensione Roslyn Preview caricata.

  • Nella seconda istanza di Visual Studio creare un nuovo progetto di applicazione console C# e aggiungere int x = "abc"; al metodo Main. Grazie alla correzione del primo bug, non dovrebbe essere segnalato alcun messaggio di avviso per questa dichiarazione di variabile locale (anche se si verifica un errore del compilatore come previsto).
  • Aggiungere quindi object s = "abc"; al metodo Main. A causa della correzione del secondo bug, non dovrebbe essere segnalato alcun avviso.
  • Infine, aggiungere un'altra variabile locale che usa la parola chiave var. Si noterà che viene segnalato un avviso e viene visualizzato un suggerimento in basso a sinistra.
  • Spostare l'editor sopra la sottolineatura squiggly e premere CTRL+.. per visualizzare la correzione del codice suggerita. Dopo aver selezionato la correzione del codice, tenere presente che la var parola chiave è ora gestita correttamente.

Infine, aggiungere il codice seguente:

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

Dopo queste modifiche, vengono visualizzate linee rosse ondulate solo per le prime due variabili. Aggiungere const sia a i che a j. Verrà visualizzato un nuovo avviso per k, perché può ora essere const.

Congratulazioni! È stata creata la prima estensione .NET Compiler Platform che esegue l'analisi del codice in tempo reale per rilevare un problema e fornisce una correzione rapida per l'errore. Nel corso di questo processo sono state descritte molte delle API di codice che fanno parte di .NET Compiler Platform SDK (API Roslyn). È possibile confrontare il proprio lavoro con l'esempio completato disponibile nel repository GitHub degli esempi.

Altre risorse