Självstudie: Skriva din första analysverktyg och kodkorrigering

.NET Compiler Platform SDK innehåller de verktyg du behöver för att skapa anpassad diagnostik (analysverktyg), kodkorrigeringar, kodrefaktorisering och diagnostiska suppressorer som riktar sig mot C# eller Visual Basic-kod. En analysator innehåller kod som identifierar överträdelser av regeln. Din kodkorrigering innehåller den kod som åtgärdar överträdelsen. De regler som du implementerar kan vara allt från kodstruktur till kodningsformat till namngivningskonventioner med mera. .NET Compiler Platform tillhandahåller ramverket för att köra analys när utvecklare skriver kod och alla Visual Studio UI-funktioner för att åtgärda kod: visa växlingsknappar i redigeraren, fylla i Visual Studio-fellistan, skapa "glödlampa"-förslagen och visa den omfattande förhandsversionen av de föreslagna korrigeringarna.

I den här självstudien utforskar du skapandet av ett analysverktyg och en tillhörande kodkorrigering med hjälp av Roslyn-API:erna. En analysverktyg är ett sätt att utföra källkodsanalys och rapportera ett problem till användaren. Om du vill kan en kodkorrigering associeras med analysatorn för att representera en ändring av användarens källkod. I den const här självstudien skapas ett analysverktyg som hittar lokala variabeldeklarationer som kan deklareras med hjälp av modifieraren, men som inte är det. Den tillhörande kodkorrigeringen ändrar dessa deklarationer för att lägga const till modifieraren.

Krav

Du måste installera .NET Compiler Platform SDK via Visual Studio Installer:

Installationsanvisningar – Installationsprogram för Visual Studio

Det finns två olika sätt att hitta .NET Compiler Platform SDK i Installationsprogrammet för Visual Studio:

Installera med hjälp av vyn Installationsprogram för Visual Studio – arbetsbelastningar

.NET Compiler Platform SDK väljs inte automatiskt som en del av arbetsbelastningen för Utveckling av Visual Studio-tillägg. Du måste välja den som en valfri komponent.

  1. Kör Installationsprogrammet för Visual Studio
  2. Välj Ändra
  3. Kontrollera arbetsbelastningen för Utveckling av Visual Studio-tillägg .
  4. Öppna noden för Visual Studio-tilläggsutveckling i sammanfattningsträdet.
  5. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den sist under de valfria komponenterna.

Du kan också vilja att DGML-redigeraren visar grafer i visualiseraren:

  1. Öppna noden Enskilda komponenter i sammanfattningsträdet.
  2. Markera kryssrutan för DGML-redigeraren

Installera med hjälp av fliken Installationsprogram för Visual Studio – enskilda komponenter

  1. Kör Installationsprogrammet för Visual Studio
  2. Välj Ändra
  3. Välj fliken Enskilda komponenter
  4. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den längst upp i avsnittet Kompilatorer, byggverktyg och körningsmiljöer .

Du kan också vilja att DGML-redigeraren visar grafer i visualiseraren:

  1. Markera kryssrutan för DGML-redigeraren. Du hittar den under avsnittet Kodverktyg .

Det finns flera steg för att skapa och verifiera analysatorn:

  1. Skapa lösningen.
  2. Registrera analysatorns namn och beskrivning.
  3. Varningar och rekommendationer för rapportanalys.
  4. Implementera kodkorrigeringen för att acceptera rekommendationer.
  5. Förbättra analysen genom enhetstester.

Skapa lösningen

  • I Visual Studio väljer du Arkiv > Nytt > projekt... för att visa dialogrutan Nytt projekt.
  • Under Visual C# > Utökningsbarhet väljer du Analyserare med kodkorrigering (.NET Standard).
  • Ge projektet namnet "MakeConst" och klicka på OK.

Anteckning

Du kan få ett kompileringsfel (MSB4062: Uppgiften "CompareBuildTaskVersion" kunde inte läsas in). Du kan åtgärda detta genom att uppdatera NuGet-paketen i lösningen med NuGet Package Manager eller använda Update-Package i package manager-konsolfönstret.

Utforska analysmallen

Analysatorn med kodkorrigeringsmallen skapar fem projekt:

  • MakeConst, som innehåller analysatorn.
  • MakeConst.CodeFixes, som innehåller kodkorrigeringen.
  • MakeConst.Package, som används för att skapa NuGet-paket för analysatorn och kodkorrigeringen.
  • MakeConst.Test, som är ett enhetstestprojekt.
  • MakeConst.Vsix, som är standardstartprojektet som startar en andra instans av Visual Studio som har läst in ditt nya analysverktyg. Tryck på F5 för att starta VSIX-projektet.

