Dela via


Handledning: Skriva ditt första analysverktyg och korrigera kod

.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 din regel. Din kodkorrigering innehåller den kod som åtgärdar överträdelsen. Reglerna 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 squiggles 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 en analysator och en tillhörande kodkorrigering med hjälp av Roslyn-API:erna. En analysator är ett sätt att utföra källkodsanalys och rapportera ett problem till användaren. Du kan också associera en kodkorrigering med analysatorn för att representera en ändring av användarens källkod. Den här självstudien skapar en analysator som hittar lokala variabeldeklarationer som kan deklareras med hjälp av const modifieraren men som inte är det. Den tillhörande kodkorrigeringen ändrar dessa deklarationer för att lägga const till modifieraren.

Förutsättningar

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

Installationsinstruktioner – Visual Studio Installer

Det finns två olika sätt att hitta .NET Compiler Platform SDK i Visual Studio Installer:

Installera med vyn Arbetsbelastningar i Visual Studio Installer

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

  1. Starta Visual Studio Installer
  2. Välj Ändra
  3. Kontrollera arbetsuppgiften Visual Studio-tilläggsutveckling.
  4. Öppna noden Visual Studio-tilläggsutveckling i översiktsträdet.
  5. Markera kryssrutan för .NET Compiler Platform SDK. Du hittar den sist under de valfria komponenterna.

Du vill också att DGML-redigeraren ska visa 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. Starta Visual Studio Installer
  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 under avsnittet Kompilatorer, byggverktyg och körning.

Du vill också att DGML-redigeraren ska visa 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. Rapportanalyzerns varningar och rekommendationer.
  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 Analysera med kodkorrigering (.NET Standard).
  • Ge projektet namnet "MakeConst" och klicka på OK.

Anmärkning

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

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-paketet 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 den nya analysatorn. Tryck på F5 för att starta VSIX-projektet.

Anmärkning

Analysverktyg 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ön (Visual Studio).

Tips/Råd

När du kör analysatorn startar du en andra kopia av Visual Studio. Den 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. Använd inte heller dina inställningar eller logga in på ditt Visual Studio-konto med hjälp av den experimentella körningen av Visual Studio. Det håller inställningarna olika.

Hive innehåller inte bara analysatorn som är under utveckling, utan även alla analysatorer som har öppnats tidigare. Om du vill återställa Roslyn Hive måste du ta bort den manuellt 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 bygga om den efter att du har raderat filen.

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 och varningstexten som tillhandahålls av en analysator visas.

Mallen skapar en analysator som rapporterar en varning för varje typdeklaration där typnamnet innehåller små bokstäver, som visas i bilden nedan.

Rapporteringsvarning för Analyzer

Mallen innehåller också en kodkorrigering som ändrar alla typnamn som innehåller gemener till alla 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. 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 är mycket snabbare än att testa dem interaktivt med Visual Studio.

Tips/Råd

Analysenhetstester är ett bra verktyg när du vet vilka kodkonstruktioner som ska och inte ska utlösa analysatorn. 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 en analysator som rapporterar till användaren eventuella lokala variabeldeklarationer som kan konverteras till lokala konstanter. Tänk till exempel på 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, som kräver syntaktisk analys, konstant analys av initialiseraruttrycket 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 första DiagnosticAnalyzer klassen i filen MakeConstAnalyzer.cs . Den här första analysatorn visar två viktiga egenskaper för varje analysator.

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

Mallen visar också de grundläggande funktionerna 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 analysatorns registrerade metod.
  2. Skapa diagnostik. När analysatorn upptäcker 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 din åsidosättning 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 konstanterna för registreringen och Initialize-metoden så att de visar din analysator "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 agerar på syntaxen. MakeConstAnalyzerAnalyzer.Initialize I -metoden hittar du 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 analyseraren undersöker SyntaxKind.LocalDeclarationStatement, inte SymbolKind.NamedType-uttalanden. Observera att under AnalyzeNode finns det röda krumelurer. Koden som du nyss 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 Usage i MakeConstAnalyzer.cs enligt den följande koden:"

private const string Category = "Usage";

Hitta lokala deklarationer som kan vara konstanta

