Besseren Code schreiben mit Statischer Code-Analyse - Feature-Bild

Besseren Code schreiben mit Statischer Code-Analyse

Autor-Bild
von Sven Woltmann – 14. August 2019

Vor vielen Jahren, als ich mich – unerfahren und naiv wie ich war – für einen deutlich besseren Programmierer hielt als ich es damals war, fragte mich jemand: "Sven, wie viele Zeilen hat Deine längste Methode?" Ich wusste weder eine Antwort auf die Frage noch verstand ich deren Hintergrund. Heute kann ich die Frage im Schlaf beantworten: 50! Sicher stellen wir das mit Checkstyle (mehr dazu unten) und wichtig ist das, da somit alle (ohne Ausnahme!) unserer Methoden ohne zu Scrollen auf den Bildschirm passen und so deren Funktion sehr schnell erfasst werden kann.

Im ersten Teil dieser Artikelserie über Statische Code-Analyse habe ich die folgenden qualitativen Ziele bei der Softwareentwicklung aufgelistet:

  • Sicherstellung eines einheitlichen Code-Stils im Team,
  • Entwicklung von lesbarem und wartbarem Code,
  • Entwicklung von möglichst fehlerfreiem Code,
  • Minimierung von Sicherheitslücken in der Software.

Klassischerweise versuchen IT-Teams durch regelmäßige Code-Reviews diesen Zielen möglichst nah zu kommen. Dieser Teil der Serie erklärt, wie Statische Code-Analyse die Erreichung der oben genannten Ziele im Einzelnen fördern kann.

Wie Statische Code-Analyse helfen kann

Entsprechend der vier oben genannten Ziele kann man die Aufgaben für Statische Code-Analyse-Tools in folgende Kategorien aufteilen:

  • Überprüfung des Code-Standards,
  • Berechnung von Softwaremetriken,
  • Erkennung von Fehlern im Code,
  • Erkennung von Sicherheitslücken.

Ein zusätzlicher Querschnittsaspekt ist die Fortschrittsüberwachung, d. h. die Speicherung von Metriken verschiedener Tools über die Zeit. Dadurch wird nachverfolgbar, ob sich die Code-Qualität im Hinblick auf die konfigurierten Aspekte verbessert oder verschlechtert.

Im Folgenden gehe ich detailliert auf die Kategorien ein.

Überprüfung des Code-Standards

Tools dieser Kategorie prüfen, ob der Quellcode vorher festgelegten und konfigurierten Code-Formatierungsvorgaben entspricht. Dabei können Aspekte wie bspw. Klammersetzung, Einrückung, Zeilenbreite, Leerzeichen und -zeilen, Klassen- und Methodenlängen, Anzahl von Methodenparametern und vieles weitere mehr überprüft werden.

Warum ist einheitlicher Code-Standard wichtig?

Einheitlicher Code-Standard hat folgende Vorteile:

  • Einheitlich formatierten Code ist einfacher zu lesen und zu verstehen.
  • Wenn alle Entwickler direkt im einheitlichen Stil schreiben, dann fallen Reviews leichter, da der Code nicht erst an die gemeinsamen Regeln angepasst werden muss.
  • Schwer lesbarer und damit schwer nachvollziehbarer Code kann zu Fehlern und Sicherheitsrisiken führen.

Welche Code-Standards gibt es?

Moderne IDEs können Code selbstständig formatieren. Dazu können in die IDE integrierte Formatierungsregeln verwendet, angepasst, exportiert und importiert werden. Die in Eclipse und IntelliJ standardmäßig integrierten Code-Stile sind allerdings unterschiedlich. Eclipse hat die Stile "Java Conventions", "Eclipse" und "Eclipse 2.1"; IntelliJ bietet den "Default"-Stil an.

