C# und Visual Basic

Verwenden von Roslyn zum Erstellen eines Live-Codeanalysemoduls für Ihre API

Alex Turner

Seit nunmehr 10 Jahren hat die Codeanalyse von Visual Studio Analysen Ihrer C#- und Visual Basic-Assemblys zur Buildzeit geliefert und dabei eine bestimmte Menge von FxCop-Regeln ausgeführt, die für das Microsoft .NET Framework 2.0 geschrieben wurden. Diese Regeln stellen eine tolle Hilfe zum Vermeiden von Fallstricken im Code dar, aber sie sind auf die Probleme gerichtet, denen sich Entwickler damals im Jahr 2005 gegenüber sahen. Und wie sieht es mit den neuen Sprachfunktionen und APIs von heute aus?

Mit den projektbasierten Live-Codeanalysemodulen in Visual Studio 2015 Preview können API-Autoren domänenspezifische Codeanalyse als Teil ihrer NuGet-Pakete ausliefern. Da diese Analysemodule auf der .NET-Compilerplattform (mit dem Codenamen „Roslyn“) basieren, können sie Warnungen in Ihrem Code ausgeben, während sie ihn eingeben, sogar noch, bevor Sie ans Ende einer Zeile gelangt sind – Sie brauchen also nicht mehr bis zum Build Ihres Codes zu warten, um herauszufinden, dass Sie einen Fehler gemacht haben. Die Analysemodule können in Form der neuen Visual Studio-Glühbirne auch einen automatischen Codefix auf der Oberfläche anzeigen, der Ihnen hilft, Ihren Code sofort zu bereinigen.

Viele dieser Live-Codeanalysemodule bestehen aus nur 50 bis 100 Zeilen Code, und Sie müssen kein Compilercrack sein, um sie zu erstellen. In diesem Artikel demonstriere ich, wie Sie ein Analysemodul erstellen können, das auf ein gängiges Problem bei der Codeerstellung abzielt, das alle betrifft, die reguläre Ausdrücke (Regex) in .NET verwenden: Wie können Sie sicherstellen, dass das erstellte Regex-Muster syntaktisch gültig ist, bevor Sie Ihre App ausführen? Ich behandle das Problem so, dass ich Ihnen zeige, wie Sie ein Analysemodul erstellen, das eine Diagnose enthält, die auf ungültige Regex-Muster hinweist. Ich schicke diesem Artikel später einen zweiten hinterher, in dem ich zeige, wie Sie einen Codefix zum Bereinigen der vom Analysemodul gefundenen Fehler hinzufügen.

Erste Schritte

Vergewissern Sie sich zunächst, dass Sie über die erforderlichen Visual Studio 2015 Preview-Teile verfügen:

  • Richten Sie Visual Studio 2015 Preview auf eine der folgenden zwei Weisen in einer Sandbox ein:
    1. Installieren Sie Visual Studio 2015 Preview von aka.ms/vs2015preview.
    2. Laden Sie ein vorkompiliertes Azure VM-Imaged von aka.ms/vs2015azuregallery herunter.
  • Installieren Sie Visual Studio 2015 Preview SDK von aka.ms/vs2015preview. Das müssen Sie auch dann tun, wenn Sie das Azure VM-Image verwenden.
  • Installieren Sie das VSIX-Paket der SDK-Vorlagen von aka.ms/roslynsdktemplates, um die Projektvorlagen für die .NET-Compilerplattform zu erhalten.
  • Installieren Sie das VSIX-Paket des Syntax Visualizers von aka.ms/roslynsyntaxvisualizer, um ein Syntax Visualizer-Toolfenster zu erhalten, das Sie bei der Erkundung der Syntaxstrukturen für die Analyse unterstützt.
  • Sie müssen außerdem den Syntax Visualizer in der Visual Studio-Experimentiersandbox installieren, den Sie zum Debuggen des Analysemoduls benötigen. Ich führe Sie weiter unten in diesem Artikel, im Abschnitt über den Syntax Visualizer, schrittweise durch diesen Vorgang.

Vertraut machen mit der Analyzer-Vorlage

Sobald Sie Visual Studio 2015, das Visual Studio-SDK und die benötigten VSIX-Pakete betriebsbereit haben, sehen Sie sich die Projektvorlage zum Erstellen eines Analysemoduls an.

Navigieren Sie in Visual Studio 2015 zu „Datei“ | „Neues Projekt“ | „C#“ | „Erweiterbarkeit“, und wählen Sie die Vorlage „+++Diagnostic with Code Fix“ (NuGet + VSIX) aus, wie in Abbildung 1 gezeigt. Sie können Analysemodule auch in Visual Basic erstellen, für diesen Artikel verwende ich aber C#. Achten Sie darauf, dass das Zielframework oben auf .NET Framework 4.5 festgelegt ist. Geben Sie dem Projekt den Namen „RegexAnalyzer“, und wählen Sie „OK“ aus, um das Projekt zu erstellen.

Erstellen des Analyzer-Projekts
Abbildung 1. Erstellen des Analyzer-Projekts

Sie werden feststellen, dass die Vorlage drei Projekte generiert:

  • RegexAnalyzer: Dies ist das Hauptprojekt, das die DLL des Analysemoduls mit der Diagnoselogik und dem Codefix erstellt. Beim Build dieses Projekts wird darüber hinaus ein projektlokales NuGet-Paket (NUPKG-Datei) generiert, das das Analysemodul enthält.
  • RegexAnalyzer.VSIX: Dies ist das Projekt, das die Analysemodul-DLL in einem für Visual Studio gültigen Erweiterungspaket (VSIX-Datei) zusammenfasst. Wenn Ihr Analysemodul keine Warnungen hinzufügen muss, die sich auf Builds auswirken, können Sie sich entscheiden, die VSIX-Datei anstelle des NuGet-Pakets zu verteilen. In beiden Fällen erlaubt das VSIX-Projekt Ihnen, F5 zu drücken und das Analysemodul in einer separaten (Debug-)Instanz von Visual Studio zu testen.
  • RegexAnalyzer.Test: Dies ist ein Komponententestprojekt, mit dessen Hilfe Sie überprüfen können, ob Ihr Analysemodul die richtige Diagnoselogik und entsprechende Fixes erzeugt, ohne jedes Mal die Debuginstanz von Visual Studio ausführen zu müssen.

