Tutorial: Schreiben Ihres ersten Analysetools und Codefixes

Das .NET Compiler Platform SDK bietet die Tools, die Sie benötigen, um benutzerdefinierte Diagnosefunktionen (Analysetools), Codekorrekturen, Coderefactoring und Diagnoseunterdrückungsfunktionen zu erstellen, die auf C#- oder Visual Basic-Code abzielen. Ein Analysetool enthält Code, der Verstöße gegen Ihre Regel erkennt. Ihr Codefix enthält den Code, der den Verstoß behebt. Die Regeln, die Sie implementieren, können sich auf alles Mögliche beziehen, von der Codestruktur über das Codeformat bis zu Benennungskonventionen usw. Die .NET Compiler Platform stellt das Framework zum Ausführen der Analyse bereit, während die Entwickler Code schreiben, mit allen Features der Visual Studio-Benutzeroberfläche für das Beheben von Codeproblemen: Anzeigen von Wellenlinien im Editor, Auffüllen der Fehlerliste von Visual Studio, Erstellen der "Glühbirnen"-Vorschläge und Darstellung der umfassenden Vorschau vorgeschlagener Fixe.

In diesem Tutorial lernen Sie die Erstellung eines Analysetools und eines begleitenden Codefixes unter Verwendung der Roslyn-APIs kennen. Ein Analysetool ist eine Möglichkeit, Quellcodeanalyse auszuführen und dem Benutzer ein Problem zu melden. Optional kann eine Codekorrektur mit dem Analysetool verknüpft werden, um eine Änderung am Quellcode des Benutzers darzustellen. In diesem Tutorial wird ein Analysetool erstellt, das Deklarationen von lokalen Variablen enthält, die mithilfe des const-Modifizierers deklariert werden könnten, es aber nicht sind. Der begleitende Codefix ändert diese Deklarationen, indem er den const-Modifizierer hinzufügt.

Voraussetzungen

Installieren Sie zunächst über den Visual Studio-Installer das SDK für die .NET Compiler Platform:

Installationsanweisungen: Visual Studio-Installer

Es gibt zwei verschiedene Möglichkeiten, das .NET Compiler Platform SDK im Visual Studio-Installer zu finden:

Installation mithilfe des Visual Studio-Installers: Workloads im Überblick

Das .NET Compiler Platform SDK wird nicht automatisch als Teil der Workload „Visual Studio-Extensionentwicklung“ ausgewählt. Sie müssen sie als optionale Komponente auswählen.

  1. Führen Sie den Visual Studio-Installer aus.
  2. Wählen Sie Ändern aus.
  3. Aktivieren Sie die Workload Visual Studio-Extensionentwicklung.
  4. Öffnen Sie den Knoten Visual Studio-Extensionentwicklung in der Zusammenfassungsstruktur.
  5. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK. Sie finden es an letzter Stelle unter den optionalen Komponenten.

Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:

  1. Öffnen Sie den Knoten Einzelne Komponenten in der Zusammenfassungsstruktur.
  2. Aktivieren Sie das Kontrollkästchen für den DGML-Editor.

Installation mithilfe des Visual Studio-Installers: Registerkarte „Einzelne Komponenten“

  1. Führen Sie den Visual Studio-Installer aus.
  2. Wählen Sie Ändern aus.
  3. Klicken Sie auf die Registerkarte Einzelne Komponenten.
  4. Aktivieren Sie das Kontrollkästchen für das .NET Compiler Platform SDK. Sie finden es an oberster Stelle im Abschnitt Compiler, Buildtools und Laufzeiten.

Optional können Sie einstellen, dass der DGML-Editor Diagramme in der Schnellansicht anzeigt:

  1. Aktivieren Sie das Kontrollkästchen für den DGML-Editor. Sie finden es im Abschnitt Codetools.

Das Erstellen und Überprüfen Ihres Analysetools erfolgt in mehreren Schritten:

  1. Erstellen der Projektmappe
  2. Registrieren von Name und Beschreibung des Analysetools
  3. Melden von Warnungen und Empfehlungen des Analysetools
  4. Implementieren des Codefixes zum Übernehmen der Empfehlungen
  5. Verbessern der Analyse durch Komponententests

Erstellen der Projektmappe

  • Wählen Sie in Visual Studio Datei > Neu > Projekt... aus, um das Dialogfeld „Neues Projekt“ anzuzeigen.
  • Wählen Sie unter Visual C# > ErweiterbarkeitAnalyzer with code fix (.NET Standard) aus.
  • Benennen Sie Ihr Projekt "MakeConst", und klicken Sie auf „OK“.