Anteckning

Analysverktygen bör rikta in sig på .NET Standard 2.0 eftersom de kan köras i .NET Core-miljön (kommandoradsversioner) och .NET Framework miljö (Visual Studio).

Tips

När du kör analysatorn startar du en andra kopia av Visual Studio. Den här andra kopian använder en annan registreringsdatafil för att lagra inställningar. Det gör att du kan särskilja de visuella inställningarna i de två kopiorna av Visual Studio. Du kan välja ett annat tema för den experimentella körningen av Visual Studio. Dessutom ska du inte använda den experimentella körningen av Visual Studio för att flytta dina inställningar eller logga in på ditt Visual Studio-konto. Det håller inställningarna annorlunda.

Registreringsdatafilen innehåller inte bara analysatorn under utveckling, utan även alla tidigare analysverktyg som öppnats. Om du vill återställa Roslyn Hive måste du manuellt ta bort den från %LocalAppData%\Microsoft\VisualStudio. Mappnamnet för Roslyn hive slutar i Roslyn, till exempel 16.0_9ae182f9Roslyn. Observera att du kan behöva rensa lösningen och återskapa den när du har raderat registreringsdatafilen.

I den andra Visual Studio-instansen som du precis har startat skapar du ett nytt C#-konsolprogramprojekt (alla målramverk fungerar – analysverktyg fungerar på källnivå.) Hovra över token med en vågig understrykning så visas varningstexten från ett analysverktyg.

Mallen skapar ett analysverktyg som rapporterar en varning för varje typdeklaration där typnamnet innehåller gemener, enligt följande bild:

Analysrapporteringsvarning

Mallen innehåller också en kodkorrigering som ändrar alla typnamn som innehåller gemener till versaler. Du kan klicka på glödlampan som visas med varningen för att se de föreslagna ändringarna. Om du godkänner de föreslagna ändringarna uppdateras typnamnet och alla referenser till den typen i lösningen. Nu när du har sett den första analysatorn i praktiken stänger du den andra Visual Studio-instansen och återgår till analysprojektet.

Du behöver inte starta en andra kopia av Visual Studio och skapa ny kod för att testa varje ändring i analysatorn. Mallen skapar också ett enhetstestprojekt åt dig. Det projektet innehåller två tester. TestMethod1 visar det typiska formatet för ett test som analyserar kod utan att utlösa en diagnostik. TestMethod2 visar formatet för ett test som utlöser en diagnostik och sedan tillämpar en föreslagen kodkorrigering. När du skapar analysatorn och kodkorrigeringen skriver du tester för olika kodstrukturer för att verifiera ditt arbete. Enhetstester för analysverktyg går mycket snabbare än att testa dem interaktivt med Visual Studio.

Tips

Analysenhetstester är ett bra verktyg när du vet vilka kodkonstruktioner som ska och inte ska utlösa analysverktyget. Att läsa in analysverktyget i en annan kopia av Visual Studio är ett bra verktyg för att utforska och hitta konstruktioner som du kanske inte har tänkt på ännu.

I den här självstudien skriver du ett analysverktyg som rapporterar till användaren eventuella lokala variabeldeklarationer som kan konverteras till lokala konstanter. Tänk dig till exempel följande kod:

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

I koden ovan x tilldelas ett konstant värde och ändras aldrig. Den kan deklareras med hjälp av const modifieraren:

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

Analysen för att avgöra om en variabel kan göras konstant är involverad, kräver syntaktisk analys, konstant analys av initieringsuttrycket och dataflödesanalys för att säkerställa att variabeln aldrig skrivs till. .NET Compiler Platform tillhandahåller API:er som gör det enklare att utföra den här analysen.

Skapa analysregistreringar

Mallen skapar den inledande DiagnosticAnalyzer klassen i filen MakeConstAnalyzer.cs . Den här inledande analysatorn visar två viktiga egenskaper för varje analysator.

  • Varje diagnostisk analysverktyg måste ange ett [DiagnosticAnalyzer] attribut som beskriver det språk som det fungerar på.
  • Varje diagnostisk analysverktyg måste härleda (direkt eller indirekt) från DiagnosticAnalyzer klassen.