IntelliJ kann von Eclipse exportierte Formate importieren, andersherum ist das nicht möglich. Da IntelliJ mit einem importierten Eclipse-Format nicht 100 % denselben Code generiert wie Eclipse, gibt es außerdem das Eclipse Code Formatter-Plugin, das den Code in IntelliJ auf die exakt gleiche Art und Weise formatiert wie Eclipse.

Die "Java Conventions" aus Eclipse entsprechen den 1997 von Sun herausgegebenen und zuletzt 1999 überarbeiteten Java Code Conventions. Entsprechend kennt dieser Stil keine neueren Sprachelemente wie bspw. Generics oder Lambdas.

Ein moderner und verbreiteter IDE-unabhängiger Code-Stil ist der Google Java Style Guide, welcher in vielen Projekten entweder direkt oder leicht abgewandelt verwendet wird. Der Google-Stil hat folgende Vorteile:

  • Es werden Konfigurationsfiles für Eclipse, IntelliJ und Checkstyle (dazu unten mehr) angeboten.
  • Es gibt nicht nur Vorgaben für Java, sondern für zahlreiche andere verbreitete Sprachen, wie HTML, CSS und JavaScript. So kann in einem Projekt, in dem verschiedene Sprachen zum Einsatz kommen, relativ einfach ein einheitlicher Stil verwendet werden.

Um die Unterschiede zu demonstrieren, werde ich das folgende (sinnlose und absichtlich unsauber formatierte) Codestück in allen zuvor genannten Code-Stilen formatieren.

Unformatierter Code
Unformatierter Code

Die Ergebnisse seht ihr in der folgenden Bilderstrecke (zum Vergrößern anklicken). Da beim Google Java Style standardmäßig mit zwei Leerzeichen eingerückt wird, habe ich noch eine zusätzlich Variante mit vier Leerzeichen eingefügt.

Wie ihr seht, sind alle Styles recht ähnlich. In allen wird eine leicht abgewandelte Variante des sogenannte "One True Brace Style" (1TBS) oder auch "Kernighan & Ritchie Style" (K&R) verwendet: Die öffnende geschweifte Klammer befindet sich in derselben Zeile wie die zugehörige Klassen- bzw. Methodendefinition oder des entsprechenden Control-Statements.

Unterschiede sieht man in Details, wie bspw.

  • ob in einer Array-Definition zwischen den eckigen und den geschweiften Klammern oder auch innerhalb der geschweiften Klamern Leerzeichen stehen,
  • ob geschweifte Klammern um einzelne Statements erforderlich oder optional sind,
  • ob einzelne Statements in derselben Zeile direkt hinter dem Control Statement stehen dürfen,
  • ob Leerzeilen entfernt oder eingefügt werden.

Gerade diese Details sind es, die beim Review stören, insbesondere wenn man Code-Änderungen hervorhebt und dann die beabsichtigten Änderungen aus dem Grundrauschen der vielen kleinen Formatierungsänderungen herausfiltern muss.

Wenn ihr einen eigenen Stil entwickelt, sollte dieser von den bekannten Stilen nur geringfügig abweichen, sodass Entwickler sich nicht lange ungewöhnen müssen, sondern den Stil möglichst intuitiv verstehen können. Mein bevorzugter Stil entspricht im Prinzip Google Style (also insbesondere das erforderliche Setzen von geschweiften Klammern um einzelne Statements) mit dem einzigen Unterschied, dass ich um 4 Leerzeichen einrücke statt um 2, so wie ich es aus den ursprünglichen Java Code Conventions gewöhnt bin.

Wie kann statische Code-Analyse einheitlichen Code-Standard sicherstellen?

Moderne IDEs können den Code zwar formattieren, allerdings gibt es folgende zwei Einschränkungen:

1. Sie können nicht alle Coding-Vorgaben sicherstellen, bspw. nicht die folgenden:

  • maximale Methoden- und Klassenlänge,
  • maximale Anzahl "Non Commenting Source Statements" in einer Methode,
  • maximale Anzahl von Parametern einer Methode,
  • maximale Verschachtelungstiefe von Schleifen und Control-Statements.