Hinweis

Möglicherweise wird ein Kompilierungsfehler angezeigt (MSB4062: Die CompareBuildTaskVersion-Aufgabe konnte nicht geladen werden). Um dies zu beheben, aktualisieren Sie die NuGet-Pakete in der Projektmappe mit dem NuGet-Paket-Manager, oder verwenden Sie Update-Package im Paket-Manager-Konsolenfenster.

Kennenlernen der Vorlage für das Analysetool

Das Analysetool Analyzer mit der Codekorrekturvorlage erstellt fünf Projekte:

  • MakeConst: Enthält das Analysetool.
  • MakeConst.CodeFixes: Enthält die Codekorrektur.
  • MakeConst.Package: Wird verwendet, um das NuGet-Paket für das Analysetool und die Codekorrektur zu generieren.
  • MakeConst.Test: Ein Komponententestprojekt.
  • MakeConst.Vsix: Das Standardstartprojekt, das eine zweite Instanz von Visual Studio startet, die das neue Analysetool geladen hat. Drücken Sie F5, um das VSIX-Projekt zu starten.

Hinweis

Analysetools sollten .NET Standard 2.0 als Ziel verwenden, da sie in der .NET Core-Umgebung (Befehlszeilenbuilds) und .NET Framework-Umgebung (Visual Studio) ausgeführt werden können.

Tipp

Wenn Sie das Analysetool ausführen, wird eine zweite Instanz von Visual Studio gestartet. Diese zweite Kopie verwendet eine andere Registrierungsstruktur zum Speichern von Einstellungen. Dadurch können Sie in beiden Instanzen von Visual Studio verschiedene Anzeigeeinstellungen verwenden. Sie können für die Testinstanz von Visual Studio ein anderes Design auswählen. Achten Sie außerdem darauf, Ihre Einstellungen oder Ihre Anmeldung nicht mithilfe der Testinstanz von Visual Studio als mobiler Benutzer auf Ihr Visual Studio-Konto zu übertragen. Auf diese Weise bleiben die unterschiedlichen Einstellungen erhalten.

Die Struktur enthält nicht nur das Analysetool in der Entwicklung, sondern auch alle vorherigen geöffneten Analysetools. Sie müssen die Roslyn-Struktur manuell unter %LocalAppData%\Microsoft\VisualStudio löschen, um sie zurückzusetzen. Der Ordnername der Roslyn-Struktur endet auf Roslyn, z. B. 16.0_9ae182f9Roslyn. Beachten Sie, dass Sie die Projektmappe möglicherweise bereinigen und nach dem Löschen der Struktur neu erstellen müssen.

Erstellen Sie in der zweiten Visual Studio-Instanz, die Sie gerade gestartet haben, ein neues Projekt für eine C#-Konsolenanwendung (beliebiges Zielframework, Analysetools arbeiten auf Quellebene). Zeigen Sie auf das wellenförmig unterstrichene Token, dann wird der vom Analysetool bereitgestellte Warnungstext angezeigt.

Die Vorlage erstellt ein Analysetool, das für jede Typdeklaration, deren Typname Kleinbuchstaben enthält, eine Warnung meldet, wie in der folgenden Abbildung dargestellt:

Analysetool beim Melden einer Warnung

Die Vorlage stellt darüber hinaus einen Codefix bereit, der jeden Typnamen, der Kleinbuchstaben enthält, in durchgängig Großbuchstaben ändert. Sie können auf die zusammen mit der Warnung angezeigte Glühbirne klicken, um die vorgeschlagenen Änderungen anzuzeigen. Wenn Sie die vorgeschlagenen Änderungen akzeptieren, werden der Typname und alle Verweise auf den betreffenden Typ in der Projektmappe aktualisiert. Nachdem Sie jetzt das anfängliche Analysetool in Aktion gesehen haben, schließen Sie die zweite Visual Studio-Instanz, und kehren Sie zu Ihrem Analysetoolprojekt zurück.

Sie brauchen keine zweite Instanz von Visual Studio zu starten und neuen Code zu erstellen, um jede Änderung an Ihrem Analysetool zu testen. Die Vorlage erstellt für Sie außerdem ein Komponententestprojekt. Dieses Projekt enthält zwei Tests. TestMethod1 zeigt das typische Format eines Tests, der Code analysiert, ohne eine Diagnose auszulösen. TestMethod2 zeigt das Format eines Tests, der eine Diagnose auslöst und dann einen vorgeschlagenen Codefix anwendet. Beim Erstellen Ihres Analysetools und Codefixes werden Sie Tests für verschiedene Codestrukturen schreiben, um Ihre Arbeit zu überprüfen. Komponententests für Analysetools sind viel schneller als interaktive Tests in Visual Studio.