Wenn Sie die Datei „DiagnosticAnalyzer.cs“ im Hauptprojekt öffnen, sehen Sie in der Vorlage den Standardcode, der die Diagnoselogik generiert. Diese Diagnoselogik tut etwas ziemlich albernes – sie „kringelt“ alle Typnamen mit Kleinbuchstaben an. Da diese Typnamen aber in den meisten Programmen vorkommen, können Sie so das Analysemodul leicht in Aktion erleben.

Achten Sie darauf, dass „RegexAnalyzer.VSIX“ als Startprojekt festgelegt ist, und drücken Sie F5. Beim Ausführen des VSIX-Projekts wird eine experimentelle Kopie von Visual Studio in einer Sandbox geladen, wodurch Visual Studio einen separaten Satz Visual Studio-Erweiterungen nachverfolgen kann. Dies ist nützlich bei der Entwicklung eigener Erweiterungen, wenn Sie Visual Studio mithilfe von Visual Studio debuggen müssen. Da es sich bei der zu debuggenden Visual Studio-Instanz um eine neue experimentelle Sandbox handelt, werden die gleichen Dialoge angezeigt, die bei der ersten Ausführung von Visual Studio 2015 angezeigt wurden. Klicken Sie sich einfach wie gewohnt durch. Es können auch einige Verzögerungen beim Herunterladen von Debugsymbolen für Visual Studio auftreten. Nach der ersten Ausführung sollte Visual Studio diese Symbole für Sie zwischenspeichern.

Sobald die zu debuggende Visual Studio-Instanz ausgeführt wird, verwenden Sie sie zum Erstellen einer C#-Konsolenanwendung. Da es sich bei dem Analysemodul, das Sie debuggen, um die VSIX-Erweiterung mit Visual Studio als Gültigkeitsbereich handelt, sollte auf der Definition der Programmklasse innerhalb von ein paar Sekunden eine grüne Schlängellinie angezeigt werden. Wenn Sie den Mauszeiger auf dieser Schlängellinie ruhen lassen oder einen Blick in die Fehlerliste werfen, wird die Nachricht „Der Typname ‘Program’ enthält Kleinbuchstaben“, wie in Abbildung 2 dargestellt. Wenn Sie auf den Schlängel klicken, wird links ein Glühbirnensymbol angezeigt. Beim Klicken auf das Glühbirnensymbol wird ein Fix „In Großbuchstaben umwandeln“ angezeigt, der das Diagnoseergebnis bereinigt, indem er den Typnamen in Großbuchstaben umwandelt.

Der Codefix aus der Vorlage des Analysemoduls
Abbildung 2 Der Codefix aus der Vorlage des Analysemoduls

Sie können das Analysemodul auch von hier aus debuggen. Legen Sie in Ihrer Hauptinstanz von Visual Studio einen Haltepunkt innerhalb der Initialisierungsmethode in „Diagnostic­Analyzer.cs“ fest. Während Ihrer Eingabe im Editor berechnet das Analysmodul die Diagnose ständig neu. Wenn Sie innerhalb von „Program.cs“ in Ihrer Visual Studio-Debuginstanz eine Eingabe machen, werden Sie feststellen, dass der Debugger an diesem Haltepunkt anhält.

Halten Sie das Konsolenanwendungsprojekt einstweilen geöffnet, da Sie es im nächsten Abschnitt wieder brauchen.

Untersuchen des relevanten Codes mithilfe des Syntax Visualizers

Da Sie sich jetzt in der Vorlage des Analysemoduls zurechtfinden, ist es an der Zeit, zu planen, nach welchen Codemustern Sie im analysierten Code suchen möchten, um sich zu entscheiden, wann Sie „kringeln“.

Ihr Ziel ist, einen Fehler einzuführen, der angezeigt wird, wenn Sie ein ungültiges Regex-Muster erstellen. Fügen Sie als erstes in der Main-Methode der Konsolenanwendung die folgende Zeile hinzu, die „Regex.Match“ mit einem ungültigen Regex-Muster aufruft:

Regex.Match("my text", @"\pXXX");

Beim Betrachten dieses Codes und mit dem Mauszeiger auf „Regex“ und „Match“ können Sie die Bedingungen entwickeln, die das Anzeigen eines Schlängels bewirken sollen:

  • Es gibt einen Aufruf der Methode „Regex.Match“.
  • Der hier beteiligte Regex-Typ ist der aus dem Namespace „System.Text.RegularExpressions“.
  • Bei dem zweiten Parameter der Methode handelt es sich um Zeichenfolgenliteral, das ein ungültiges Muster für einen regulären Ausdruck darstellt. In der Praxis könnte der Ausdruck ebenso ein Verweis auf eine Variable oder Konstante – oder eine berechnete Zeichenfolge – sein, aber für die Ursprungsversion des Analysemoduls konzentriere ich mich erstmal auf Zeichenfolgenliterale. Es ist oft sinnvoll, ein Analysemodul zuerst einmal von Anfang bis Ende für einen einfachen Falll zum Funktionieren zu bringen, bevor man mit der Unterstützung weiterer Codemuster fortfährt.

Wie übersetzten Sie also diese einfachen Bedingungen in Code für die .NET-Compilerplattform? Der Syntax Visualizer ist ein tolles Werkzeug, das Ihnen hilft, das herauszufinden.