2. Sie können nicht sicherstellen, dass alle Entwickler die Formatter aktiviert und korrekt konfiguriert haben.

Hier springen statische Code Analyse-Tools der Kategorie "Überprüfung des Code-Standards" ein, die eben genau diese Lücke schließen. Sind die entsprechenden Tools einmal konfiguriert und in die Build-Pipeline integriert, dann ist sichergestellt, dass über das gesamte Projekt und von allen Teammitgliedern der gleiche Code-Stil verwendet wird.

Tools zur Überprüfung des Code-Standards

Der bekannteste Open-Source-Vertreter ist Checkstyle. Checkstyle kann ziemlich flexibel konfiguriert und auf alle oben genannten Code-Stile angepasst werden. Für den Google Java Style ist eine Checkstyle-Konfigurationsdatei zum Download verfügbar. Ebenso kann Checkstyle in den Build-Prozess integriert werden, so dass dieser Warnungen ausgibt oder fehlschlägt, wenn gegen die Regeln verstoßen wurde. Auf Checkstyle werde ich im dritten Teil des Artikels detaillierter eingehen.

Berechnung von Softwaremetriken

Softwaremetriken sind Funktionen, die bestimmte Qualitätsmerkmale einer Software (wie z. B. Wartbarkeit, Erweiterbarkeit oder Verständlichkeit) in einem objektiven und vergleichbaren numerischen Wert (z. B. "maintainability index", "zyklomatische Komplexität") darstellen. Softwaremetriken können Entwickler und Teams dabei helfen Qualitätsziele zu erreichen. Dazu werden entsprechende Tools in den Build-Prozess integriert, um die Softwaremetriken zu berechnen und die Entwickler bei Abweichungen von zuvor festgelegten Sollwerten zu alarmieren.

Welche Softwaremetriken gibt es?

Einige der bekanntesten Metriken sind die folgenden:

Kompexitätsmetriken:

  • Kopplung (coupling): das Maß der Abhängigkeiten zwischen den Modulen eines Systems – eine niedrige Kopplung führt zu besserer Verständlichkeit und Wartbarkeit;
  • Kohäsion (cohesion): das Maß der Abhängigkeiten innerhalb eines Softwaremoduls eine hohe Kohäsion führt zu besserer Verständlichkeit und Wartbarkeit;
  • Average Component Dependency: die durchschnittliche Anzahl von Abhängigkeiten der Komponenten in einem Softwaresystem;
  • Zirkulären Abhängigkeiten (circular dependencies): diese sind gleichzustellen mit einer hohen Kopplung der involvierten Module – keines kann alleinstehend wiederverwendet werden – und keines kann ohne die anderen verstanden werden;
  • Zyklomatische Komplexität (cyclomatic complexity): die Anzahl unterschiedlicher Pfade durch ein Softwaremodul – je höher die zyklomatische Komplexität, desto schwieriger ist das Softwaremodul zu verstehen;
  • Maintainability Index: ein Wert, der sich aus der Kombination bestimmter anderer Metriken ergibt.

Metriken zur Testabdeckung (code coverage / test coverage):

  • Line coverage: dieser Wert gibt an, wie viele Codezeilen im Verhältnis zur Gesamtanzahl der Codezeilen durch automatische Tests abgedeckt sind;
  • Branch coverage: das Verhältnis der durch Tests abgedeckten Programmablaufpfade zu den gesamt möglichen Ablaufpfaden.

Folgende Grafik zeigt die zirkulären Abhängigkeiten eines Projekts, an dem ich gearbeitet habe:

Zirkuläre Abhängigkeiten zwischen 63 Java-Packages
Zirkuläre Abhängigkeiten zwischen 63 Java-Packages