Tipp

Analysetool-Komponententests sind ein hervorragendes Werkzeug, wenn Sie wissen, welche Codekonstrukte Ihr Analysetool auslösen sollten und welche nicht. Das Laden Ihres Analysetools in einer weiteren Visual Studio-Instanz ist ein tolles Hilfsmittel, um Konstrukte zu finden und zu untersuchen, die Ihnen möglicherweise noch nicht in den Sinn gekommen sind.

In diesem Tutorial schreiben Sie ein Analysetool, das dem Benutzer alle lokalen Variablendeklarationen meldet, die in lokale Konstanten konvertiert werden können. Beachten Sie z. B. folgenden Code:

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

Im Code oben ist x ein konstanter Wert zugewiesen, der nie geändert wird. Er kann mithilfe des const-Modifizierers deklariert werden:

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

Dies bringt die Analyse mit sich, mit der bestimmt wird, ob eine Variable zu einer Konstanten gemacht werden kann, wozu Syntaxanalyse, Konstantenanalyse des Initialisiererausdrucks und eine Datenflussanalyse erforderlich sind, um sicherzustellen, dass zu keinem Zeitpunkt in die Variable geschrieben wird. Die .NET Compiler Platform stellt APIs zur Verfügung, die das Durchführen dieser Analyse erleichtern.

Erstellen von Analysetoolregistrierungen

Die Vorlage erstellt die anfängliche DiagnosticAnalyzer-Klasse in der Datei MakeConstAnalyzer.cs. Dieses anfängliche Analysetool zeigt zwei wichtige Eigenschaften jedes Analysetools.

  • Jedes Diagnoseanalysetool muss ein [DiagnosticAnalyzer]-Attribut bereitstellen, das die Sprache beschreibt, in der es arbeitet.
  • Jedes Diagnoseanalysetool muss (direkt oder indirekt) von der DiagnosticAnalyzer-Klasse abgeleitet sein.

Die Vorlage zeigt außerdem die grundlegenden Features, die jedes Analysetool auszeichnen:

  1. Registrieren von Aktionen. Die Aktionen stellen Codeänderungen dar, die Ihr Analysetool auslösen sollten, um den Code auf Verstöße hin zu untersuchen. Wenn Visual Studio Codebearbeitungen erkennt, die mit einer registrierten Aktion übereinstimmen, ruft es die registrierte Methode Ihres Analysetools auf.
  2. Erstellen von Diagnosen. Wenn Ihr Analysemodul einen Verstoß erkennt, erstellt es ein Diagnoseobjekt, das von Visual Studio verwendet wird, um den Benutzer vom Verstoß zu benachrichtigen.

Sie registrieren Aktionen in Ihrer Überschreibung der DiagnosticAnalyzer.Initialize(AnalysisContext)-Methode. In diesem Tutorial suchen Sie auf der Suche nach lokalen Deklarationen Syntaxknoten auf und sehen, welche von ihnen konstante Werte aufweisen. Wenn eine Deklaration eine Konstante vorsehen könnte, erstellt und meldet Ihr Analysetool eine Diagnose.

Der erste Schritt besteht darin, die Registrierungskonstanten und die Initialize-Methode zu aktualisieren, damit diese Konstanten Ihr „Make Const“-Analysetool anzeigen. Die meisten der Zeichenfolgenkonstanten sind in der Zeichenfolgen-Ressourcendatei definiert. Sie sollten sich zwecks einfacherer Lokalisierung auch an diese Praxis halten. Öffnen Sie die Resources.resx-Datei für das MakeConst-Analysetoolprojekt. Dadurch wird der Ressourcen-Editor angezeigt. Aktualisieren Sie die Zeichenfolgenressourcen wie folgt:

  • Ändern Sie AnalyzerDescription in „Variables that are not modified should be made constants.“.
  • Ändern Sie AnalyzerMessageFormat in „Variable '{0}' can be made constant“.
  • Ändern Sie AnalyzerTitle in „Variable can be made constant“.

Wenn Sie fertig sind, sollte der Ressourcen-Editor wie in der folgenden Abbildung gezeigt aussehen:

Zeichenfolgenressourcen aktualisieren