Sie sollten den Vizualizer in der experimentellen Sandbox installieren, die Sie zum Debuggen des Analysemoduls verwenden. Möglicherweise haben Sie den Visualizer bereits vorher installiert, das Installationsmodul installiert das Paket jedoch nur in der Visual Studio-Hauptinstanz.

Öffnen Sie in der Visual Studio-Debuginstanz „Tools“ | „Erweiterungen & Updates“ | „Online“, und suchen Sie in der Visual Studio Gallery nach „syntax visualizer“. Laden Sie das Syntax Visualizer-Paket für die .NET-Compilerplattform herunter, und installieren Sie es. Wählen Sie anschließend „Jetzt neu starten“ aus, um Visual Studio neu zu starten.

Öffnen Sie nach dem Neustart von Visual Studio das gleiche Konsolenanwendungsprojekt, und öffnen Sie den Syntax Visualizer, indem Sie „Ansicht“ | „Andere Fenster“ | „Roslyn Syntax Visualizer“ auswählen. Jetzt können Sie Caretzeichen im Editor bewegen und sich ansehen, wie Syntax Visualizer Ihnen den relevanten Teil der Syntaxstruktur anzeigt. Abbildung 3 zeigt die Ansicht für den Aufrufausdruck von „Regex.Match“, der uns hier interessiert.

Der Syntax Visualizer im Betrieb für den Zielaufrufsausdruck
Abbildung 3 Der Syntax Visualizer im Betrieb für den Zielaufrufsausdruck

Die Teile der Syntaxstruktur Beim Durchsuchen der Syntaxstruktur sehen Sie verschiedene Elemente.

Die blauen Knoten in der Struktur sind die Syntaxknoten, die die logische Struktur Ihres Codes nach der Analyse der Datei durch den Compiler darstellen.

Die grünen Knoten sind die Syntaxknoten, die einzelne Wörter, Zahlen und Symbole, die der Compiler beim Lesen der Quelldatei erkannt hat. Token werden in der Struktur unter dem Syntaxknoten, zu dem sie gehören, angezeigt.

Die roten Knoten in der Struktur sind die Trivia, die alles andere darstellen, das kein Token ist: Whitespace, Kommentare usw. Einige Compiler verwerfen diese Informationen, die .NET-Compilerplattform hält jedoch an ihnen fest, sodass Ihr Codefix die Trivia bei Bedarf beibehalten kann, wenn Ihr Fix den Code des Benutzers ändert.

Indem Sie Code im Editor auswählen, können Sie die relevanten Knoten in der Struktur anzeigen und umgekehrt. Zum Visualisieren der Knoten, die Sie interessieren, können Sie mit der rechten Maustaste auf den InvocationExpression (Aufrufsausdruck) in der Syntax Visualizer-Struktur klicken und „Gerichtetes Syntaxdiagramm anzeigen“ auswählen. Dadurch wird ein DGML-Diagramm generiert, das die Struktur unterhalb des ausgewählten Knotens visualisiert, wie in Abbildung 4 dargestellt.

Syntaxdiagramm für den Zielaufrufsausdruck
Abbildung 4 Syntaxdiagramm für den Zielaufrufsausdruck

In diesen Fall können Sie sehen, dass Sie nach einem Aufrufsausdruck suchen, der einen Aufruf von „Regex.Match“ darstellt, dessen „ArgumentList“ einen zweiten „Argument“-Knoten aufweist, der einen Zeichenfolgenliteral-Ausdruck enthält. Wenn der Zeichenfolgenwert ein ungültiges Regex-Muster enthält, wie etwa „\pXXX“, haben Sie den Bereich gefunden, der mit einem Schlängel zu versehen ist. Sie haben jetzt die meisten Informationen beisammen, die Sie zum Erstellen Ihres Diagnoseanalysemoduls benötigen.

Symbole und das semantische Modell: Jenseits der Syntaxstruktur Zwar stellen die Syntaxknoten, Token und Trivia, die im Syntax Visualizer angezeigt werden, den vollständigen Text der Datei dar, Sie können daraus aber nicht alle Informationen ablesen. Sie müssen außerdem wissen, worauf jeder der Bezeichner im Code tatsächlich verweist. Sie wissen z. B., dass dieser Aufruf sich auf eine Match-Methode eines Regex-Typs mit zwei Parametern bezieht, aber Sie kennen den Namespace des Regex-Typs nicht und wissen auch nicht, welche Überladung von „Match“ aufgerufen wird. Das genaue Ermitteln der Definitionen, auf die verwiesen wird, verlangt von den Compilern, die Bezeichner mithilfe von Anweisungen im Kontext ihrer Umgebung zu analysieren.

Für die Beantwortung von Fragen dieser Art müssen Sie das semantische Modell bitten, Ihnen das einem bestimmten Ausdrucksknoten zugeordnete Symbol anzugeben. Symbole stellen die logischen Entitäten dar, die in Ihrem Code definiert sind, wie etwa Ihre Typen und Methoden. Der Vorgang des Ermittelns des Symbols, auf das sich ein bestimmter Ausdruck bezieht, wird als Bindung bezeichnet. Symbole können auch die Entitäten darstellen, die Sie aus Bibliotheken nutzen, auf die verwiesen wird, wie etwa den Regex-Typ aus der Basisklassenbibliothek (BCL, Base Class Library).

Wenn Sie mit der rechten Maustaste auf den Aufrufsausdruck klicken und „Symbol anzeigen“ auswählen, wird das Eigenschaftenraster darunter mit Informationen aus dem Methodensymbol der aufgerufenen Methode aufgefüllt, wie in Abbildung 5 dargestellt.

Anzeigen des Methodensymbols für „Regex.Match“ im Syntax Visualizer
Abbildung 5 Anzeigen des Methodensymbols für „Regex.Match“ im Syntax Visualizer