Bis wir die zirkulären Abhängigkeiten aufgelöst hatten (durch Neuzuordnung von Klassen zu Packages sowie durch Anwendung des Dependency Inversion Principle), war es unmöglich Teile des Codes in wiederverwendbare Module auszulagern. Auch war es insbesondere für neue Entwickler im Team sehr schwer sich in Code-Teile einzuarbeiten, da dies letztendlich ein zumindest grobes Verständnis aller 63 Packages erforderte.

Tools zur Berechnung von Softwaremetriken

Die bekanntesten Open-Source-Tools dieser Kategorie sind JaCoCo und Cobertura zur Messung der Testabdeckung, Sonargraph Explorer für die Berechnung und Darstellung von (zyklischen) Abhängigkeiten, sowie SonarQube für die Berechnung von Metriken zur Komplexität, Wartbarkeit, Zuverlässigkeit, Sicherheit und Testabdeckung.

Erkennung von Fehlern im Code

Tools dieser Kategorie versuchen potentielle Fehler im Code zu finden, in dem sie häufig vorkommende Fehlermuster erkennen. Beispiele dafür sind:

  • sichere/potentielle NullPointerExceptions,
  • Vergleich mit equals() auf Objekten verschiedener Klassen,
  • Klassen, die equals() definieren, aber nicht hashCode() – bzw. vice versa,
  • String-Vergleiche mit == oder != (so etwas ist entweder ein Fehler oder – wenn beabsichtigt – für den nächsten Entwickler verwirrend und somit fehleranfällig),
  • switch-Statements ohne default-Option,
  • switch-Statements mit "fall throughs" (diese sind verwirrend, da der Leser oft nicht weiß, ob sie beabsichtigt sind),
  • unbenutze Konstruktor- oder Methodenparameter,
  • unbenutzte lokale Variablen,
  • unbenutzte private Felder und Methoden,
  • Resourcen, die nicht (in jedem Fall) geschlossen werden,
  • Objekte, die Referenzen auf veränderliche interne Objekte, wie bspw. Listen nach außen sichtbar machen (anstatt Kopien oder Read-Only-Proxys),
  • fehlende assert-Statements in Unit-Tests.

Statische Code-Analyse-Tools zur Erkennung von Fehlern sind bspw. der Java-Compiler selbst (mit entsprechenden Parametern), die Open Source Tools PMD, FindBugs bzw. dessen Nachfolger SpotBugs, und SonarLint, welche ich im dritten Teil der Artikelserie detailliert vorstellen werde.

Erkennung von Sicherheitslücken

Letztendlich werden Sicherheitslücken in der Software durch Fehler im Code verursacht. In Abgrenzung zur vorangegangenen Kategorie handelt es sich hierbei jedoch um Fehler, die deutlich schwieriger zu erkennen sind, da hierfür aufwändige Datenfluss-Analysen durchgeführt werden müssen: Es muss geprüft werden, wie Eingabedaten durch das System und ggf. auch durch externe Libraries fließen und verarbeitet werden, sowohl über reguläre als auch außerordentliche Ausführungspfade. Beispiele für Sicherheitslücken sind:

  • Command und SQL Injection: fehlende/unzureichende Verifizierung von Eingabewerten kann dazu führen, dass z. B. User-Eingaben in Formularen durch die Software als (SQL-)Kommandos interpretiert werden können (z. B. "; delete * from User;" – wird dies unverändert als Teil einer Query an die Datenbank geschickt, wird möglicherweise die komplette User-Tabelle geleert);
  • Fehler in der Zugriffskontrolle, so dass unberechtigte User Funktionen ausführen können, für die sie keine Berechtigung haben (z. B. private Daten anderer User auslesen);
  • Cross-Site-Scripting: das Einschleusen von schädlichem ausführbaren Code z. B. über Request-Parameter in einer URL;
  • Cross-Site-Request-Forgery: hierbei wird einem eingeloggten User eines Systems ein Link untergeschoben, über den dieser unbewusst eine für ihn schädliche Aktion ausführt (z. B. könnte er dadurch sein Passwort auf ein durch den Angreifer festgelegtes Passwort ändern, woraufhin dieser sich in den User-Account einloggen könnte).