Mallen visar också de grundläggande funktioner som ingår i alla analysverktyg:

  1. Registrera åtgärder. Åtgärderna representerar kodändringar som ska utlösa analysatorn för att undersöka kod för överträdelser. När Visual Studio identifierar kodredigeringar som matchar en registrerad åtgärd anropas analysverktygets registrerade metod.
  2. Skapa diagnostik. När analysverktyget identifierar en överträdelse skapas ett diagnostikobjekt som Visual Studio använder för att meddela användaren om överträdelsen.

Du registrerar åtgärder i åsidosättningen av DiagnosticAnalyzer.Initialize(AnalysisContext) metoden. I den här självstudien går du till syntaxnoder som letar efter lokala deklarationer och ser vilka av dessa som har konstanta värden. Om en deklaration kan vara konstant skapar och rapporterar analysatorn en diagnostik.

Det första steget är att uppdatera registreringskonstanterna och Initialize -metoden så att dessa konstanter anger analysatorn "Make Const". De flesta strängkonstanterna definieras i strängresursfilen. Du bör följa den metoden för enklare lokalisering. Öppna filen Resources.resx för MakeConst-analysprojektet . Då visas resursredigeraren. Uppdatera strängresurserna på följande sätt:

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

När du är klar bör resursredigeraren visas enligt följande bild:

Uppdatera strängresurser

De återstående ändringarna finns i analysfilen. Öppna MakeConstAnalyzer.cs i Visual Studio. Ändra den registrerade åtgärden från en som agerar på symboler till en som fungerar med syntax. MakeConstAnalyzerAnalyzer.Initialize I metoden letar du upp raden som registrerar åtgärden för symboler:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Ersätt den med följande rad:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Efter den ändringen kan du ta bort AnalyzeSymbol metoden. Den här analysatorn undersöker SyntaxKind.LocalDeclarationStatement, inte SymbolKind.NamedType -instruktioner. Observera att AnalyzeNode har röda squiggles under den. Koden som du precis lade till refererar till en AnalyzeNode metod som inte har deklarerats. Deklarera den metoden med hjälp av följande kod:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Category Ändra till iUsageMakeConstAnalyzer.cs enligt följande kod:

private const string Category = "Usage";

Hitta lokala deklarationer som kan vara nackdelar

Det är dags att skriva den första versionen av AnalyzeNode metoden. Den bör leta efter en enda lokal deklaration som kan vara const men inte är det, som i följande kod:

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

Det första steget är att hitta lokala deklarationer. Lägg till följande kod AnalyzeNode i i MakeConstAnalyzer.cs:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Den här rollbesättningen lyckas alltid eftersom analysatorn har registrerats för ändringar i lokala deklarationer och endast lokala deklarationer. Ingen annan nodtyp utlöser ett anrop till din AnalyzeNode metod. Kontrollera sedan deklarationen för eventuella const modifierare. Om du hittar dem, gå tillbaka omedelbart. Följande kod söker efter eventuella const modifierare i den lokala deklarationen:

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

Slutligen måste du kontrollera att variabeln kan vara const. Det innebär att se till att den aldrig tilldelas när den har initierats.

Du utför viss semantisk analys med hjälp av SyntaxNodeAnalysisContext. Du använder context argumentet för att avgöra om den lokala variabeldeklarationen kan göras const. A Microsoft.CodeAnalysis.SemanticModel representerar all semantisk information i en enda källfil. Du kan läsa mer i artikeln som beskriver semantiska modeller. Du använder Microsoft.CodeAnalysis.SemanticModel för att utföra dataflödesanalys på den lokala deklarationssatsen. Sedan använder du resultatet av den här dataflödesanalysen för att säkerställa att den lokala variabeln inte skrivs med ett nytt värde någon annanstans. GetDeclaredSymbol Anropa tilläggsmetoden för att hämta ILocalSymbol för variabeln och kontrollera att den inte ingår i insamlingen DataFlowAnalysis.WrittenOutside av dataflödesanalysen. Lägg till följande kod i slutet av AnalyzeNode metoden:

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

Koden som just har lagts till säkerställer att variabeln inte ändras och därför kan göras const. Det är dags att höja diagnostiken. Lägg till följande kod som den sista raden i AnalyzeNode:

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

Du kan kontrollera förloppet genom att trycka på F5 för att köra analysatorn. Du kan läsa in konsolprogrammet som du skapade tidigare och sedan lägga till följande testkod:

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