In diesem Fall können Sie die Eigenschaft „Original­Definition“ betrachten und sehen, dass der Aufruf auf die Methode „System.Text.RegularExpressions.Regex.Match“ verweist, nicht auf eine Methode eines anderen Regex-Typs. Das letzte Teil im Puzzle beim Schreiben Ihrer Diagnose besteht darin, diesen Aufruf zu binden und zu überprüfen, ob das zurückgegebene Symbol der Zeichenfolge „System.Text.RegularExpressions.Regex.Match“ entspricht.

Erstellen der Diagnoselogik

Da Sie jetzt Ihre Strategie kennen, schließen Sie die Visual Studio-Debuginstanz, und kehren Sie zu Ihrem Analysemodulprojekt zurück, um mit dem Erstellen Ihrer Diagnoselogik zu beginnen.

Die Eigenschaft „SupportedDiagnostics“ Öffnen Sie die Datei „Diagnostic­Analyzer.cs“, und betrachten Sie die vier Zeichenfolgenkonstanten oben. Das ist der Ort, an dem Sie die Metadaten für Ihre Diagnoseregel definieren. Selbst bevor Ihre Analysemodul Schlängel erzeugt, verwenden der Regelsatz-Editor und andere Visual Studio-Features diese Metadaten, um die Details der Diagnose zu erkennen, die möglicherweise von Ihrem Analysemodul hervorgebracht werden.

Aktualisieren Sie diese Zeichenfolgen so, dass sie der Regex-Diagnose entsprechen, die Sie erstellen möchten:

public const string DiagnosticId = "Regex";
internal const string Title = "Regex error parsing string argument";
internal const string MessageFormat = "Regex error {0}";
internal const string Category = "Syntax";

Die Diagnose-ID und die eingesetzte Nachrichtenzeichenfolge werden Benutzern in der Fehlerliste angezeigt, wenn diese Diagnose gestellt wird. Ein Benutzer kann die ID in diesem Quellcode auch in einer #pragma-Anweisung verwenden, um eine Instanz der Diagnose zu unterdrücken. Im Anschlussartikel demonstriere ich, wie diese ID verwendet werden kann, um dieser Regel Ihren Codefix zuzuordnen.

In der Zeile, in der das „Rule“-Feld deklariert wird, können Sie außerdem den Schweregrad der erstellten Diagnose aktualisieren, um Fehler anstelle von Warnungen anzuzeigen. Wenn sich die Regex-Zeichenfolge nicht analysieren lässt, löst die Match-Methode definitiv zur Laufzeit eine Ausnahme aus, und Sie sollten den Build blockieren wie bei einem C#-Compilerfehler. Ändern Sie den Schweregrad der Regel in „DiagnosticSeverity.Error“:

internal static DiagnosticDescriptor Rule =
  new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
   Category, DiagnosticSeverity.Error, isEnabledByDefault: true);

Ebenfalls in dieser Zeile können Sie festlegen, ob die Regel standardmäßig aktiviert sein soll. Ihr Analysemodul kann eine größere Menge Regeln definieren, die standardmäßig alle deaktiviert sind, und die Benutzer können sich dafür entscheiden, einige oder alle der Regeln zu aktivieren. Lassen Sie diese Regel standardmäßig aktiviert.

Die „SupportedDiagnostics“-Eigenschaft gibt diesen Diagnosebezeichner („DiagnosticDescriptor“) als einziges Element eines unveränderlichen Arrays zurück. In diesem Fall erzeugt das Analysemodul nur eine Art Diagnose, sodass hier nichts zu ändern ist. Wenn das Analysemodul Diagnosen mehrerer Arten erzeugen kann, können Sie mehrere Bezeichner erstellen und sie alle von „SupportedDiagnostics“ zurückgeben lassen.

Methode „Initialize“ Der Haupteinstiegspunkt für Ihr Diagnoseanalysemodul ist die Initialize-Methode. In dieser Methode registrieren Sie eine Menge von Aktionen zur Verarbeitung verschiedener Ereignisse, die vom Compiler bei seinem Gang durch Ihren Code ausgelöst werden, etwa beim Finden verschiedener Syntaxknoten oder der Deklaration eines neuen Symbols. Das alberne Standardanalysemodul, das Sie von der Vorlage erhalten, ruft „RegisterSymbolAction“ auf, um herauszufinden, wann sich Typsymbole ändern oder eingeführt werden. In diesem Fall lässt die Symbolaktion das Analysemodul jedes Typsymbol untersuchen, um festzustellen, ob es auf einen Typ mit einem „ungültigen“ Namen hinweist, der einen Schlängel verdient hat.

In diesem Fall müssen Sie eine SyntaxNode-Aktion registrieren, um festzustellen, wenn ein neuer Aufruf an „Regex.Match“ eintritt. Erinnern Sie sich an Ihren Ausflug in den Syntax Visualizer, dann wissen Sie dass die spezifische Knotenart, nach der Sie suchen, „InvocationExpression“ ist. Ersetzen Sie also den Register-Aufruf in der Initialize-Methode durch den folgenden Aufruf:

context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);

Das Regex-Analysemodul musste nur eine Syntaxknotenaktion registrieren, die Diagnosen lokal erzeugt. Aber wie sieht es mit komplexeren Analysen aus, die Daten übergreifend von mehreren Methoden sammeln? Mehr dazu finden Sie im Abschnitt „Verarbeiten weiterer Registrierungsmethoden“ weiter unten in diesem Artikel.

Methode „AnalyzeNode“ Löschen Sie die „AnalyzeSymbol“-Methode der Vorlage, die Sie nicht mehr benötigen. An ihrer Stelle erstellen Sie nun eine Methode „AnalyzeNode“. Klicken Sie innerhalb des „RegisterSyntaxNodeActionv“-Aufrufs auf „AnalyzeNode“, und drücken Sie STRG+PUNKT, um das neue Glühbirnenmenü aufzuziehen. Wählen Sie hier die Methode „Generate“ aus, um eine „AnalyzeNode“-Methode mit der richtigen Signatur zu erstellen. Ändern Sie in der generierten Methode „AnalyzeNode“ den Namen des Parameters von „obj“ in „context“.