Wichtige Begriffe im Zusamenhang mit Softwaresicherheit sind:

  • CWE – Common Weakness Enumeration: eine durch die Entwickler-Community erstellte Liste häufig auftretender Sicherheitslücken, in der jeder Schwachstelle ein eindeutiger Bezeichner zugewiesen wird, wie bspw. CWE-77 für die oben erwähnte "Command Injection" oder CWE-89 für "SQL Injection". Diese Liste ist nicht priorisiert. Sie dient sozusagen als gemeinsame Sprache: fast alle Softwaresicherheitstools geben bei erkannten Schwachstellen den entsprechenden CWE-Identifikator mit an.
  • OWASP – Open Web Application Security Project: eine Non-Profit-Organisation mit dem Ziel die Sicherheit von Software zu erhöhen. Das bekannteste Projekt ist die „OWASP Top 10“-Liste, die die aktuell zehn kritischsten Sicherheitsrisiken von Webanwendungen (einschließlich ihrer CWE-Klassifikation) auflistet. Die Liste wurde zuletzt 2017 aktualisiert.
  • CWE/SANS Top 25 Most Dangerous Software Errors: eine alternative Liste der 25 kritischsten Sicherheitsrisiken – allerdings seit 2011 nicht aktualisiert.

Diese Listen sind priorisiert und können somit verwendet werden, um die Behebung von Sicherheitslücken zu priorisieren (s. Rollout-Strategie).

OWASP-Benchmark

Ein weiteres OWASP-Projekt ist der OWASP-Benchmark. Diese kostenlose, quelloffene Test-Suite prüft, wie gut ein Softwaresicherheitstool bestimmte Schwachstellen aufspürt, d. h. wie viele tatsächliche Fehler es findet und – dem gegenübergestellt – wie viele false positives es meldet. Der OWASP-Benchmark hilft die verfügbaren Sicherheitstools untereinander zu vergleichen und entsprechend den Anforderungen an die Softwaresicherheit zu beurteilen.

Tools für die Erkennung von Sicherheitslücken

Im Open-Source-Bereich sind mir hier Find Security Bugs, ein FindBugs/SpotBugs-Plugin, und das oben bereits erwähnte SonarQube bekannt. Beide werde ich im dritten Teil der Serie näher vorstellen. Für Anwendungen mit erhöhten Sicherheitsanforderungen sollte man hier ggf. auf Tools kommerzieller Anbieter zurückgreifen, welche allerdings außerhalb des Rahmens dieser Artikelserie liegen.

Einen sehr in die Tiefe gehenden Artikel zu diesem Thema findet ihr im Java Magazin 7/2018 ab S. 16. unter dem Titel "Neue Wege analysieren – Softwaresicherheit und Datenschutz mithilfe statischer Codeanalyse".

Querschnittsaspekt: Fortschrittsüberwachung

Die Tools aller zuvor genannten Kategorien liefern bei jedem Durchlauf numerische Werte, anhand derer quantitative Aussagen über die Qualität der Software gemacht werden können, wie bspw.:

  • Anzahl Code-Stil-Warnungen bzw. -Fehler,
  • Anzahl Tests und Anzahl bzw. Prozentsatz der Code-Zeilen, die durch Tests abgedeckt sind,
  • Anzahl duplizierter Code-Blöcke bestimmter Längen,
  • Anzahl zirkulärer Abhängigkeiten zwischen Klassen bzw. Packages,
  • Zyklomatische Komplexität,
  • Anzahl potentieller Fehler bestimmter Kategorien.

Es gibt Tools, die diese Metriken im zeitlichen Verlauf speichern und darstellen können. Somit kann das Entwicklungsteam nachverfolgen, wie sich die Code-Qualität im Laufe der Zeit verändert. Dies ermöglicht es auch Ziele zu setzen, wie z. B. die Test-Coverage in Zeitraum x um y Prozentpunkte zu erhöhen, oder die zyklomatische Komplexität in Zeitraum x um Betrag y zu reduzieren.