Glödlampan ska visas och analysatorn bör rapportera en diagnostik. Beroende på din version av Visual Studio ser du dock antingen:

  • Glödlampan, som fortfarande använder den mallgenererade kodkorrigeringen, kommer att berätta att den kan göras versaler.
  • Ett banderollmeddelande överst i redigeraren med texten "MakeConstCodeFixProvider" påträffade ett fel och har inaktiverats. Det beror på att kodkorrigeringsprovidern ännu inte har ändrats och fortfarande förväntar sig att hitta TypeDeclarationSyntax element i stället för LocalDeclarationStatementSyntax element.

I nästa avsnitt beskrivs hur du skriver kodkorrigeringen.

Skriva kodkorrigeringen

En analys kan tillhandahålla en eller flera kodkorrigeringar. En kodkorrigering definierar en redigering som åtgärdar det rapporterade problemet. För analysatorn som du skapade kan du ange en kodkorrigering som infogar nyckelordet const:

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

Användaren väljer det från glödlampans användargränssnitt i redigeraren och Visual Studio ändrar koden.

Öppna filen CodeFixResources.resx och ändra CodeFixTitle till "Make constant".

Öppna filen MakeConstCodeFixProvider.cs som lagts till av mallen. Den här kodkorrigeringen är redan kopplad till diagnostik-ID:t som genereras av diagnostikanalysen, men den implementerar ännu inte rätt kodtransformering.

Ta sedan bort MakeUppercaseAsync metoden. Det gäller inte längre.

Alla kodkorrigeringsprovidrar härleds från CodeFixProvider. De åsidosätter CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) alla för att rapportera tillgängliga kodkorrigeringar. I RegisterCodeFixesAsyncändrar du den överordnade nodtypen som du söker efter till en LocalDeclarationStatementSyntax för att matcha diagnostiken:

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

Ändra sedan den sista raden för att registrera en kodkorrigering. Korrigeringen skapar ett nytt dokument som resulterar i const att modifieraren läggs till i en befintlig deklaration:

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

Du ser röda växlingsknappar i koden som du precis lade till på symbolen MakeConstAsync. Lägg till en deklaration som MakeConstAsync liknar följande kod:

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

Den nya MakeConstAsync metoden omvandlar Document den som representerar användarens källfil till en ny Document som nu innehåller en const deklaration.

Du skapar en ny const nyckelordstoken som ska infogas längst fram i deklarationssatsen. Var noga med att först ta bort eventuella inledande trivia från den första token i deklarationssatsen och koppla den till const token. Lägg till följande kod i MakeConstAsync-metoden:

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

Lägg sedan till token i const deklarationen med hjälp av följande kod:

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

Formatera sedan den nya deklarationen så att den matchar C#-formateringsreglerna. Om du formaterar ändringarna så att de matchar befintlig kod får du en bättre upplevelse. Lägg till följande instruktion omedelbart efter den befintliga koden:

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

Ett nytt namnområde krävs för den här koden. Lägg till följande using direktiv överst i filen:

using Microsoft.CodeAnalysis.Formatting;

Det sista steget är att redigera. Det finns tre steg i den här processen:

  1. Hämta ett handtag till det befintliga dokumentet.
  2. Skapa ett nytt dokument genom att ersätta den befintliga deklarationen med den nya deklarationen.
  3. Returnera det nya dokumentet.

Lägg till följande kod i slutet av MakeConstAsync metoden:

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

Kodkorrigeringen är redo att provas. Tryck på F5 för att köra analysprojektet i en andra instans av Visual Studio. I den andra Visual Studio-instansen skapar du ett nytt C#-konsolprogramprojekt och lägger till några lokala variabeldeklarationer som initierats med konstanta värden i Main-metoden. Du ser att de rapporteras som varningar enligt nedan.

Kan göra const-varningar

Du har gjort många framsteg. Det finns squiggles under de deklarationer som kan göras const. Men det finns fortfarande arbete att göra. Detta fungerar bra om du lägger till const i deklarationerna från och med i, och j slutligen k. Men om du lägger till const modifieraren i en annan ordning, från och med k, skapar analysatorn fel: k kan inte deklareras const, såvida inte i och j är båda redan const. Du måste göra mer analys för att se till att du hanterar de olika sätt som variabler kan deklareras och initieras på.

Skapa enhetstester