Die verbleibenden Änderungen finden sich in der Analysetooldatei. Öffnen Sie MakeConstAnalyzer.cs in Visual Studio. Ändern Sie die registrierte Aktion von einer Aktion, die für Symbole aktiv ist, in eine, die mit Syntax agiert. Suchen Sie in der MakeConstAnalyzerAnalyzer.Initialize-Methode die Zeile, die die Aktion für Symbole registriert:

context.RegisterSymbolAction(AnalyzeSymbol, SymbolKind.NamedType);

Ersetzen Sie sie durch die folgende Zeile:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.LocalDeclarationStatement);

Nach dieser Änderung können Sie die AnalyzeSymbol-Methode löschen. Dieses Analysetool untersucht SyntaxKind.LocalDeclarationStatement-Anweisungen, keine SymbolKind.NamedType-Anweisungen. Beachten Sie, dass AnalyzeNode mit roten Wellenlinien unterstrichen ist. Der Code, den Sie soeben hinzugefügt haben, verweist auf eine AnalyzeNode-Methode, die noch nicht deklariert wurde. Deklarieren Sie diese Methode mithilfe des folgenden Codes:

private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
}

Ändern Sie Category wie im folgenden Code gezeigt in „Usage“ in MakeConstAnalyzer.cs:

private const string Category = "Usage";

Suchen von lokalen Deklarationen, die „const“ lauten könnten

Es ist Zeit, die erste Version der AnalyzeNode-Methode zu schreiben. Sie soll nach einer einzelnen lokalen Deklaration suchen, die const sein könnte, es aber nicht ist, wie der folgende Code:

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

Der erste Schritt besteht darin, nach lokalen Deklarationen zu suchen. Fügen Sie in MakeConstAnalyzer.cs den folgenden Code zu AnalyzeNode hinzu:

var localDeclaration = (LocalDeclarationStatementSyntax)context.Node;

Diese Umwandlung funktioniert immer, da Ihr Analysetool für Änderungen an lokalen Deklarationen und nur an lokalen Deklarationen registriert wurde. Kein anderer Knotentyp löst einen Aufruf Ihrer AnalyzeNode-Methode aus. Überprüfen Sie als Nächstes die Deklaration für alle const-Modifizierer. Wenn Sie sie finden, geben Sie sofort zurück. Der folgende Code sucht nach allen const-Modifizierern in der lokalen Deklaration:

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

Schließlich müssen Sie überprüfen, ob die Variable const sein könnte. Das bedeutet, sicherzustellen, dass sie nach der Initialisierung nie zugewiesen wird.

Sie führen semantische Analysen mithilfe von SyntaxNodeAnalysisContext durch. Sie verwenden das context-Argument, um festzustellen, ob die lokale Variablendeklaration als const festgelegt werden kann. Ein Microsoft.CodeAnalysis.SemanticModel stellt sämtliche semantischen Informationen in einer einzelnen Quelldatei dar. Mehr können Sie im Artikel über semantische Modelle erfahren. Sie verwenden das Microsoft.CodeAnalysis.SemanticModel zum Durchführen einer Datenflussanalyse für die lokale Deklarationsanweisung. Anschließend nutzen Sie die Ergebnisse dieser Datenflussanalyse, um sicherzustellen, dass zu keiner Zeit irgendwo ein neuer Wert in die lokale Variable geschrieben wird. Rufen Sie die GetDeclaredSymbol-Erweiterungsmethode auf, um das ILocalSymbol für die Variable abzurufen und zu sicherzustellen, dass sie nicht in der DataFlowAnalysis.WrittenOutside-Sammlung der Datenflussanalyse enthalten ist. Fügen Sie am Ende der AnalyzeNode-Methode den folgenden Code hinzu:

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

Der Code, den Sie soeben hinzugefügt haben, stellt sicher, dass die Variable nicht verändert wird und daher als const deklariert werden kann. Es ist Zeit, die Diagnose zu stellen. Fügen Sie in AnalyzeNode den folgenden Code als letzte Zeile hinzu:

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

Sie können Ihren Fortschritt überprüfen, indem Sie F5 drücken, um Ihr Analysetool auszuführen. Sie können die Konsolenanwendung laden, die Sie zuvor erstellt haben, und dann den folgenden Testcode hinzufügen:

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

