Statische Codeanalyse mit SonarQube

Manchmal fragen sich Entwickler: „Was mache ich hier eigentlich?“ Das hat dann nichts mit einer plötzlichen Sinneskrise oder dem Zusammenbruch der persönlichen Weltanschauung zu tun. Vielmehr wacht der innere Softwarearchitekt auf und verlangt nach einer Darstellung des Großen Ganzen.

Dieser Artikel zeigt, wie man mit SonarQube Antworten auf folgende Fragen erhalten kann:

  • Wie sieht die Software in ihrer Struktur aus?
  • Welche Bereiche sind besonders von Programmierfehlern betroffen?
  • Wo wurden falsche Abhängigkeiten eingebaut?
  • Was machen eigentlich meine Schichten und sind sie noch dicht?

Eher sollte man sich allerdings die Frage stellen, warum der Architekt lang genug schlief, dass solche Selbstzweifel überhaupt aufkommen können. Leider ist die Antwort relativ einfach: Typische Fehler im Alltag. Es kommt immer mal vor, dass man über den einen oder anderen Punkt hinwegsieht, obwohl man es eigentlich besser weiß. Da wird vielleicht nicht überlegt, ob die Logik wirklich noch in diese eine Komponente reingehört oder extrahiert werden sollte. Da kann man sich später noch den Kopf drüber zerbrechen und eventuell refaktorisieren. Oder es kann eben nicht nachverfolgt werden, warum dieser Test jetzt plötzlich fehlschlägt, obwohl doch alles richtig ist. Zeit drängt, es warten noch weitere Tickets.

Daraus ergeben sich folgende Problemfelder:

  • Keine oder ungenügende Tests: War die Software korrekt, bevor ich sie angepasst habe? Ist sie es danach immernoch?
  • Zeitdruck: Darf nicht existieren, kommt leider dennoch vor.
  • Legacy Code: Übersetzt bedeutet „Legacy“ unter anderem Erbe. Manche Erbschaften möchte man aber nicht antreten.

Solche Fehler summieren sich dann schnell. Selbst wenn immer die gleichen Entwickler am Projekt arbeiten, erodiert eine Codebasis. Namen für solche Projekte gibt es dann viele:

  • Big Ball of Mud: Es gibt keine erkennbare Architektur oder klare Abhängigkeiten. Alles ist irgendwie miteinander verwoben.
  • Gasfabrik: Eine Gasfabrik verbraucht viel Gas und äußerlich betrachtet kommt nur heiße Luft heraus, weil sie niemand versteht. Effektiv das Gegenteil eines Big Ball of Mud.
  • Spaghetticode: Lange Methoden. Gern zusammengesehen mit Copy Pasta.
  • Innere Plattform: Teile der Applikation sind so stark in ihrem Verhalten konfigurierbar, dass sie zu einer schwachen Kopie der Plattform werden, mit der sie gebaut wurde.
  • Sumohochzeit: Voneinander getrennte Teile der Applikation sind dermaßen stark aneinander gebunden, dass sie effektiv untrennbar sind.

Was man dann genau hat, wo man am besten anfängt aufzuräumen, wie man misst, ob es denn auch besser wird, darüber kann statische Codeanalyse Auskunft geben.

Was macht statische Codeanalyse?

Statische Codeanalyse nimmt kompilierten Code oder Quelltext, wendet Metriken darauf an und generiert Zahlen. Damit das nicht von Hand gemacht werden muss, gibt es fertige Werkzeuge dafür. Die bekanntesten in der Javawelt dürften FindBugs, PMD und checkstyle sein:

  • FindBugs arbeitet als einziges von den drei genannten auf Bytecodeebene. Es sucht dabei nach harten Fehlern wie Probleme in Klassenhierarchien, fehlerhafte Arraybehandlungen, unmögliche Typenumwandlungen oder nicht in Paaren überschriebene equals- und hashcode-Methoden.
  • PMD, dessen Name übrigens keine offizielle Bedeutung hat, sucht nach ineffizientem Code. Darunter fallen beispielsweise leere Codeblöcke, ungenutzte Variablen oder verschwenderisches Nutzen von Strings oder StringBuffern.
  • checkstyle, als dritter im Bunde, prüft den Programmierstil und arbeitet dabei genau wie PMD auf Quelltextebene. Dadurch kann die Einhaltung von Programmierrichtlinien oder Formattern erzwungen werden.

