In den letzten Wochen setzte ich mich mit verschiedenen Werkzeugen zur Verbesserung meines Codes und dem Code anderer auseinander. Teilweise laß und hörte ich, dass durch statische Code-Analyse sowie Code-Coverage, einfach gesagt, der eigene Code und die eigene Software besser wird. Dabei hatte ich ein flaues Gefühl im Magen.
Ich sah mir also folgende Dinge an: SonarQube für die statische Analyse von Quellcode, Google-Test mit gcov beziehungsweise llvm-cov für Code-Coverage, aber auch zusätzlich clang-format und clang-tidy, um einheitliche Codeformatierung hinzubekommen und bereits Warnings zur Bearbeitungszeit zu Problemen in meinem Code. Dies versuchte ich mittels VIM und ALE abzudecken, wobei es da zu größeren, bisher ungelösten Problemen kam.
SonarQube
SonarQube gibt es in verschiedenen Lizensierungsmodellen. Neben der Community-Edition, die nur wenige Programmier- und Auszeichnungssprachen beherrscht, gibt es beispielsweise auch die Developer-Edition, die einige mehr kann. Meine Erfahrungen habe ich mit C++-Code versucht, zu machen. Dementsprechend kommt man um die Developer-Edition nicht herum. Diese ist kostenpflichtig und wird nach zu analysierenden Zeilen von Code gezahlt. Das kann schnell teuer werden, zum jetzigen Zeitpunkt, Oktober 2023, kostet das System 150 USD für 100.000 Zeilen Quellcode pro Jahr. SonarQube unterteilt sich in unterschiedliche Programme, sofern man es für C++ (und C und Objective-C) einsetzt: Die Website, auf der die Ergebnisse illustriert werden, den build-wrapper, der aber auch durch compiledb ersetzt werden kann und den sonar-scaner. Die SonarQube-Werkzeuge sind in Java geschrieben und bringen ihr eigenes JRE mit, können aber auch mit einem anderen genutzt werden. Hier ist Vorsicht geboten, sind die Scripte, die die SonarQube-Komponenten aufrufen, in der Shebang doch fest auf /bin/bash gestellt, was auf verschiedenen Betriebssystemen zu Problemen führen kann.
Nicht jedes jedes System wird mit allen Tools unterstützt. Beispielsweise wird macOS arm64 nicht vom build-wrapper unterstützt, so dass man schlussendlich auf compiledb ausweichen muss. Das impliziert: verschiedene Architekturen, verschiedene Tools. Darum soll es hier aber nicht gehen.
Der Vorgang ist folgender: nachdem SonarQube installiert und initial konfiguriert wurde, erstellt man sein erstes eigenes Projekt und erhält einen Token, den man dann beim sonar-scanner entweder auf der Kommandozeile übergibt oder in die SonarQube-Konfiguration einträgt. Dann lässt man sein Make-System einmal mit dem build-wrapper oder compiledb durchlaufen, damit SonarQube weitere Informationen erhält. Zum Schluß wird der sonar-scanner gestartet, der dann die Code-Analyse durchführt, das Coverage verarbeitet, das aber durch andere Tools vorher erstellt werden muss, und die Ergebnisse ins Internet schiebt, so dass sie auf der Website erscheinen.
So, wie ich das verstanden habe, besteht die Analyse aus verschiedenen Teilbereichen. Es wird teils nach echten Bugs geschaut, teils nach sogennanten Code-Smells. SonarQube erkannte tatsächlich sowas:
auto *x = new y();
if (x) {
//
} else {
x->method();
}
Aber auch sowas:
const int x = 10;
return x == 10 ? a : b;
SonarQube stufte diese Fehler als Bugs ein und markierte sie als kritisch, was auch gut war. Womit ich nicht zufrieden war, war die Kategorisierung der anderen Regeln. Dies lässt sich in der Konfiguration zwar abändern, aber ich gebe diese zwei Beispiele trotzdem. Bei beiden geht es um sogenannte Code-Smells. Dabei handelt es sich um Code-Konventionen, die keine Bugs sind, sondern dem in SonarQube eingestellten Regelwerk unterliegen und gefixt werden sollten. Im ersten Fall gilt eine Einrückungstiefe innerhalb von Kontrollstrukturen tiefer als drei als kritisch:
if (true) {
if (true) {
if (true) {
if (true) {
}
}
}
}
Im Gegensatz dazu wurde folgender Code “nur” als Major eingestuft, obwohl er meiner Meinung nach kritisch sein sollte:
int var = 10;
if (true)
int var = 5;
Man sieht klar, dass das Regelwerk für jeden Programmierer oder jedes Projekt oder jedes Team innerhalb verschiedener Konstellationen angepasst werden muss.
Aber kommen wir zur wichtigen Frage: wird mein Code und/oder meine Software durch den Einsatz von statischer Code-Analyse, wie mit SonarQube, automatisch gut?
Meine Antwort darauf ist nein. Das Positive war, dass SonarQube im Projekt zwei potentielle Bugs gefunden hat, das meiste Andere war eher kosmetischer Natur. Dabei kennt SonarQube keinen Kontext. Es prüft nicht, ob Abläufe richtig sind, ob externe Bibliotheken mit hineinspielen, ob Toolkits bestimmte Voraussetzungen haben, an die man sich halten muss und es prüft natürlich auch nicht die Logik oder Algorithmen. Weiterhin verpflichtet SonarQube nicht. Mittels // NOSONAR direkt im Code schaltet man alle Prüfungen für die Zeile ab. Je nach Situation macht das durchaus Sinn, kann aber natürlich auch vom “faulen” Programmierer missbraucht werden.
Ein anderes Problem ist, dass je nach Regelwerk der Code komplexer und auch schwieriger zu lesen wird und der Programmierer mitunter beginnt, an diesem Regelwerk vorbeizuarbeiten. Je nach Firma und Verständnis ist die Übersichtseite (z.B. im Falle von SonarQube) natürlich gut für jeden einsehbar und auch Entscheidungsträger können hier anhand von roten, gelben und grünen Symbolen auf die vermeintliche Qualität der Software schielen und Mitarbeiter dementsprechend beurteilen. Somit könnte der Programmierer natürlich versuchen, gewisse, nervende Regeln zu umgehen. Ein Beispiel: In SonarQube dürfen Klassen aus nicht mehr als 35 Methoden und 20 Membern bestehen. Spätestens bei GUIs kann das schnell zum Problem werden. Diese Klassen dann aufzusplitten, erhöht den Aufwand, die Komplexität und bietet aber mitunter keinen vernünftigen Nutzwert. In diesem Fall könnte der Programmierer beginnen – bei C++ – mit inneren Klassen und/oder Structs zu arbeiten, um dieser Problematik zu entgehen, da er sicher nicht die gesamte GUI-Datei in der Konfiguration ausklammern möchte. Dabei könnte dann folgendes herauskommen:
class MyGUI : public GUI {
public:
struct UI {
Button b0;
Button b1;
};
UI ui;
};
Das stimmt SonarQube an der Stelle glücklich. Wie man aber sieht, wird der Code selbst umfangreicher.
Die Qualität kann zunehmen, muss sie aber nicht. Für mich ist an der Stelle statische Codeanalyse ein Werkzeug für mich, aber absolut kein Garant für guten Code, noch für gute Software.
Code-Coverage
Code-Coverage ist vom Wording meiner Meinung nach nicht ganz korrekt. Test-Code-Coverage oder ähnlich sollte es eher immer heißen.
Damit ist gemeint, dass mit Testszenarien (Unit-Tests, Intergrations-Tests, …) eine gewisse Menge Code getestet wird, also abgedeckt ist. Nimmt man hier als Beispiel gcov, ist es recht einfach. Man kompiliert sein Programm mit –coverage und führt es aus. Beim Kompilieren und Ausführen werden verschiedene Parameter gesammelt. Wichtig hier ist, welche Zeile im Code aufgerufen wird. Wird sie mindestens einmal aufgerufen, gilt sie als abgedeckt.
Hier sieht man, dass mitunter Funktionen oder Methoden selbst nicht explizit aufgerufen werden, sondern implizit und dennoch gelten sie als gecovert. Auch sagt Code-Coverage nichts darüber aus, wie sinnvoll ein Test ist, ob es nicht noch weitere Tests geben müsste, ob wirklich alles getestet ist und damit, ob die Qualität der eigenen Software verbessert wurde.
Noch ein Wort zu Formattern und Lintern
Beide Teile, also Formatter und Linter, sind spätestens im Team natürlich durchaus praktisch. Der Linter zeigt an, wo es noch Probleme bei Konventionen im Code geben könnte. Hier funktioniert clang-tidy recht gut. Der Formatter formatiert den Code so, dass er für alle gleich ist. Sprich, er achtet auf Einrückungen und ähnliches.
Fazit
Also, was denke ich? Wenn man diese Programme nutzt, als das, was sie sind, können sie dem Programmierer durchaus nützlich sein. Selbst, wenn man aus verschiedenen Gründen nicht alle Regeln einhalten kann oder möchte, regen sie doch zum Nachdenken an. Aber sie sichern selbst nicht die Qualität des Codes. Das muss der Entwickler selbst tun, wobei es sich hier um einen iterativen Prozess handeln kann.
Diese Tools dürfen aber niemals zum Einsatz gegen den Programmierer genutzt werden, sondern bei Problemen sollte dieser immer erst befragt werden.