Die Glühbirne sollte angezeigt werden, und Ihr Analysetool sollte eine Diagnose melden. Abhängig von der jeweiligen Version von Visual Studio kann jedoch Folgendes angezeigt werden:

  • Die Glühbirne, die weiterhin den aus der Vorlage erzeugten Codefix verwendet, weist darauf hin, dass Großschreibung möglich ist.
  • Eine Bannermeldung am oberen Rand des Editors mit dem Hinweis, dass in „MakeConstCodeFixProvider“ ein Fehler aufgetreten ist und deaktiviert wurde. Dies liegt daran, dass der Codefixanbieter noch nicht geändert wurde und weiterhin erwartet, TypeDeclarationSyntax-Elemente anstelle von LocalDeclarationStatementSyntax-Elementen zu finden.

Im nächsten Abschnitt erfahren Sie, wie der Codefix geschrieben wird.

Schreiben des Codefixes

Ein Analysetool kann einen Codefix oder mehrere zur Verfügung stellen. Ein Codefix definiert eine Bearbeitung, die das gemeldete Problem angeht. Für das Analysetool, das Sie erstellt haben, können Sie einen Codefix bereitstellen, der das Schlüsselwort „const“ einfügt:

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

Der Benutzer wählt es im Editor in der Benutzeroberfläche der Glühbirne aus, und Visual Studio ändert den Code.

Öffnen Sie die Datei CodeFixResources.resx, und ändern Sie CodeFixTitle in „Make constant“.

Öffnen Sie die von der Vorlage hinzugefügte Datei MakeConstCodeFixProvider.cs. Dieser Codefix ist bereits mit der Diagnose-ID verschaltet, die von Ihrem Diagnoseanalysetool erzeugt wird, er implementiert jedoch noch nicht die gewünschte Codetransformation.

Löschen Sie als Nächstes die MakeUppercaseAsync-Methode. Sie trifft nicht mehr zu.

Alle Codefixanbieter werden von CodeFixProvider abgeleitet. Sie alle überschreiben CodeFixProvider.RegisterCodeFixesAsync(CodeFixContext), um verfügbare Codefixe zu melden. Ändern Sie in RegisterCodeFixesAsync den Typ des Vorgängerknotens, nach dem Sie suchen, in LocalDeclarationStatementSyntax, damit er mit der Diagnose übereinstimmt:

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

Ändern Sie dann die letzte Zeile, um einen Codefix zu registrieren. Ihr Fix erstellt ein neues Dokument, das sich daraus ergibt, dass einer vorhandenen Deklaration der const-Modifizierer hinzugefügt wird:

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

Sie werden rote Wellenlinien in dem soeben hinzugefügten Code unter dem Symbol MakeConstAsync bemerken. Fügen Sie eine Deklaration für MakeConstAsync hinzu, wie etwa den folgenden Code:

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

Ihre neue MakeConstAsync-Methode transformiert das Document, das die Quelldatei des Benutzers darstellt, in ein neues Document, das jetzt eine const-Deklaration enthält.

Sie erstellen ein neues const-Schlüsselworttoken, um es am Anfang der Deklarationsanweisung einzufügen. Achten Sie darauf, zuerst alle führenden Trivia aus dem ersten Token der Deklarationsanweisung zu entfernen und sie an das const-Token anzufügen. Fügen Sie der MakeConstAsync-Methode folgenden Code hinzu:

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

Fügen Sie als Nächstes der Deklaration das const-Token mithilfe des folgenden Codes hinzu:

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

Formatieren Sie anschließend die neue Deklaration gemäß den C#-Formatierungsregeln. Das Formatieren Ihrer Änderungen in Anlehnung an den vorhandenen Code macht einen besseren Eindruck. Fügen Sie unmittelbar hinter dem vorhandenen Code die folgende Anweisung hinzu:

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

Für diesen Code ist ein neuer Namespace erforderlich. Fügen Sie am Anfang der Datei die folgende using-Anweisung hinzu:

using Microsoft.CodeAnalysis.Formatting;

Der letzte Schritt besteht im Ausführen Ihrer Bearbeitung. Dieser Prozess besteht aus drei Schritten:

  1. Abrufen eines Handles zu dem vorhandenen Dokument.
  2. Erstellen eines neuen Dokuments durch Ersetzen der vorhandenen Deklaration durch die neue Deklaration.
  3. Zurückgeben des neuen Dokuments.

Fügen Sie den folgenden Code am Ende der MakeConstAsync-Methode hinzu:

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

Ihr Codefix ist nun bereit, ausprobiert zu werden. Drücken Sie F5, um das Analysetoolprojekt in einer zweiten Instanz von Visual Studio auszuführen. Erstellen Sie in der zweiten Visual Studio-Instanz ein neues Konsolenanwendungsprojekt in C#, und fügen Sie der Methode „Main“ einige lokale Variablendeklarationen hinzu, die mit konstanten Werten initialisiert sind. Sie sehen, dass sie als Warnungen gemeldet werden, wie unten dargestellt.