Diese Werkzeuge nutzen entsprechend ihrer Natur und Einsatzgebiete mal mehr, mal weniger Metriken, um ihre jeweiligen Statistiken zu erzeugen. Der Name „Metrik“ trägt jedoch wenig Bedeutung von dem in sich, was eine Metrik ausmacht. Schlägt man nämlich nach woher der Name kommt, landet man im Lateinischen: „ars metrica„, die Lehre von den Maßen. Fragt man jedoch das Institute of Electrical and Electronics Engineers, was eine Softwaremetrik ist, erhält man folgende Antwort:

„software quality metric: A function whose inputs are software data and whose output is a single numerical value that can be interpreted as the degree to which software possesses a given attribute that affects its quality.“ „Eine Softwarequalitätsmetrik ist eine Funktion, die eine Software-Einheit in einen Zahlenwert abbildet, welcher als Erfüllungsgrad einer Qualitätseigenschaft der Software-Einheit interpretierbar ist.“ – IEEE Standard 1061, 1998

Dies bedeutet, dass eine Metrik am Ende eine Funktion ist, die für beliebige Eingaben Zahlen erzeugt. Diese sind so beschaffen, dass sie untereinander vergleichbar sind, solange sie von derselben Funktion erzeugt wurden. Dadurch kann man Rückschlüsse auf die Eingabe mit Hinblick auf die Funktion erzielen.

Ein Beispiel dafür ist die McCabe-Metrik, auch zyklomatische Komplexität genannt. Diese sehr grundlegende Metrik berechnet die Anzahl der unterschiedlichen Pfade durch ein Stück Code. Die Formel ist sehr einfach: Es wird die Anzahl an Kontrollstrukturen wie if, while, case und boolescher Operatoren wie && und || hochsummiert und 1 addiert. Betrachten wir diese Information nochmals anhand eines Beispiels:

String nameOfDayInWeek(int nr) {
    switch(nr) {
        case 1: return "Monday";
        case 2: return "Tuesday";
        case 3: return "Wednesday";
        case 4: return "Thursday";
        case 5: return "Friday";
        case 6: return "Saturday";
        case 7: return "Sunday";
    }
    return "";
}

Diese sehr einfache Methode gibt den Namen eines Wochentages entsprechend seiner 1-indizierten Position innerhalb der Woche zurück. Ihre zyklomatische Komplexität beträgt acht: 1 plus 7 mal case. Dies ist ein relativ hoher Wert: Ein Maximalwert von 10 gilt als allgemein akzeptiert und ausreichend erprobt. Um also die Komplexität dieser Methode zu verringern, wird sie refaktorisiert:

String nameOfDayInWeek(int nr) {
    String[] names =  new String[] {
        "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday",
        "Sunday"
    };
    if(nr > 0 && nr <= names.length) {
        return names[nr - 1];
    }
    return "";
}

Die zyklomatische Komplexität dieser Methode beträgt drei: 1 plus 1 mal if plus 1 mal &&. Durch den unterschiedlichen Ansatz wird die Komplexität verringert, jedoch ist es relativ unstrittig, dass die erste Version schneller verstanden werden kann.

Will man nun also alle Tools zusammen benutzen, müssen alle konfiguriert und ihre Ergebnisse zusammengeführt werden, damit sich ein gemeinsames Bild ergibt. Außerdem kommt es dabei zwangsweise zu Dopplungen in ausgewerteten Metriken oder anderen Kennzahlen. PMD beispielsweise besitzt durch seinen relativ vagen Aufgabenbereich Überschneidungen im Hinblick auf Codestil mit checkstyle, während es aber auch genauso wie FindBugs auf toten Code achtet. An solchen und weiteren Stellen kann SonarQube Verbesserungen herbeiführen.

SonarQube