Jetzt, das Sie zum Kern Ihres Analysemoduls gelangt sind, ist es Zeit, den fraglichen Syntaxknoten zu betrachten, um zu entscheiden, ob Sie eine Diagnose anzeigen sollten.

Zuerst sollten Sie STRG+F5 drücken, um die Debuginstanz von Visual Studio erneut zu starten (diesmal, ohne zu debuggen). Öffnen Sie die Konsolenanwendung, die Sie gerade betrachtet haben, und vergewissern Sie sich, dass der Syntax Visualizer verfügbar ist. Sie sollten sich den Visualizer mehrfach ansehen, um wichtige Details zu finden, während Sie die Methode „AnalyzeNode“ erstellen.

Abrufen des Zielknotens Ihr erster Schritt in der AnalyzeNode-Methode besteht darin, das Knotenobjekt, das Sie analysieren, in den relevanten Typ umzuwandeln. Zum Suchen dieses Typs verwenden Sie den Syntax Visualizer, den Sie soeben geöffnet haben. Wählen Sie den Regex.Match-Aufruf aus, und navigieren Sie in der Syntaxstruktur zum InvocationExpression-Knoten. Sie sehen direkt oberhalb des Eigenschaftsrasters, dass „InvocationExpression“ die Art des Knotens ist, während der Typ „InvocationExpressionSyntax“ ist.

Sie können den Typ eines Knotens mit einer Typprüfung überprüfen oder seine bestimmte Art mit der IsKind-Methode testen. In diesem Fall können Sie jedoch auch ohne Test garantieren, dass die Typumwandlung funktioniert, da Sie in der Initialize-Methode bereits diese bestimmte Art angegeben hatten. Der Knoten, den Ihre Aktion analysiert, ist in der Node-Eigenschaft im Kontextparameter verfügbar:

var invocationExpr = (InvocationExpressionSyntax)context.Node;

Da Sie jetzt über den Aufrufknoten verfügen, müssen Sie überprüfen, ob es sich um einen Regex.Match-Aufruf handelt, der einen Schlängel benötigt.

Prüfung 1: Ist dies ein Aufruf einer Match-Methode? Die erste Prüfung, die Sie benötigen, stellt sicher, dass der Aufruf sich an die richtige „Regex.Match“ richtet. Da dieses Analysemodul bei jedem Tastendruck im Editor ausgeführt wird, ist es sinnvoll, die schnellsten Prüfungen zuerst auszuführen und aufwändigere Fragen nur an die API zu richten, wenn diese anfänglichen Prüfungen bestanden werden.

Der unaufwändigste Test besteht darin, zu prüfen, ob die Aufrufsyntax einen Aufruf einer Methode mit dem Namen „Match“ darstellt. Das kann bestimmt werden, bevor der Compiler irgendwelche besonderen Arbeiten verrichtet, um herauszufinden, um welche Match-Methode es sich im Einzelnen handelt.

Auf den Syntax Visualizer zurückblickend erkennen Sie, dass der Aufrufausdruck zwei untergeordnete Hauptknoten aufweist, nämlich „SimpleMemberAccess­Expression“ und „ArgumentList“. Indem Sie den Match-Bezeichner im Editor auswählen, wie in Abbildung 6 dargestellt, können Sie sehen, dass der gesuchte Knoten der zweite „IdentifierName“ innerhalb von „Simple­MemberAccessExpression“ ist.

Suchen des Match-Bezeichners in der Syntaxstruktur
Abbildung 6 Suchen des Match-Bezeichners in der Syntaxstruktur

Beim Entwickeln eines Analysemoduls tauchen Sie ziemlich häufig in dieser Weise in Syntax und Symbole ein, um die relevanten Typen und Eigenschaftswerte zu finden, auf die Sie in Ihrem Code verweisen müssen. Beim Erstellen von Analysemodulen ist es zweckmäßig, ein Zielprojekt mit Syntax Visualizer zur Hand zu haben.

Wieder im Code Ihres Analysemoduls können Sie die Elemente von „invocationExpr“ in IntelliSense durchsuchen und eine Eigenschaft finden, die jedem der Unterknoten von InvocationExpression entspricht, eine mit dem Namen „Expression“ und eine mit dem Namen „ArgumentList“. In diesem Fall geht es um die Eigenschaft „Expression“. Da der Teil eines Aufrufs, der vor der Argumentliste steht, viele Formen aufweisen kann (z. B. kann er ein Stellvertreteraufruf einer lokalen Variablen sein), gibt diese Eigenschaft einen allgemeinen Basistyp zurück, „ExpressionSyntax“. Im Syntax Visualizer können Sie sehen, dass der konkrete Typ, den Sie erwarten, „MemberAccessExpressionSyntax“ ist, also wandeln Sie in diesen Typ um:

var memberAccessExpr =
  invocationExpr.Expression as MemberAccessExpressionSyntax;

Einen ähnlichen Abriss sehen Sie, wenn Sie in die Eigenschaften von „memberAccessExpr“ einsteigen. Da finden Sie eine Eigenschaft „Expression“, die für den arbiträren Ausdruck vor dem Punkt steht, und eine „Name“-Eigenschaft, die den Bezeichner rechts vom Punkt darstellt. Da Sie prüfen möchten, ob Sie eine Match-Methode aufrufen, überprüfen Sie den Wert der Zeichenfolge der Name-Eigenschaft. Bei einem Syntaxknoten stellt das Abrufen des Zeichenfolgenwerts ein schnelles Verfahren dar, an den Quelltext für diesen Knoten zu gelangen. Sie können den neuen C#-Operator „?.“ verwenden, um den Fall zu behandeln, dass es sich bei dem Ausdruck, den Sie analysieren, nicht um einen Elementzugriff handelt, was dazu führt, dass die „as“-Klausel einen Nullwert zurückgibt:

if (memberAccessExpr?.Name.
    ToString() != "Match") return;

Wenn die aufgerufene Methode nicht den Namen „Match“ trägt, steigen Sie einfach aus. Ihre Analyse wurde mit minimalem Aufwand abgeschlossen, und es gibt keine Diagnose, die erstellt werden müsste.

Prüfung 2: Ist dies ein Aufruf der richtigen „Regex.Match“-Methode? Wenn die Methode den Namen „Match“ aufweist, führen Sie einen ehrgeizigeren Test durch, der den Compiler bittet, präzise festzustellen, welche „Match“-Methode vom Code aufgerufen wird. Die genaue Bestimmung der Match-Methode erfordert eine Aufforderung an das semantische Modell des Kontexts, diesen Ausdruck zu binden, um das Symbol, auf das verwiesen wird, abrufen zu können.

Rufen Sie die Methode „GetSymbolInfo“ für das semantische Modell auf, und übergeben Sie ihr den Ausdruck, zu dem Sie das Symbol suchen:

var memberSymbol =
  context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;

Das Symbolobjekt, das Sie zurückerhalten, ist das gleiche, das Sie im Syntax Visualizer anzeigen können, indem Sie mit der rechten Maustaste auf „SimpleMemberAccessExpression“ klicken und „Symbol anzeigen“ auswählen. In diesem Fall entscheiden Sie sich, das Symbol für die allgemeine IMethodSymbol-Schnittstelle zu wandeln. Diese Schnittstelle wird durch den internen Typ „PEMethodSymbol“ implementiert, die für das Symbol im Syntax Visualizer genannt wird.

Da Sie jetzt über das Symbol verfügen, können Sie es mit dem vollqualifizierten Namen vergleichen, den Sie bei der richtigen Regex.Match-Methode erwarten. Im Fall eines Symbols bedeutet das Abrufen des Zeichenfolgenwerts, dass Sie über den vollqualifizierten Namen verfügen. Sie kümmern sich jetzt noch nicht darum, welche Überladung Sie aufrufen, daher können Sie einfach bis zum Wort „Match“ prüfen:

if (!memberSymbol?.ToString().
  StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return;

Wie beim vorhergehenden Test prüfen Sie, ob das Symbol mit dem erwarteten Namen übereinstimmt, und wenn es das nicht tut oder tatsächlich kein Methodensymbol ist, steigen Sie aus. Es kommt Ihnen vielleicht ein bisschen seltsam vor, dass wir uns hier mit Zeichenfolgen befassen, aber Zeichenfolgenvergleiche sind in Compilern ein häufiger Vorgang.

Die verbleibenden Prüfungen Jetzt beginnen sich ein Rhythmus für Ihre Tests zu entwickeln. Mit jedem Schritt dringen Sie etwas weiter in die Struktur vor und überprüfen entweder die Syntaxknoten oder das semantische Modell, um zu testen, ob Sie sich noch immer in einer Fehlersituation befinden. Sie können jedes Mal den Syntax Visualizer verwenden, um zu sehen, welche Typen und Eigenschaftswerte Sie erwarten, damit Sie wissen, wann Sie zurückkehren und wann Sie fortfahren müssen. Folgen Sie diesem Muster, um die nächsten Bedingungen zu prüfen.

Achten Sie darauf, dass die „ArgumentList“ mindestens zwei Argumente aufweist:

var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
if ((argumentList?.Arguments.Count ?? 0) < 2) return;

Vergewissern Sie sich dann, dass das zweite Argument ein „LiteralExpression“ ist, denn Sie erwarten ein Zeichenfolgenliteral zurück:

var regexLiteral =
  argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
if (regexLiteral == null) return;

Schließlich, sobald Sie wissen, dass es sich um ein Literal handelt, bitten Sie das semantische Modell, Ihnen seinen Konstantenwert zur Kompilierungszeit zu übergeben, und überprüfen, dass es sich um ein Zeichenfolgenliteral handelt:

 

var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
if (!regexOpt.HasValue) return;
var regex = regexOpt.Value as string;
if (regex == null) return;

Überprüfen des Regex-Musters An diesem Punkt besitzen Sie alle erforderlichen Daten. Sie wissen, dass Sie „Regex.Match“ aufrufen, und Sie haben den Zeichenfolgenwert des Musterausdrucks. Wie überprüfen Sie ihn denn nun?

Sie rufen einfach die gleiche „Regex.Match“-Methode auf und übergeben ihr die Musterzeichenfolge. Da Sie nur nach Analysefehlern in der Musterzeichenfolge suchen, können Sie als erstes Argument eine leere Eingabezeichenfolge übergeben. Nehmen Sie den Aufruf innerhalb eines Try-Catch-Blocks vor, sodass Sie die „ArgumentException“ abfangen können, die „Regex.Match“ ausgibt, wenn eine ungültige Musterzeichenfolge angetroffen wird:

try
{
  System.Text.RegularExpressions.Regex.Match("", regex);
}
catch (ArgumentException e)
{
}

Wenn die Musterzeichenfolge ohne Fehler analysiert wird, wird Ihre „AnalyzeNode-Methode“ normal beendet, und es gibt nichts zu melden. Wenn ein Analysefehler auftritt, fangen Sie die Argumentausnahme ab – und sind damit bereit, eine Diagnose zu melden!

Melden einer Diagnose Innerhalb des Catchblocks verwenden Sie das „Rule“-Objekt, das Sie früher eingesetzt hatten, um ein Diagnoseobjekt zu erstellen, das für einen bestimmten Schlängel steht, den Sie erzeugen möchten. Jede Diagnose benötigt zwei Dinge, die für die betreffende Instanz spezifisch sind: den Bereich des Codes, der unterschlängelt werden soll, und die einzusetzenden Zeichenfolgen für das zuvor definierte Nachrichtenformat:

var diagnostic =
    Diagnostic.Create(Rule,
    regexLiteral.GetLocation(), e.Message);

In diesem Fall möchten Sie das Zeichenfolgenliteral anschlängeln, also übergeben Sie dessen Position als Bereich für die Diagnose. Ferner ziehen Sie die Ausnahmenachricht heraus, die beschreibt, welcher Fehler bei der Musterzeichenfolge vorlag, und schließen sie in die Diagnosemeldung ein.

Der letzte Schritt besteht darin, diese Diagnose an den an „AnalyzeNode“ übergebenen Kontext zurückzumelden, damit Visual Studio weiß, dass der Fehlerliste eine Zeile und im Editor eine Schlängellinie hinzugefügt werden müssen:

context.ReportDiagnostic(diagnostic);

Ihr Code in „DiagnosticAnalyzer.cs“ sollte jetzt aussehen wie Abbildung 7.

Abbildung 7 Der vollständige Code für „DiagnosticAnalyzer.cs“

using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
namespace RegexAnalyzer
{
  [DiagnosticAnalyzer(LanguageNames.CSharp)]
  public class RegexAnalyzerAnalyzer : DiagnosticAnalyzer
  {
    public const string DiagnosticId = "Regex";
    internal const string Title = "Regex error parsing string argument";
    internal const string MessageFormat = "Regex error {0}";
    internal const string Category = "Syntax";
    internal static DiagnosticDescriptor Rule =
      new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
      Category, DiagnosticSeverity.Error, isEnabledByDefault: true);
    public override ImmutableArray<DiagnosticDescriptor>
      SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }
    public override void Initialize(AnalysisContext context)
    {
      context.RegisterSyntaxNodeAction(
        AnalyzeNode, SyntaxKind.InvocationExpression);
    }
    private void AnalyzeNode(SyntaxNodeAnalysisContext context)
    {
      var invocationExpr = (InvocationExpressionSyntax)context.Node;
      var memberAccessExpr =
        invocationExpr.Expression as MemberAccessExpressionSyntax;
      if (memberAccessExpr?.Name.ToString() != "Match") return;
      var memberSymbol = context.SemanticModel.
        GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
      if (!memberSymbol?.ToString().StartsWith(
        "System.Text.RegularExpressions.Regex.Match") ?? true) return;
      var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
      if ((argumentList?.Arguments.Count ?? 0) < 2) return;
      var regexLiteral =
        argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
      if (regexLiteral == null) return;
      var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
      if (!regexOpt.HasValue) return;
      var regex = regexOpt.Value as string;
      if (regex == null) return;
      try
      {
        System.Text.RegularExpressions.Regex.Match("", regex);
      }
      catch (ArgumentException e)
      {
        var diagnostic =
          Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message);
        context.ReportDiagnostic(diagnostic);
      }
    }
  }
}