Analysatorn och kodkorrigeringen fungerar på ett enkelt fall av en enda deklaration som kan göras const. Det finns många möjliga förklaringsinstruktioner där den här implementeringen gör misstag. Du kommer att åtgärda dessa fall genom att arbeta med enhetstestbiblioteket som skrivits av mallen. Det går mycket snabbare än att öppna en andra kopia av Visual Studio upprepade gånger.

Öppna filen MakeConstUnitTests.cs i enhetstestprojektet. Mallen skapade två tester som följer de två vanliga mönstren för ett analysverktyg och enhetstest för kodkorrigering. TestMethod1 visar mönstret för ett test som säkerställer att analysatorn inte rapporterar någon diagnostik när den inte borde det. TestMethod2 visar mönstret för att rapportera en diagnostik och köra kodkorrigeringen.

Mallen använder Microsoft.CodeAnalysis.Testing-paket för enhetstestning.

Tips

Testbiblioteket stöder en särskild markeringssyntax, inklusive följande:

  • [|text|]: anger att en diagnostik rapporteras för text. Som standard kan det här formuläret endast användas för att testa analysverktyg med exakt ett DiagnosticDescriptor som tillhandahålls av DiagnosticAnalyzer.SupportedDiagnostics.
  • {|ExpectedDiagnosticId:text|}: anger att en diagnostik med IdExpectedDiagnosticId rapporteras för text.

Ersätt malltesterna MakeConstUnitTest i klassen med följande testmetod:

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

Kör det här testet för att se till att det godkänns. I Visual Studio öppnar du Test Explorer genom att välja Testa>Windows>Test Explorer. Välj sedan Kör alla.

Skapa tester för giltiga deklarationer

Som en allmän regel bör analysverktyg avslutas så snabbt som möjligt och utföra minimalt arbete. Visual Studio anropar registrerade analysverktyg när användaren redigerar kod. Svarstider är ett viktigt krav. Det finns flera testfall för kod som inte bör generera diagnostiken. Analysatorn hanterar redan ett av dessa tester, det fall där en variabel tilldelas efter att ha initierats. Lägg till följande testmetod för att representera det fallet:

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

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

Det här testet godkänns också. Lägg sedan till testmetoder för villkor som du inte har hanterat ännu:

  • Deklarationer som redan constär , eftersom de redan är nackdelar:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklarationer som inte har någon initiering eftersom det inte finns något värde att använda:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklarationer där initiatorn inte är en konstant, eftersom de inte kan kompilera tidskonstanter:

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

Det kan vara ännu mer komplicerat eftersom C# tillåter flera deklarationer som en instruktion. Överväg följande testfallssträngskonstant:

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

Variabeln i kan göras konstant, men variabeln j kan inte göra det. Detta påstående kan därför inte göras i en beständig förklaring.

Kör testerna igen så ser du att de nya testfallen misslyckas.

Uppdatera analysatorn för att ignorera korrekta deklarationer

Du behöver vissa förbättringar av analysatorns AnalyzeNode metod för att filtrera bort kod som matchar dessa villkor. De är alla relaterade villkor, så liknande ändringar kommer att åtgärda alla dessa villkor. Gör följande ändringar AnalyzeNodei :

  • Din semantiska analys undersökte en enda variabeldeklaration. Den här koden måste finnas i en foreach loop som undersöker alla variabler som deklareras i samma -instruktion.
  • Varje deklarerad variabel måste ha en initierare.
  • Varje deklarerad variabels initiator måste vara en kompileringskonstant.

Ersätt den ursprungliga semantiska analysen i din AnalyzeNode -metod:

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

med följande kodfragment:

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

Den första foreach loopen undersöker varje variabeldeklaration med hjälp av syntaktisk analys. Den första kontrollen garanterar att variabeln har en initiering. Den andra kontrollen garanterar att initieraren är en konstant. Den andra loopen har den ursprungliga semantiska analysen. De semantiska kontrollerna finns i en separat loop eftersom det har en större inverkan på prestanda. Kör testerna igen så bör du se att alla godkänns.

Lägg till den slutliga poleringen

Nästan klart. Det finns några fler villkor som analysatorn kan hantera. Visual Studio anropar analysverktyg medan användaren skriver kod. Det är ofta så att analysverktyget anropas för kod som inte kompileras. Diagnostikanalysmetoden kontrollerar inte om konstantvärdet konverteras AnalyzeNode till variabeltypen. Den aktuella implementeringen konverterar därför gärna en felaktig deklaration, till exempel int i = "abc" till en lokal konstant. Lägg till en testmetod för det här fallet:

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

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