Warnungen „Kann als ‚const‘ deklariert werden“

Sie haben große Fortschritte erzielt. Unter den Deklarationen, die in const umgewandelt werden können, werden Wellenlinien angezeigt. Aber die Arbeit ist noch nicht abgeschlossen. Alles hier funktioniert, wenn Sie const den Deklarationen beginnend bei i, dann weiter mit j und schließlich k hinzufügen. Wenn Sie den const-Modifizierer aber in einer anderen Reihenfolge zu i zuweisen, beginnend mit k, erzeugt Ihr Analysetool Fehler: k kann nicht const deklariert werden, sofern nicht i und j beide bereits const sind. Sie müssen weitere Analysen durchführen, um sicherzustellen, dass Sie mit den verschiedenen Weisen umgehen können, in denen Variablen deklariert und initialisiert werden können.

Erstellen von Komponententests

Ihr Analysetool und der Codefix funktionieren beim einfachen Fall einer einzelnen Deklaration, die als „const“ deklariert werden kann. Es gibt eine Vielzahl von möglichen Deklarationsanweisungen, bei denen diese Implementierung zu Fehlern führt. Sie tragen diesen Fällen Rechnung, indem Sie mit der Komponententestbibliothek arbeiten, die von der Vorlage erstellt wurde. Das funktioniert viel schneller, als wiederholt eine zweite Instanz von Visual Studio zu öffnen.

Öffnen Sie die MakeConstUnitTests.cs-Datei im Komponententestprojekt. Die Vorlage hat zwei Tests erstellt, die den zwei allgemeinen Mustern für einen Komponententest für Analysetools und Codefixe folgen. TestMethod1 zeigt das Muster für einen Test, der sicherstellt, dass das Analysetool keine Diagnose meldet, wenn es das nicht sollte. TestMethod2 zeigt das Muster für das Melden einer Diagnose und das Ausführen des Codefixes.

Die Vorlage verwendet Microsoft.CodeAnalysis.Testing-Pakete für Komponententests.

Tipp

Die Testbibliothek unterstützt eine spezielle Markupsyntax, einschließlich der folgenden Optionen:

  • [|text|]: Gibt an, dass eine Diagnosemeldung für text vorliegt. Standardmäßig darf diese Form nur zum Testen von Analysetools mit genau einem DiagnosticDescriptor verwendet werden, der von DiagnosticAnalyzer.SupportedDiagnostics bereitgestellt wird.
  • {|ExpectedDiagnosticId:text|}: Gibt an, dass eine Diagnosemeldung mit IdExpectedDiagnosticId für text vorliegt.

Ersetzen Sie die Vorlagentests in der Klasse MakeConstUnitTest durch die folgende Testmethode:

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

Führen Sie diesen Test aus, um zu überprüfen, ob er erfolgreich ist. Öffnen Sie in Visual Studio den Test-Explorer, indem Sie Test>Windows>Test-Explorer auswählen. Wählen Sie anschließend Alle ausführen aus.

Erstellen von Tests für gültige Deklarationen

Ganz allgemein sollten Analysetool so schnell wie möglich beendet werden und nur minimale Arbeit verrichten. Visual Studio ruft registrierte Analysetools auf, während der Benutzer den Code bearbeitet. Reaktionsfähigkeit ist eine wichtige Anforderung. Es gibt mehrere Testfälle für Code, der Ihre Diagnose nicht auslösen soll. Ihr Analysetool verarbeitet bereits einen dieser Tests, den Fall, in dem eine Variable nach der Initialisierung zugewiesen wird. Fügen Sie die folgende Testmethode hinzu, um diesen Fall darzustellen:

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

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