SonarQube besteht im Wesentlichen aus drei grob voneinander abtrennbaren Komponenten: Einem Scanner, der Code entgegennimmt und analysiert, einer Datenbank, in der Analyseergebnisse gespeichert werden, und einer Webkomponente, die die gesammelten Ergebnisse aufbereitet anzeigt. Auf diese Weise kann der Scanner aus beliebigen Quellen aufgerufen werden, beispielsweise von einem Maven-Build, einem CI-Server oder aus IDEs heraus. SonarQube steht unter der LGPL v3 und ist damit OpenSource.

In Sachen Analyse und Metriken bedient sich SonarQube dabei unter anderem an den bereits genannten Tools. Die Analysen aus PMD und checkstyle sind fest integriert, FindBugs kann über eine Pluginschnittstelle nachinstalliert werden. Über diese können viele weitere Funktionen nachgereicht werden. Neben Sprachensupport, beispielweise für Javascript oder PHP, bieten Plugins auch Funktionen für Quelltextverwaltungssysteme wie Git, Subversion oder GitHubs Pull Requests.

Für die Analyseergebnisse von Metriken werden von SonarQube Richtwerte bereitgestellt. Für jeden Verstoß gegen einen solchen Richtwert wird ein Issue erstellt. Diese werden nach Kategorie und Schwere sortiert.

Für die folgenden Screenshots wurde eine Analyse von Apache Log4j in der Version 1.2.18-SNAPSHOT gefahren.

Kategorien

Bei der Analyse unterteilt SonarQube Metrikverstöße, Issues genannt, neben der Schwere in drei Kategorien:

  • Code Smell: Beispiele hierfür sind die zyklomatische Komplexität, als Deprecated markierter Code oder unnütze mathematische Funktionen, beispielsweise das Runden von Konstanten. Solche Issues deuten in den meisten Fällen auf tiefer liegende Probleme hin. Ist beispielsweise die zyklomatische Komplexität einer Methode zu hoch, deutet das eventuell auf einen Designmangel in der Architektur hin.
  • Vulnerability: In dieser Kategorie landen Issues, bei denen es um Sicherheit geht. Damit ist nicht nur die Sicherheit in Form von SQL Injection oder fest einprogrammierten Passwörtern gemeint, sondern auch die innere Sicherheit. Beispielsweise wird der Issue „public static fields should be constants“ hier eingeordnet.
  • Bug: Hier kann man klassische Handwerksfehler finden, die beispielsweise der Javaspezifikation widersprechen. So werden hier Issues, wie der Vergleich von Klassen über ihren nicht voll qualifizierten Namen, unendliche Schleifen oder das dereferenzieren von bekannten null-Variablen, verortet.

Schwere

Weiterhin werden Issues nach ihrer Schwere unterteilt. Diese reichen vom Entwicklungsstopp bis zum Hinweis am Rande:

  • Blocker: In der schwersten Kategorie befinden sich Issues wie fest hinterlegte Passwörter. Wie der Name schon nahelegt, sollte kein Projekt mit dergestaltigen Issues weiterentwickelt werden, ohne sie zu beseitigen.
  • Critical: Hier landen Issues wie unbehandelte Exceptions oder andere, die Programmabläufe schwer beeinträchtigen können.
  • Major: Issues, die hier landen, gehören bereits mehr in den Bereich Codestil oder Konventionen: Leere Codeblöcke ohne erklärenden Kommentar, fehlende @Override-Annotationen oder auskommentierter Code.
  • Minor: Issues dieser Schwere sind syntaktisch korrekt, ihre Semantik lässt jedoch zu wünschen übrig. So gehören hier unnötige Typenumwandlungen, Duplikationen oder ungenutzte Rückgabewerte hin.
  • Info: Diese Schwere wird Issues zugeordnet, die irgendwann mal behandelt werden sollten, aber sonst auch weiterhin stiefmütterlich behandelt werden können: TODO-Kommentare entfernen oder @Deprecated-Code entfernen.

Automatische Interpretation