Probelauf Das war's schon – Ihre Diagnose ist jetzt vollständig! Um sie auszuprobieren, drücken Sie einfach F5 (achten Sie darauf, dass „RegexAnalyzer.VSIX“ das Startprojekt ist), und öffnen Sie die Konsolenanwendung in der Debuginstanz von Visual Studio erneut. Sie sollten bald eine rote Schlängellinie unter dem Musterausdruck sehen, die angibt, warum bei dessen Analyse ein Fehler auftrat, wie in Abbildung 8 dargestellt.

Probelauf des Diagnoseanalysemoduls
Abbildung 8 Probelauf des Diagnoseanalysemoduls

Wenn Sie die Schlängellinie sehen, herzlichen Glückwunsch! Wenn nicht, können Sie einen Haltepunkt innerhalb der AnalyzeNode-Methode setzen, ein Zeichen in die Musterzeichenfolge eingeben, um die erneute Analyse auszulösen, und dann schrittweise durch den Code des Analysemoduls fortschreiten, um festzustellen, an welcher Stelle das Analysemodul vorzeitig aussteigt. Sie können den Code auch mit Abbildung 7 vergleichen, die den vollständigen Code für „DiagnosticAnalyzer.cs“ enthält.

Anwendungsfälle für Diagnoseanalysemodule

Zusammengefasst: Ausgehend von der Analysemodulvorlage und durch Erstellen von ungefähr 30 Zeilen eigenem Code konnten Sie ein echtes Problem im Code Ihres Benutzers identifizieren und einen Schlängel dafür bereitstellen. Wichtiger, dafür war es nicht erforderlich, sich zum ausgemachten Experten für die internen Vorgänge des C#-Compilers zu entwickeln. Sie konnten den Blick weiterhin auf Ihr Zielgebiet der regulären Ausdrücke richten und den Syntax Visualizer verwenden, um Sie durch die kleine Menge von Syntaxknoten und Symbolen zu führen, die für Ihre Analyse relevant waren.

Es gibt viele Bereiche in der täglichen Codeerstellung, in der das Schreiben eines Analysemoduls zur Diagnose schnell erfolgen und nützlich sein kann:

  • Als Entwickler oder Teamleiter sehen Sie vielleicht bei anderen wiederholt die gleichen Fehler, wenn Sie den Code überprüfen. Jetzt können Sie ein einfaches Analysemodul erstellen, das diese Antimuster anschlängelt, und das Analysemodul in die Quellcodeverwaltung einchecken, was sicherstellt, dass jeder, der einen solchen Bug einführt, das noch während der Eingabe bemerkt.
  • Als Verwalter einer gemeinsamen Ebene, die Geschäftsobjekte für Ihre Organisation definiert, verfügen Sie möglicherweise über Geschäftsregeln für die ordnungsgemäße Verwendung dieser Objekte, die im Typsystem nur schwer in Code zu fassen sind, insbesondere, wenn sie numerische Werte oder Schritte in einem Prozess umfassen, bei dem Vorgänge in einer bestimmten Reihenfolge abgearbeitet werden müssen. Jetzt können Sie diese weicheren Regeln durchsetzen, die die Nutzung Ihrer gemeinsamen Ebene bestimmen, und nehmen den Ball da auf, wo ihn das Typsystem liegen lässt.
  • Als Besitzer eines Open Source- oder gewerblichen API-Pakets sind Sie es vielleicht leid, in den Foren wieder und wieder die gleichen Fragen beantworten zu müssen. Möglicherweise haben Sie sogar Whitepapers und Dokumentation erstellt und müssen feststellen, dass viele Ihrer Kunden noch immer auf die gleichen Probleme treffen, da sie nicht lesen, was Sie geschrieben haben. Jetzt können Sie Ihre API und die maßgebliche Codeanalyse in einem NuGet-Paket zusammenfassen und so sicherstellen, dass jeder, der Ihre API verwendet, vom Start weg die gleiche Hilfestellung erhält.