Dieser Test wird ebenfalls bestanden. Fügen Sie dann Testmethoden für Bedingungen hinzu, die Sie noch nicht behandelt haben:

  • Deklarationen, die bereits const sind, da sie bereits Konstanten sind:

            [TestMethod]
            public async Task VariableIsAlreadyConst_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            const int i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklarationen, die keinen Initialisierer besitzen, weil es keinen zu verwendenden Wert gibt:

            [TestMethod]
            public async Task NoInitializer_NoDiagnostic()
            {
                await VerifyCS.VerifyAnalyzerAsync(@"
    using System;
    
    class Program
    {
        static void Main()
        {
            int i;
            i = 0;
            Console.WriteLine(i);
        }
    }
    ");
            }
    
  • Deklarationen, bei denen der Initialisierer keine Konstante ist, da sie zur Kompilierzeit keine Konstanten sein können:

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

Es kann sogar noch komplizierter sein, da C# mehrere Deklarationen in Form einer Anweisung zulässt. Betrachten Sie die folgende Zeichenfolgenkonstante aus einem Testfall:

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

Die Variable i kann als Konstante deklariert werden, die Variable j jedoch nicht. Daher kann aus dieser Anweisung keine const-Deklaration gemacht werden.

Führen Sie Ihre Tests erneut aus – Sie werden feststellen, dass bei den neuen Testfällen Fehler auftreten.

Aktualisieren Ihres Analysetools, damit korrekte Deklarationen ignoriert werden

Sie benötigen noch einige Verbesserungen an der AnalyzeNode-Methode Ihres Analysetools, um Code herauszufiltern, der den Bedingungen entspricht. Es handelt sich bei allen um verwandte Bedingungen, daher lassen sich auch alle mit ähnlichen Änderungen beheben. Nehmen Sie an AnalyzeNode die folgenden Änderungen vor:

  • Ihre semantische Analyse hat eine einzelne Variablendeklaration untersucht. Dieser Code muss sich in einer foreach-Schleife befinden, die alle innerhalb der gleichen Anweisung deklarierten Variablen untersucht.
  • Jede deklarierte Variable muss über einen Initialisierer verfügen.
  • Der Initialisierer jeder deklarierten Variable muss zur Kompilierzeit eine Konstante sein.

Ersetzen Sie in Ihrer AnalyzeNode-Methode die ursprüngliche semantische Analyse:

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

durch den folgenden Codeausschnitt:

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

Die erste foreach-Schleife untersucht jede Variablendeklaration mithilfe der Syntaxanalyse. Die erste Prüfung stellt sicher, dass die Variable einen Initialisierer aufweist. Die zweite Prüfung stellt sicher, dass der Initialisierer eine Konstante ist. Die zweite Schleife enthält die ursprüngliche semantische Analyse. Die semantischen Prüfungen befinden sich in einer separaten Schleife, da sie einen größeren Einfluss auf die Leistung haben. Führen Sie Ihre Tests erneut aus – sie sollten alle bestanden werden.

Der letzte Schliff

Sie haben es fast geschafft! Es gibt noch ein paar weitere Bedingungen, mit denen Ihr Analysetool umgehen muss. Visual Studio ruft Analysetools auf, während der Benutzer Code schreibt. Es tritt häufig der Fall ein, dass Ihr Analysetool für Code aufgerufen wird, der sich nicht kompilieren lässt. Die Methode AnalyzeNode des Diagnoseanalysetools überprüft nicht, ob der Konstantenwert in den Variablentyp konvertiert werden kann. Die derzeitige Implementierung wandelt also eine fehlerhafte Deklaration wie int i = "abc" bereitwillig in eine lokale Konstante um. Fügen Sie eine Testmethode für diesen Fall hinzu:

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

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

Darüber hinaus werden Verweistypen nicht ordnungsgemäß behandelt. Der einzige konstante Wert, der für einen Verweistyp zulässig ist, ist null. Nur für System.String sind Zeichenfolgenliterale zulässig. Das heißt, const string s = "abc" ist zulässig, const object s = "abc" aber nicht. Diese Bedingung wird mit diesem Codeausschnitt überprüft:

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

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

Wenn Sie gründlich sein wollen, müssen Sie einen weiteren Test hinzufügen, um sicherzustellen, dass Sie für eine Zeichenfolge eine Konstantendeklaration erstellen können. Der folgende Codeausschnitt definiert sowohl den Code, der die Diagnose auslöst, als auch den Code nach der Anwendung des Fixes:

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

Schließlich ergreift bei einer Variablen, die mit dem Schlüsselwort var deklariert ist, der Codefix die falsche Aktion und generiert eine const var-Deklaration, was von der Sprache C# nicht unterstützt wird. Um diesen Fehler zu beheben, muss der Codefix das Schlüsselwort var durch den Namen des abgeleiteten Typs ersetzen:

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

Glücklicherweise lassen sich alle oben aufgeführten Fehler mithilfe genau der Techniken beheben, die Sie gerade gelernt haben.

Zum Beheben des ersten Fehlers öffnen Sie zuerst MakeConstAnalyzer.cs und suchen die foreach-Schleife, in der jeder der Initialisierer der lokalen Deklaration überprüft wird, um sicherzustellen, dass allen konstante Werte zugewiesen wurden. Rufen Sie unmittelbar vor der ersten foreach-Schleife context.SemanticModel.GetTypeInfo() auf, um Detailinformationen über den deklarierten Typ der lokalen Deklaration abzurufen:

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

Überprüfen Sie anschließend innerhalb Ihrer foreach-Schleife jeden Initialisierer, um sicherzustellen, dass er in den Variablentyp konvertiert werden kann. Fügen Sie die folgende Überprüfung hinzu, nachdem sichergestellt wurde, dass der Initialisierer eine Konstante ist:

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

Die nächste Änderung baut auf der letzten auf. Fügen Sie vor der schließenden geschweiften Klammer der ersten foreach-Schleife den folgenden Code hinzu, um den Typ der lokalen Deklaration zu überprüfen, wenn die Konstante eine Zeichenfolge oder NULL ist.

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

Sie müssen etwas mehr Code in Ihrem Codefixanbieter schreiben, um das Schlüsselwort var durch den korrekten Typnamen zu ersetzen. Kehren Sie zu MakeConstCodeFixProvider.cs zurück. Der Code, den Sie hinzufügen, führt die folgenden Schritte aus:

  • Überprüft, ob die Deklaration eine var-Deklaration ist, und wenn dies zutrifft:
  • Erstellt einen neuen Typ für den abgeleiteten Typ.
  • Vergewissert sich, dass die Typdeklaration kein Alias ist. Ist das der Fall, ist eine Deklaration als const var zulässig.
  • Stellt sicher, dass var kein Typname in diesem Programm ist. (In dem Fall ist const var zulässig).
  • Vereinfacht den vollständigen Typnamen

Das hört sich nach ziemlich viel Code an. Ist es aber nicht. Ersetzen Sie die Zeile, in der newLocal deklariert und initialisiert wird, durch den folgenden Code. Er gehört direkt hinter die Initialisierung von 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);