SonarQube nutzt ein Verfahren zur groben Bewertung der Codebasis: die schon andernorts viel zitierte technische Schuld. Jedem Issue wird dabei eine Zeit zugeordnet, die benötigt wird, ihn zu beheben. Die Summe dieser technischen Schuld wird dann im Verhältnis zum Gesamtaufwand des Projektes gestellt und ergibt das Maintainability Rating:
Maintainability Rating = Technical Debt / Development Cost
Je nach Verhältnis dieser beiden Kennziffern fällt sodann die von SonarQube genannte Maintainability Rating aus:

  • A: 0 – 0,1
  • B: 0,11 – 0,2
  • C: 0,21 – 0,5
  • D: 0,5 – 1
  • E: > 1

Nun weiß man selbst nie genau, und schon gar nicht SonarQube, wieviel Entwicklungsaufwand wirklich in ein Projekt, oder besser noch, in eine einzige Codezeile geflossen ist. Also wird wie immer geschätzt: SonarQube nimmt an, dass pro Codezeile 30 Minuten Entwicklungszeit ins Land geflossen sind, egal wie alt diese schon ist. Dies trifft natürlich lange nicht auf jede Codezeile zu, aber als grober Mittelwert sollte dies für große Projekte über die Zeit zutreffen.

Eine Beispielrechung: Wir haben ein kleines Projekt von 2.500 Zeilen Größe und 50 Tage technische Schuld angesammelt. Bei einem typischen Arbeitstag von acht Stunden Länge, von dem auch SonarQube ausgeht, können 16 Zeilen Code geschafft werden, oder 0,0625 Tage pro Zeile benötigt werden. Dies führt zu folgender Rechung: 50 / (0,0625 * 2.500) = 0,32. Laut der obigen Tabelle ergibt das die Note C.

Schaut man sich den Bewertungsmaßstab noch einmal genauer an, wird erkenntlich, dass pro Zeiteinheit Entwicklung, maximal zehn Prozent davon an technischer Schuld generiert werden muss, um schlechter als mit einer A-Note bewertet zu werden. Aus Erfahrung kann ich sagen, dass jedes Projekt, das groß genug ist, in der Gesamtbetrachtung genau diese Note erreicht. Das wirkt weniger erstaunlich, wenn man bedenkt, dass große Projekte lange laufen und über genau diesen langen Zeitraum hinweg im Durchschnitt viel durchschnittlich guter Code produziert wurde. Daher ist es interessanter, wenn Maintainability Rating nach den bereits bekannten Kategorien berechnet wird: Bugs, Vulnerability und Code Smells, wie es der Screenshot „Kategorien“ oben schon zeigt.

Manuelle Interpretation

Nun besitzt man eine eher unüberschaubar große Menge von Statistiken und Zahlen. Der interessante Teil bei Codeanalyse ist nämlich nicht allein diese Statistiken zu aktzeptieren und ihnen schlichtweg zu glauben, sondern ihnen durch Interpretation Bedeutung zu verleihen. Dabei gilt es ein paar einfache Regeln zu beachten:

  • Relative Werte schlagen absolute: 5.000 Issues im Projekt? Klingt viel, aber nicht mehr, wenn man weiß, dass es nur fünf pro Klasse sind.
  • Änderungen schlagen den Status Quo: Ganz dem Prinzip des Vorwärtsdenkens entsprechend, ist es interessanter, dass 5000 Issues im letzten Release geschlossen werden konnten und nur 500 hinzukamen, als dass 4.500 noch offen sind. Die positive Entwicklung ist das Wichtige.
  • Metriken verstehen: Wenn eine Metrik nicht verstanden wurde, kann sie weder angewandt, noch ihr Ergebnis interpretiert werden. Beispielsweise ist es schön zu wissen, was zyklomatische Komplexität ist. Wer das allerdings mit Lesbarkeit oder gar Wartbarkeit gleichsetzt, befindet sich auf dem Holzweg.
  • Metriken in Relation zueinander stellen: Wie der vorige Punkt schon andeutet, sagt eine Metrik allein wenig aus. Eine hohe zyklomatische Komplexität kann nämlich durchaus bedeuten, dass der betroffene Code schwer zu lesen ist. Allerdings gibt es auch noch Metriken wie die maximale Verschachtelungstiefe oder die Komplexität boolescher Ausdrücke, die da auch noch ein Wörtchen mitzureden haben.

Metriken verstehen