Det är dags att skriva den första versionen av AnalyzeNode metoden. Den bör leta efter en enda lokal deklaration som skulle kunna vara const men inte är det, likt 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 casten 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, returnera 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 om 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 precis har lagts till säkerställer att variabeln inte ändras och därför kan göras const. Det är dags att förbättra 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 följande:

  • Glödlampan, som fortfarande använder den mallgenererade kodkorrigeringen, kommer att indikera att koden kan ändras till versaler.
  • Ett banderollmeddelande överst i redigeraren med texten "MakeConstCodeFixProvider" påträffade ett fel och har inaktiverats. Detta 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.

Utför kodkorrigeringen

En analysator 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 den MakeConstCodeFixProvider.cs fil som lagts till av mallen. Den här kodkorrigeringen är redan kopplad till diagnostik-ID:t som skapas 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 alla åsidosätter CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext) för att rapportera tillgängliga kodfixar. I RegisterCodeFixesAsyncändrar du den överordnade nodtyp 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 att modifieraren läggs till i const 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 markeringar i koden som du precis lade till på symbolen MakeConstAsync. Lägg till en deklaration för MakeConstAsync som 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 deklarationen och bifoga det till const-tokenen. Lägg till följande kod i metoden 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));

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#-formateringsregler. Om du formaterar ändringarna så att de matchar befintlig kod skapas 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 göra din redigering. Det finns tre steg i den här processen:

  1. Hämta en referens 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);

Din kodkorrigering är redo att testas. 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 orsaka const-varningar

Du har gjort många framsteg. Det finns krumelurer 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 skapar kanalysatorn 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

Din analys och kodkorrigering fungerar på ett enkelt fall av en enda deklaration som kan göras const. Det finns många möjliga förklaringsförklaringar där den här implementeringen gör misstag. Du kommer att åtgärda dessa fall genom att arbeta med enhetstestbiblioteket som skapats 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 kodkorrigeringsenhetstest. 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/Råd

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 en 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. Öppna Testutforskaren i Visual Studio 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 analysatorer avslutas så snabbt som möjligt, och utföra minimalt arbete. Visual Studio anropar registrerade analysverktyg när användaren redigerar kod. Svarstid är ett viktigt krav. Det finns flera testfall för kod som inte bör utlösa din diagnostik. Analysatorn hanterar redan flera av dessa tester. Lägg till följande testmetoder för att representera dessa fall:

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

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

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

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

Dessa tester godkänns eftersom analysatorn redan hanterar följande villkor:

  • Variabler som tilldelats efter initieringen identifieras av dataflödesanalys.
  • Deklarationer som redan har const filtreras bort genom att söka efter nyckelordet const.
  • Deklarationer utan initierare hanteras av dataflödesanalysen som identifierar tilldelningar utanför deklarationen.

Lägg sedan till testmetoder för villkor som du inte har hanterat ännu:

  • Deklarationer där initieraren inte är en konstant, eftersom de inte kan vara kompileringstidskonstanter.

            [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. Det går därför inte att göra en const-deklaration.

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

Uppdatera din analysator för att bortse från 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 löser 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 initialiserare 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 initierare. 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 större inverkan på prestanda. Kör testerna igen och du bör se att alla godkänns.

Lägg till den sista 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 analysatorn anropas för kod som inte kompileras. Diagnostikanalysatorns metod kontrollerar inte om konstantvärdet kan 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. Det är med andra ord const string s = "abc" lagligt, men const object s = "abc" det är det 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 C#-språket. För att åtgärda felet måste kodkorrigeringen ersätta nyckelordet var med den infererade 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 den första buggen genom att först öppna MakeConstAnalyzer.cs och leta upp foreach-loopen där var och en av den lokala deklarationens initialiserare kontrolleras för att säkerställa att de tilldelas med 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 den lokala deklarationen:

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 är konvertibel till variabeltypen. Lägg till följande kontroll när du har kontrollerat att initiatorn ä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 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 det 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 typen Simplifier :

using Microsoft.CodeAnalysis.Simplification;

Kör dina tester, och de bör alla passera. 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 Roslyn Preview-tillägget 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 ska 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 redigeringsmarkören ö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 krumelurer på de två första variablerna. Lägg till const i både i och j, och du får en ny varning på k eftersom det nu kan vara const.

Grattis! Du har skapat ditt första .NET Compiler Platform-tillägg som utför kodanalys direkt 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 kod-API:erna som ingår i .NET Compiler Platform SDK (Roslyn API:er). Du kan kontrollera ditt arbete mot det slutförda exemplet på github-lagringsplatsen för exempel.

Andra resurser