Sie müssen eine using-Anweisung hinzufügen, um den Simplifier-Typ zu verwenden:

using Microsoft.CodeAnalysis.Simplification;

Führen Sie Ihre Tests aus – sie sollten alle bestanden werden. Gratulieren Sie sich, indem Sie Ihr fertiges Analysetool ausführen. Drücken Sie STRG+F5, um das Analysetoolprojekt in einer zweiten Instanz von Visual Studio mit geladener Roslyn-Vorschauerweiterung auszuführen.

  • Erstellen Sie in der zweiten Visual Studio-Instanz ein neues C#-Konsolenanwendungsprojekt, und fügen Sie int x = "abc"; zur Methode „Main“ hinzu. Dank der ersten Fehlerbehebung sollte keine Warnung für diese lokale Variablendeklaration gemeldet werden (obwohl es erwartungsgemäß einen Compilerfehler gibt).
  • Fügen Sie als Nächstes object s = "abc"; zur Methode „Main“ hinzu. Aufgrund der zweiten Fehlerbehebung sollte keine Warnung gemeldet werden.
  • Fügen Sie schließlich eine weitere lokale Variable hinzu, die das Schlüsselwort var verwendet. Sie sehen, dass eine Warnung gemeldet und ein Vorschlag links unterhalb der Meldung angezeigt wird.
  • Bewegen Sie den Textcursor des Editors über die Wellenlinien-Unterstreichung, und drücken Sie STRG+., um den vorgeschlagenen Codefix anzuzeigen. Beachten Sie beim Auswählen des Codefixes, dass das Schlüsselwort var jetzt ordnungsgemäß verarbeitet wird.

Fügen Sie schließlich den folgenden Code hinzu:

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

Nach diesen Änderungen erhalten Sie rote Wellenlinien nur unter den ersten zwei Variablen. Fügen Sie const sowohl zu i als auch zu j hinzu, und Sie erhalten eine neue Warnung zu k, da das nun als const deklariert werden kann.

Herzlichen Glückwunsch! Sie haben Ihre erste Erweiterung für die .NET Compiler Platform erstellt, die dynamische Codeanalyse durchführt, um ein Problem zu erkennen, und eine schnelle Problembehebung zu seiner Korrektur bereitstellt. Auf diesem Weg haben Sie viele der Code-APIs kennengelernt, die Teil der .NET Compiler Platform SDKs sind (Roslyn-APIs). Sie können Ihre Arbeit anhand des fertiggestellten Beispiels in unserem GitHub-Beispielrepository überprüfen.

Weitere Ressourcen