C#

Ausnahmen im Code mit Roslyn finden

Mit Hilfe von Roslyn lässt sich sehr einfach der Code analysieren. In diesem Beispiel werden alle Methoden ermittelt, die eine Exception schmeißen.

Ausnahmen mit Roslyn im Code auffinden
Ricardo Gomez Angel

Ziel des heutigen Posts ist es anhand einer kleinen Aufgabe das Verwenden des Syntaxbaums von Roslyn zu zeigen. Der Syntaxbaum ist eine Repräsentation des Quellcodes, in welchem alle verwendeten Schlüsselwörter hierarchisch abgelegt sind. Näheres zum Syntaxbaum kann in diesem Whitepaper nachgelesen werden.

Als Aufgabe soll eine kleine Konsolenanwendung erstellt werden, welche die geworfenen Ausnahmen innerhalb eines Quellcodes auflistet. Damit ist gemeint, dass eine Visual Studio Solution oder ein Visual Studio Projekt geöffnet werden kann und alle Methoden aufgelistet werden die eine Ausnahme werfen. Bei der Ausgabe soll neben der Methode auch die dazugehörige Klasse ausgegeben werden.

Für folgenden Code sollte dann die nachfolgend aufgeführte Ausgabe generiert werden:

using System;

class KlasseA
{
  public void Methode1()
  {
  }

  public void Methode2()
  {
    throw new NotImplementedException();
  }
}

class KlasseB
{
  public void Methode1()
  {
    if (1 != 2)
    {
      throw new Exception();
    }
  }
}

KlasseA
– Methode2
– NotImplementedException
KlasseB
– Methode1
– Exception

Was muss das Tool nun tun? Nach kurzem Überlegen fallen mir folgende Schritte ein:

  • C#-Dateien (*.cs) für Solution/Projekt ermitteln
  • Die in den C#-Dateien enthaltenen Klassen ermitteln
  • Alle Methoden der Klassen ermitteln
  • Alle Ausnahmen pro Methode ermitteln
  • Ergebnis auf dem Bildschirm ausgeben

Aus diesen Schritten lässt sich folgendes Flow Design ableiten:

Flow Design für Exception Lister

Damit wir mit Roslyn eine Visual Studio Solution oder ein Visual Studio Projekt laden können, benötigen wir eine Instanz der Klasse MSBuildWorkspace. Diese kann über die statische Methode Create() erzeugt werden.

private static MSBuildWorkspace CreateWorkspace()
{
  return MSBuildWorkspace.Create();
}

Eine Solution kann dann über die Methode OpenSolutionAsync() geladen werden. Mit der Methode OpenProjectAsync() hingegen kann ein einzelnes Projekt geladen werden. Die hier vorgestellte Lösung kann mit beidem umgehen. Die Unterscheidung wird dabei allerdings nur anhand der Dateinamenserweiterung gemacht. Zu beachten dabei ist, dass beide Methoden nur als asynchrone Variante zur Verfügung stehen. Das ist aus meiner Sicht positiv, das Task-based Asynchronous Pattern (TAP) zieht sich durch die komplette API von Roslyn. Hier die Implementierung der Operation LoadProjectsFromFile():

private static IEnumerable<Project> LoadProjectsFromFile(
    string filename, MSBuildWorkspace workspace)
{
  if (Path.GetExtension(filename) == ".sln")
  {
    var solution = workspace.OpenSolutionAsync(filename).Result;
    return solution.Projects;
  }

  var project = workspace.OpenProjectAsync(filename).Result;
  return new List<Project> {project};
}

Auf die einzelnen Dokumente, in diesem Fall *.cs-Dateien, kann dann für jedes Projekt über die Eigenschaft Documents zugegriffen werden. Um den jeweiligen C#-Code eines Dokuments programmatisch zu verarbeiten, muss die syntaktische Analyse durchgeführt werden. Dies geschieht über die asynchrone Methode GetSyntaxRootAsync(). Als Ergebnis erhält man den Rootknoten des Syntaxbaums. Dieser Baum enthält alle Elemente der C#-Datei.

private static IEnumerable<DocumentInfo> OpenAllDocuments(
    IEnumerable<Project> projects)
{
  var documents = projects.SelectMany(x => x.Documents);
  return documents.Select(x => new DocumentInfo {
      Node = x.GetSyntaxRootAsync().Result});
}

Nachdem wir nun eine Auflistung aller Syntaxbäume haben, können wir mithilfe dieser, alle Klassen dies sich im Sourcecode befinden ermitteln. Dies geschieht am einfachsten, wenn die Methode DescendantNodes() in Kombination mit der LINQ-Methode OfType<ClassDeclarationSyntax>() verwendet wird. DescendantNodes() liefert dabei eine flache Liste aller Elemente im Syntaxbaum zurück, dies hat den großen Vorteil, dass man leicht nach Elementen Suchen/Filtern kann. Leider birgt dies unter Umständen den Nachteil, dass es aus Performanzgesichtspunkten nicht die eleganteste Art ist, da der komplette Bauminhalt verarbeitet wird. Dennoch ist dies, gerade durch LINQ, eine sehr einfache Art um nach bestimmten Knoten zu Suchen.

private static IEnumerable<ClassInfo> FindClasses(
    IEnumerable<DocumentInfo> documents)
{
  return documents.SelectMany(
    x => x.Node.DescendantNodes().OfType<ClassDeclarationSyntax>().Select(
       y => new ClassInfo {Document = x, Node = y}));
}

Das anschließende Finden der Methoden und Ausnahmen funktioniert analog zum Ermitteln der Klassen. Wobei in diesem Falle dann nach MethodDeclarationSyntax bzw. ThrowStatementSyntax gefiltert wird. Abschließend müssen die gefundenen Ausnahmen nur noch auf dem Bildschirm ausgegeben werden. Voilà, wir haben unser Kommandozeilentool, welches die anfangs gestellten Anforderungen erfüllt. Der komplette Sourcecode kann auf GitHub eingesehen werden.

Wer sich jetzt wundert, woher man Informationen zum Aufbau des Syntaxbaums bekommen kann, der sollte sich den Roslyn Syntax Visualizer anschauen. Welcher als Erweiterung für Visual Studio zur Verfügung steht. Mit ihm kann man beliebigen Source Code innerhalb des Visual Studios analysieren. Die Erweiterung kann über die Visual Studio Galerie installiert werden. Hier ein Beispiel, dass den Sourcecode von oben visualisiert:

Roslyn Syntax Visualizer
Roslyn Syntax Visualizer

Ich hoffe ich konnte mit dieser kleinen Anwendung nicht nur ein weiteres Beispiel für Flow Design aufzeigen, sondern auch den Syntaxbaum von Roslyn etwas näher zeigen. Wer sich die Anwendung genauer ansieht, wird aber auch feststellen, dass die aktuelle Lösung nicht den komplette Namen (inkl. Namensraum) für die Ausnahmen ermittelt. Um dies zu ermöglichen, muss auf das semantische Modell zurückgegriffen werden, dass ich in einem späteren Blogpost zeigen möchte.

Anmerkung: Bei diesem Text handelt es sich um einen überarbeiteten Repost eines alten Blog-Artikels aus 2015 von mir.