Die beliebteste, zugleich aber auch am häufigsten missbrauchte Metrik, ist die der Lines of Code, oft als LOC abgekürzt. Dass aber LOC nicht gleich LOC ist, zeigt ein kurzer Blick in gängige Nachschlagewerke:

    • Lines of Code (LOC): Alle physisch existierenden Zeilen. Kommentare, Klammern, Leerzeilen etc.
    • Source Lines of Code (SLOC): Wie LOC, nur ohne Leerzeilen und Kommentare
    • Comment Lines of Code (CLOC): Alle Kommentarzeilen
    • Non-Comment Lines of Code (NCLOC): Alle Zeilen, die keine Leerzeilen, Kommentare, Klammen, includes etc. sind

(Quelle: https://de.wikipedia.org/wiki/Lines_of_Code)

Wenn man nun die Größe eines Projekts beziffert welche Kennziffer sollte man heranziehen? Andere Frage: wie groß mag der Einfluss von unterschiedlichen Code Styleguides auf die Anzahl LOC und auf die Anzahl NCLOC sein?
SonarQube nutzt für seine Angaben über die Anzahl an Codezeilen NCLOC.

Metriken in Relation zueinander stellen

Betrachten wir ein weiteres Mal Beispielcode:

DTO search(List<List<DTO>> rawData, int id) {
  if(rawData != null) {
    for(List<DTO> sublist : rawData) {
      for(DTO dto : sublist) {
        if(dto.getId() == id) {
          return dto;
        }
      }
    }
  }
  return null;
}

Die oben stehende Methode sucht in einer doppelt geschachtelten Liste nach einem Objekt mit einer gegebenen Id. Nach McCabe besitzt sie eine zyklomatische Komplexität von 5, alles im Rahmen. Allerdings besitzt sie gleichzeitig eine maximale Verschachtelungstiefe von vier. SonarQube definiert ein Maximum von 3, also besitzt diese Methode ihren ersten Issue. Hätten wir nur McCabe betrachtet, wäre sie als grün durchgegangen und im Bericht nicht aufgefallen.

Eine early return später haben wir das Problem gelöst:

DTO search(List<List<DTO>> rawData, int id) {
  if(rawData == null) {
    return null;
  }

  for(List<DTO> sublist : rawData) {
    for(DTO dto : sublist) {
      if(dto.getId() == id) {
        return dto;
      }
    }
  }
  return null;
}

Die Metriken sind zufrieden, wir aber auch? Es gibt einen Grund, warum sie zur Kategorie Code Smell gehören: hier wurde sehr wahrscheinlich beim Architekturdesign versagt: Warum wurde eine doppelt geschachtelte Liste in die Methode hineingereicht? Arbeitet der Code absichtlich auf dieser geringen Abstraktionsebene? Vielleicht wurde aber auch am Domänendesign vorbei gearbeitet? Warum wurde nicht gleich das passende DTO aus der Datenquelle abgefragt, statt eine Methode mit hoher Laufzeitkomplexität anzustoßen? Viele sagen auch, dass das DTO-Pattern ein Antipattern ist. Aus diesen Fragen allein kann man schließen, dass der eigentliche Fehler nicht in der einzelnen Methode liegt, sondern auf einer höheren Ebene zu suchen ist.

Eine weitere Kombination aus Metriken wäre die bereits genannte Größe der Codebasis, gezählt in Anzahl Zeilen und die Anzahl der Issues. Ist die Codebasis groß genug, wird die absolute Anzahl an Issues schon groß genug sein. Dies spiegelt sich auch in der Maintainability Rating wider. Nimmt man jedoch eine dritte Dimension hinzu, die Projektlaufzeit, wird die Betrachtung viel interessanter: Ist das Projekt in schneller Zeit sehr stark gewachsen, kriegt eine hohe oder niedrige Anzahl Issues eine ganz andere Bedeutung. Schrumpft es jedoch und die Issues bleiben gleich, kann es sein, dass nicht genug Zeit da war, sich um diese zu kümmern. War jedoch genug Zeit vorhanden, kriegt diese Entwicklung einen faden Beigeschmack. An der Stelle sei noch einmal darauf hingewiesen, dass die Anzahl unterschiedlicher Entwickler an einem Projekt und deren Fluktuation eine ganz hervorragende Metrik ist.

Als weiterer Punkt wurde oben schon die Entwicklung genannt: Änderungen schlagen den Status Quo. SonarQube erstellt Historien, die sich auch wunderbar auswerten lassen. Beispielsweise kann man die Entwicklung der Projektgröße mit der Anzahl der Duplikationen vergleichen:

Im selben Zeitraum, in dem 5.000 Zeilen Code entfernt wurden, kamen 2,5% Codeduplikationen hinzu. Dies kann bedeuten, dass redundanter Code geschaffen wurde, oder nicht redundanter Code gelöscht wurde, sodass Duplikationen schwerer wiegen konnten. Wird dann noch in Betracht gezogen, dass log4j insgesamt nur 16.000 Zeilen Code besitzt, ist letzteres wahrscheinlicher.

Erfahrungswerte

  1. Eine ausreichend große Codebasis vorausgesetzt, erreichen viele Legacy-Projekte ein A-Rating. Wie bereits gesagt, ist das wenig verwunderlich. Wenn der durchschnittliche Codebeitrag durschnittlich gut ist, ist der Durchschnitt gut. Die Aufteilung nach Issuekategorie hilft an dieser Stelle enorm.
  2. Im Embedded-Bereich gibt es den Grundsatz, dass man alle 30 Regelverstöße drei kleinere und einen schwerwiegenden Bug erwarten kann. Diese Regel hilft, aus der Anzahl der Issues während der Entwicklung auf die Bugs während des Betriebs zu schließen. Denn meistens fällt es schwer, eine direkte Verbindung zwischen diesen Kennziffern herzustellen: In manchen prognostizierten NullPointer laufen Benutzer beispielweise einfach nicht hinein, weil die Applikation gar nicht auf die nötige Weise bedient wird. Allerdings helfen auch hier Mittelwerte wie die oben genannten. Die reinen Zahlenwerte müssen sicherlich fein abgestimmt werden, um auf den jeweiligen Einsatzbereich zu passen, das Muster sollte jedoch passen.
  3. Live mit SonarQube zu arbeiten, kann zu Gasfabriken führen. Ist ein dedizierter SonarQube-Monitor im Büro aufgestellt, werden die Teammitglieder zusehen, Issues entweder sofort zu beheben oder sie gleich im Vorfeld zu vermeiden. Diese durchaus lobenswerte Neigung kann allerdings dazu führen, dass zu komplex gedacht und das eigentliche Ziel aus den Augen verloren wird. Dieses Verhaltensmuster hat einen Namen: Over Engineering. Empfehlenswerter ist es, gemeinsam zum Sprintende oder vergleichbaren Terminen zu sichten, wie sich die Codebasis entwickelt.

Fazit

Zusammenfassend kann man folgendes mitnehmen:

  • Nie die nackten Zahlen hinnehmen. Statistiken sind zwar interessant und es kann viel Spaß machen sich hindurchzuklicken, jedoch ist eine statische Codeanalyse erst dann wirklich vollständig, wenn ein Mindestmaß an Interpretation hineingeflossen ist.
  • Codeanalyse liefert ein Gefühl für die Codebasis. Erst so können fundierte Aussagen darüber getroffen werden, welche Bereiche des Projekts besonders gefährdet, instabil oder renovierungsbedürftig sind.
  • Regelmäßige Analysen können die Teammotivation erhöhen. Eine positive Issuebilanz am Ende eines Sprints und aufwärtszeigende Historiengraphen sollten gute Treiber für eine Gruppe Entwickler und Beweis der eigenen Leistung sein.
  • Analyseergebnisse können als Argumentationsgrundlage dienen. Mit Hilfe der Projekthistorie, die eine Auswahl gut darstellbarer Kennzahlen beinhaltet, kann vor Kunden oder Entscheidern besser über ein eventuell nötiges technisches Release diskutiert werden.

Der Original-Artikel aus der „Java aktuell“ steht hier als PDF zum Download bereit.

Diesen Beitrag teilen

Josha von Gizycki
Software Development
Ständiges Dazulernen und der Austausch mit Gleichgesinnten sind seiner Meinung nach zwei der wichtigsten Bausteine zur stetigen Verbesserung.