Dessutom hanteras inte referenstyper korrekt. Det enda konstanta värdet som tillåts för en referenstyp är null, förutom i fallet System.Stringmed , som tillåter strängliteraler. Med andra ord, const string s = "abc" är lagligt, men const object s = "abc" är inte. Det här kodfragmentet verifierar det villkoret:

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

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

För att vara noggrann måste du lägga till ytterligare ett test för att se till att du kan skapa en konstant deklaration för en sträng. Följande kodfragment definierar både koden som genererar diagnostiken och koden efter att korrigeringen har tillämpats:

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

Om en variabel deklareras med nyckelordet var gör kodkorrigeringen slutligen fel sak och genererar en const var deklaration som inte stöds av språket C#. För att åtgärda felet måste kodkorrigeringen ersätta nyckelordet var med den härledda typens namn:

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

Lyckligtvis kan alla ovanstående buggar åtgärdas med samma tekniker som du just har lärt dig.

Åtgärda det första felet genom att först öppna MakeConstAnalyzer.cs och leta upp foreach-loopen där var och en av den lokala deklarationens initierare kontrolleras för att säkerställa att de tilldelas konstanta värden. Omedelbart före den första foreach-loopen anropar du context.SemanticModel.GetTypeInfo() för att hämta detaljerad information om den deklarerade typen av lokal deklaration:

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

I loopen foreach kontrollerar du sedan varje initierare för att se till att den konverteras till variabeltypen. Lägg till följande kontroll när du har säkerställt att initieraren är en konstant:

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

Nästa ändring bygger på den sista. Innan du stänger klammerparentesen för den första foreach-loopen lägger du till följande kod för att kontrollera typen av lokal deklaration när konstanten är en sträng eller 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;
}

Du måste skriva lite mer kod i kodkorrigeringsprovidern för att ersätta nyckelordet var med rätt typnamn. Gå tillbaka till MakeConstCodeFixProvider.cs. Koden som du lägger till utför följande steg:

  • Kontrollera om deklarationen är en var deklaration och om den är:
  • Skapa en ny typ för den härledda typen.
  • Kontrollera att typdeklarationen inte är ett alias. I så fall är det lagligt att deklarera const var.
  • Kontrollera att var inte är ett typnamn i det här programmet. (I så fall const var är det lagligt).
  • Förenkla det fullständiga typnamnet

Det låter som mycket kod. Det är det inte. Ersätt raden som deklarerar och initierar newLocal med följande kod. Det går omedelbart efter initieringen av 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);

Du måste lägga till ett using direktiv för att använda Simplifier typen :

using Microsoft.CodeAnalysis.Simplification;

Kör dina tester, så borde alla klara sig. Gratulera dig själv genom att köra din färdiga analysator. Tryck på Ctrl+F5 för att köra analysprojektet i en andra instans av Visual Studio med tillägget Roslyn Preview inläst.

  • I den andra Visual Studio-instansen skapar du ett nytt C#-konsolprogramprojekt och lägger till int x = "abc"; i Main-metoden. Tack vare den första felkorrigeringen bör ingen varning rapporteras för den här lokala variabeldeklarationen (även om det finns ett kompilatorfel som förväntat).
  • Lägg sedan till object s = "abc"; i main-metoden. På grund av den andra felkorrigeringen ska ingen varning rapporteras.
  • Lägg slutligen till en annan lokal variabel som använder nyckelordet var . Du ser att en varning rapporteras och ett förslag visas under till vänster.
  • Flytta redigeraren över den vågiga understrykningen och tryck på Ctrl+. för att visa den föreslagna kodkorrigeringen. Observera att nyckelordet var nu hanteras korrekt när du väljer din kodkorrigering.

Lägg slutligen till följande kod:

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

Efter dessa ändringar får du bara röda växlingar på de två första variablerna. Lägg till const i både i och j, så får du en ny varning eftersom k den nu kan vara const.

Grattis! Du har skapat ditt första .NET Compiler Platform-tillägg som utför on-the-fly-kodanalys för att identifiera ett problem och ger en snabbkorrigering för att korrigera det. Längs vägen har du lärt dig många av de kod-API:er som ingår i .NET Compiler Platform SDK (Roslyn-API:er). Du kan kontrollera ditt arbete mot det slutförda exemplet i github-lagringsplatsen för exempel.

Andra resurser