Das wohl bekannteste Open-Source-Tool dieser Art ist die Community Edition von SonarQube, welches nicht nur – wie oben erwähnt – Code-Metriken berechnen kann, sondern diese auch im zeitlichen Verlauf speichert und visualisiert. Über Plugins kann SonarQube die Metriken anderer statischer Code Analyse-Tools importieren und auch deren Historien speichern.

Rollout-Strategie

Optimalerweise setzt man Statische Code-Analyse-Tools bereits zu Beginn des Projekts ein, um teure Refactorings zu einem späteren Zeitpunkt zu vermeiden. Selbstverständlich kann man die Tools auch in bestehende Projekte einführen. Um die Entwickler in der IDE und durch Build-Reports nicht mit Tausenden von Warnmeldungen zu überfluten (deren Behebung dann aufgrund der unüberschaubaren Anzahl gerne wieder und wieder verschoben wird) sollte man eine Rollout-Strategie erarbeiten.

Bei AndroidPIT haben wir das wir folgt gemacht: Wir haben zunächst alle Tools mit den Default-Einstellungen installiert und uns einen groben Überblick über die Meldungen verschafft. Dann haben wir im Team entschieden, welche wir für sinnvoll halten und welche nicht. Die für uns nicht sinnvollen haben wir abgeschaltet. Gab es zu bestimmten Fehlerarten nur vereinzelte Warnungen, haben wir diese direkt behoben, sofern dies kein allzu hoher Aufwand war. Was übrig blieb (immer noch mehrere Tausend Meldungen) haben wir priorisiert. Dabei haben wir allen sicherheitsrelevanten Meldungen als erstes betrachtet und diese entsprechend der OWASP Top 10 priorisiert. Wie wir nicht sicherheitsrelevante Meldungen priorisieren, haben wir im Team entschieden.

Dann haben wir alle Meldungen bis auf die höchst priorisierte Kategorie deaktiviert und nach und nach die verbleibenden Warnungen analysiert und den Code entsprechend verbessert. Teilweise haben wir dies im Rahmen von Tickets gemacht (insbesondere bei den Sicherheitsrisiken), teilweise haben Team-Mitglieder, wenn sie eine Abwechslung von ihrem regulären Task brauchten, Warnungen abgearbeitet. Sobald alle Warnungen beseitigt waren, haben wir die nächstwichtigte Kategorie aktiviert und wiederum abgearbeitet. Zwischendurch kamen durch Updates der Tools neue Warnungen hinzu, die wir dann wiederum einpriorisiert und vorübergehend deaktiviert haben. Jenkins haben wir so konfiguriert, dass unsere Projekte trotz bestehender Warnungen stets stabil waren und nur dann als unstabil gekennzeichnet wurden, wenn während der Entwicklung neue Probleme eingefügt wurden.

Bis wir alle Warnungen wieder aktiviert und anschließend beseitigt hatten, verging etwa ein Jahr.

Zusammenfassung und Ausblick

Dieser Artikel hat gezeigt, wie die im ersten Teil aufgeführten Herausforderungen bei der Softwareentwicklung durch statische Code-Analyse-Tools gelöst werden können. Im dritten Teil werde ich die folgenden, alle zuvor genannten, Tools aus dem Java-Umfeld detaillierter vorstellen (hier alphabetisch sortiert):

  • Checkstyle
  • Cobertura
  • FindBugs / SpotBugs
  • Find Security Bugs
  • JaCoCo
  • PMD
  • Sonargraph Explorer
  • SonarLint
  • SonarQube

Bis dahin, happy Coding!

Hat dir der Artikel gefallen und kennst Du jemanden, der sich ebenfalls für das Thema interessiert? Dann freue ich mich, wenn Du den Artikel über einen der folgenden Buttons teilst.