Hoffentlich hat dieser Artikel Sie inspiriert, über die Analysemodule nachzudenken, die Sie erstellen möchten, um Ihre eigenen Projekte zu verbessern. Mit der .NET-Compilerplattform hat Microsoft die Schwerarbeit geleistet, um tief greifendes Sprachverständnis und leistungsfähige Codeanalyse für C# und Visual Basic zur Verfügung zu stellen – der Rest liegt bei Ihnen!

Ausblick

Jetzt kann Ihr Visual Studio also Schlängellinien unter ungültigen regulären Ausdrücken anzeigen. Können Sie mehr erreichen?

Wenn Sie über das erforderliche Wissen bei regulären Ausdrücken verfügen, um nicht nur zu sehen, was bei einer Musterzeichenfolge nicht stimmt, sondern auch, wie man es behebt, können Sie in der Glühbirne eine Lösung vorschlagen, wie Sie es im Standardanalysemodul der Vorlage gesehen haben.

Im nächsten Artikel zeige ich, wie man den Codefix erstellt, während Sie erfahren, wie Sie Änderungen an Ihrer Syntaxstruktur vornehmen. Bleiben Sie am Ball!

Verarbeiten weiterer Registrierungsmethoden

Sie können sich in den Kontextparameter der „Initialize“-Methode graben, um die ganze Sammlung der „Register“-Methoden anzuzeigen, die Sie aufrufen können. Die Methoden in Abbildung A ermöglichen Ihnen, sich in verschiedene Ereignisse in der Pipeline des Compilers einzuhängen.

Ein wichtiger Punkt, den es im Auge zu behalten gilt, ist, dass eine Aktion der oberen Ebene, die mithilfe einer beliebigen Register-Methode registriert wurde, niemals irgendeinen Status in Instanzfeldern vom Typ des Analysemoduls verstecken sollte. Visual Studio verwendet eine Instanz vom Typ dieses Analysemlduls für die gesamte Visual Studio-Sitzung, um wiederholte Zuordnungen zu vermeiden. Jeder Status, den Sie speichern und erneut verwenden, ist mit hoher Wahrscheinlichkeit veraltet, wenn eine spätere Kompilierung analysiert wird, und kann sich sogar zu einem Speicherleck entwickeln, wenn er die Garbage Collection alter Syntaxknoten oder Symbole verhindert.

Wenn Sie einen Status übergreifend über Aktionen beibehalten müssen, sollten Sie „RegisterCodeBlockStartAction“ oder „RegisterCompilationStartAction“ aufrufen und den Status als lokalen Wert innerhalb der Aktionsmethode speichern. Mithilfe dem dieser Aktionen übergebenen Kontextobjekt können Sie verschachtelte Aktionen als Lambda-Ausdrücke registrieren, und diese verschachtelten Aktionen können die lokalen Werte in den äußeren Aktionen einschließen, um den Status beizubehalten.

Abbildung A Registriermethoden zum Einhaken in verschiedene Ereignisse

RegisterSyntaxNodeAction Wird ausgelöst, wenn eine bestimmte Art Syntaxknoten analysiert wurde
RegisterSymbolAction Wird ausgelöst, wenn eine bestimmte Art Symbol analysiert wurde
RegisterSyntaxTreeAction Ausgelöst, wenn die gesamte Syntaxstruktur einer Datei analysiert wurde
RegisterSemanticModelAction Ausgelöst, wenn ein semantisches Modell für die gesamte Datei zur Verfügung steht

RegisterCodeBlockStartAction

RegisterCodeBlockEndAction

Vor/nach der Analyse eines Methodenkörpers oder eines anderen Codeblocks ausgelöst

RegisterCompilationStartAction

RegisterCompilationEndAction

Vor/nach der Analyse des gesamten Projekts ausgelöst

Alex Turner ist ein leitender Programmmanager für das Managed Languages-Team bei Microsoft, wo er für C# und Visual Basic im .NET-Compilerplattformprojekt („Roslyn“) verantwortlich ist. Er hat mit einem Master in Computerwissenschaft an der Stony Brook University graduiert und Vorträge bei den Konferenzen Build, PDC, TechEd, TechDays und MIX gehalten.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Bill Chiles und Lucian Wischik
Bill Chiles hat den größten Teil seiner Laufbahn an Sprachen (CMU Common Lisp, Dylan, IronPython und C#) und Entwicklertools gearbeitet. Die letzten 17 Jahre hat der in der Developer Division von Microsoft verbracht und an allem mitgewirkt, von Visual Studio-Kernfeatures über die Dynamic Language Runtime bis zu C#.

Lucian Wischik ist im Visual Basic/C# Language Design Team bei Microsoft und insbesondere für Visual Basic zuständig. Bevor er zu Microsoft kam, hat er im universitären Bereich zur Theorie der Parallelität und Asynchronität gearbeitet. Er ist ein begeisterter Segler und Langstreckenschwimmer.