Implementieren Sie statische Analysen in den Prozess, anstatt sie zum Auffinden von Fehlern zu verwenden

Den Anlass zum Schreiben dieses Artikels gab mir die große Menge an Materialien zur statischen Analyse, die mir zunehmend in den Sinn kommen. Erstens dies PVS-Studio-Blog, das sich auf Habré aktiv mit Hilfe von Überprüfungen von Fehlern bewirbt, die sein Tool in Open-Source-Projekten gefunden hat. Kürzlich wurde PVS-Studio implementiert Java-Unterstützung, und natürlich die Entwickler von IntelliJ IDEA, deren integrierter Analysator heute wahrscheinlich der fortschrittlichste für Java ist, konnte nicht wegbleiben.

Wenn man solche Rezensionen liest, hat man das Gefühl, dass es sich um ein magisches Elixier handelt: Drücken Sie den Knopf und schon haben Sie eine Liste mit Mängeln vor Augen. Es scheint, dass mit der Verbesserung der Analysegeräte immer mehr Fehler automatisch gefunden werden und die von diesen Robotern gescannten Produkte ohne unser Zutun immer besser werden.

Aber es gibt keine magischen Elixiere. Ich möchte darüber sprechen, worüber in Beiträgen wie „Hier sind die Dinge, die unser Roboter finden kann“ normalerweise nicht gesprochen wird: Was Analysatoren nicht können, was ihre wahre Rolle und Stellung im Softwarebereitstellungsprozess ist und wie man sie richtig implementiert .

Implementieren Sie statische Analysen in den Prozess, anstatt sie zum Auffinden von Fehlern zu verwenden
Ratsche (Quelle: Vikipedia).

Was statische Analysatoren niemals leisten können

Was ist Quellcode-Analyse aus praktischer Sicht? Wir stellen Quellcode als Eingabe bereit und erhalten als Ausgabe in kurzer Zeit (viel kürzer als bei der Durchführung von Tests) einige Informationen über unser System. Die grundlegende und mathematisch unüberwindbare Einschränkung besteht darin, dass wir auf diese Weise nur eine relativ enge Klasse von Informationen erhalten können.

Das bekannteste Beispiel für ein Problem, das mit statischer Analyse nicht gelöst werden kann, ist Abschaltproblem: Dies ist ein Theorem, das beweist, dass es unmöglich ist, einen allgemeinen Algorithmus zu entwickeln, der anhand des Quellcodes eines Programms bestimmen kann, ob es in einer endlichen Zeit eine Schleife durchläuft oder endet. Eine Erweiterung dieses Theorems ist Satz von Rice, die besagt, dass für jede nicht triviale Eigenschaft berechenbarer Funktionen die Bestimmung, ob ein beliebiges Programm eine Funktion mit einer solchen Eigenschaft auswertet, ein algorithmisch unlösbares Problem ist. Beispielsweise ist es unmöglich, einen Analysator zu schreiben, der anhand eines Quellcodes feststellen kann, ob das analysierte Programm eine Implementierung eines Algorithmus ist, der beispielsweise die Quadrierung einer ganzen Zahl berechnet.

Daher weist die Funktionalität statischer Analysatoren unüberwindbare Einschränkungen auf. Ein statischer Analysator wird niemals in allen Fällen in der Lage sein, Dinge wie beispielsweise das Auftreten einer „Nullzeiger-Ausnahme“ in Sprachen zu erkennen, die den Wert Null zulassen, oder in allen Fällen das Auftreten einer „Nullzeiger-Ausnahme“ zu bestimmen. Attribut nicht gefunden“ in dynamisch typisierten Sprachen. Der fortschrittlichste statische Analysator kann lediglich Sonderfälle hervorheben, deren Anzahl unter allen möglichen Problemen mit Ihrem Quellcode ohne Übertreibung nur ein Tropfen auf den heißen Stein ist.

Bei der statischen Analyse geht es nicht darum, Fehler zu finden

Daraus folgt die Schlussfolgerung: Die statische Analyse ist kein Mittel, um die Anzahl der Fehler in einem Programm zu reduzieren. Ich wage zu sagen: Wenn es zum ersten Mal auf Ihr Projekt angewendet wird, werden Sie „interessante“ Stellen im Code finden, aber höchstwahrscheinlich werden Sie keine Fehler finden, die die Qualität Ihres Programms beeinträchtigen.

Die Beispiele für Fehler, die von Analysatoren automatisch gefunden werden, sind beeindruckend, aber wir sollten nicht vergessen, dass diese Beispiele durch das Scannen einer großen Menge großer Codebasen gefunden wurden. Nach dem gleichen Prinzip finden Hacker, die die Möglichkeit haben, mehrere einfache Passwörter für eine große Anzahl von Konten auszuprobieren, letztendlich diejenigen Konten, die ein einfaches Passwort haben.

Bedeutet das, dass die statische Analyse nicht verwendet werden sollte? Natürlich nicht! Und genau aus dem gleichen Grund lohnt es sich, jedes neue Passwort zu überprüfen, um sicherzustellen, dass es in die Stoppliste der „einfachen“ Passwörter aufgenommen wird.

Statische Analyse ist mehr als nur das Finden von Fehlern

Tatsächlich sind die durch Analyse praktisch gelösten Probleme viel umfassender. Denn im Allgemeinen handelt es sich bei der statischen Analyse um jede Überprüfung von Quellcodes, die vor der Veröffentlichung durchgeführt wird. Hier sind einige Dinge, die Sie tun können:

  • Überprüfung des Codierungsstils im weitesten Sinne des Wortes. Dazu gehört sowohl die Überprüfung der Formatierung, die Suche nach der Verwendung leerer/zusätzlicher Klammern, das Festlegen von Schwellenwerten für Metriken wie die Anzahl der Zeilen/zyklomatische Komplexität einer Methode usw. – alles, was möglicherweise die Lesbarkeit und Wartbarkeit des Codes beeinträchtigt. In Java ist ein solches Tool Checkstyle, in Python - flake8. Programme dieser Klasse werden üblicherweise „Linters“ genannt.
  • Es kann nicht nur ausführbarer Code analysiert werden. Ressourcendateien wie JSON, YAML, XML, .properties können (und sollten!) automatisch auf Gültigkeit überprüft werden. Schließlich ist es besser, in einem frühen Stadium der automatischen Pull-Request-Überprüfung herauszufinden, dass die JSON-Struktur aufgrund einiger ungepaarter Anführungszeichen kaputt ist, als während der Testausführung oder Laufzeit? Entsprechende Werkzeuge stehen zur Verfügung: z.B. YAMLlint, JSONLint.
  • Kompilierung (oder Parsing für dynamische Programmiersprachen) ist ebenfalls eine Art statische Analyse. Im Allgemeinen sind Compiler in der Lage, Warnungen zu erzeugen, die auf Probleme mit der Quellcodequalität hinweisen und nicht ignoriert werden sollten.
  • Manchmal ist Kompilieren mehr als nur das Kompilieren von ausführbarem Code. Wenn Sie beispielsweise über eine Dokumentation im Format verfügen AsciiDoctor, dann im Moment der Umwandlung in HTML/PDF der AsciiDoctor-Handler (Maven Plugin) kann beispielsweise Warnungen vor fehlerhaften internen Links ausgeben. Und das ist ein guter Grund, den Pull Request mit Dokumentationsänderungen nicht anzunehmen.
  • Auch die Rechtschreibprüfung ist eine Art statische Analyse. Dienstprogramm ein Zauberspruch ist in der Lage, die Rechtschreibung nicht nur in der Dokumentation, sondern auch in Programmquellcodes (Kommentare und Literale) in verschiedenen Programmiersprachen, darunter C/C++, Java und Python, zu überprüfen. Auch ein Rechtschreibfehler in der Benutzeroberfläche oder Dokumentation ist ein Mangel!
  • Konfigurationstests (was sie sind – siehe. diese и diese Berichte) werden zwar in einer Unit-Test-Laufzeitumgebung wie Pytest ausgeführt, sind aber tatsächlich auch eine Art statische Analyse, da sie während ihrer Ausführung keine Quellcodes ausführen.

Wie Sie sehen, spielt die Suche nach Fehlern in dieser Liste die untergeordnete Rolle, und alles andere ist durch die Verwendung kostenloser Open-Source-Tools möglich.

Welche dieser Arten der statischen Analyse sollten Sie in Ihrem Projekt verwenden? Natürlich gilt: Je mehr, desto besser! Die Hauptsache ist die richtige Umsetzung, worauf später noch eingegangen wird.

Lieferpipeline als mehrstufiger Filter und statische Analyse als erste Stufe

Die klassische Metapher für kontinuierliche Integration ist eine Pipeline, durch die Änderungen fließen, von Änderungen am Quellcode über die Auslieferung bis hin zur Produktion. Die Standardsequenz der Phasen in dieser Pipeline sieht folgendermaßen aus:

  1. statische Analyse
  2. Zusammenstellung
  3. Unit-Tests
  4. Integrationstests
  5. UI-Tests
  6. manuelle Prüfung

Auf der N-ten Stufe der Pipeline abgelehnte Änderungen werden nicht auf die Stufe N+1 übertragen.

Warum genau so und nicht anders? Im Testteil der Pipeline erkennen Tester die bekannte Testpyramide.

Implementieren Sie statische Analysen in den Prozess, anstatt sie zum Auffinden von Fehlern zu verwenden
Testpyramide. Quelle: Beitrag Martin Fowler.

Am unteren Ende dieser Pyramide stehen Tests, die einfacher zu schreiben, schneller auszuführen sind und nicht zum Scheitern neigen. Daher sollte es mehr davon geben, sie sollten mehr Code abdecken und zuerst ausgeführt werden. An der Spitze der Pyramide ist das Gegenteil der Fall, sodass die Anzahl der Integrations- und UI-Tests auf das notwendige Minimum reduziert werden sollte. Der Mensch in dieser Kette ist die teuerste, langsamste und unzuverlässigste Ressource, er steht also ganz am Ende und führt die Arbeit nur aus, wenn in den vorherigen Stufen keine Mängel festgestellt wurden. Allerdings werden beim Aufbau einer Pipeline in Teilen, die nicht direkt mit dem Testen zusammenhängen, dieselben Prinzipien verwendet!

Ich möchte eine Analogie in Form eines mehrstufigen Wasserfiltersystems anbieten. Schmutziges Wasser (Änderungen bei Defekten) wird dem Eingang zugeführt; am Ausgang müssen wir sauberes Wasser erhalten, in dem alle unerwünschten Verunreinigungen beseitigt wurden.

Implementieren Sie statische Analysen in den Prozess, anstatt sie zum Auffinden von Fehlern zu verwenden
Mehrstufiger Filter. Quelle: Wikimedia Commons

Wie Sie wissen, sind Reinigungsfilter so konzipiert, dass jede weitere Kaskade einen immer feineren Anteil an Verunreinigungen herausfiltern kann. Gleichzeitig bieten gröbere Reinigungskaskaden einen höheren Durchsatz und geringere Kosten. In unserer Analogie bedeutet dies, dass Input-Quality-Gates schneller sind, weniger Aufwand beim Starten erfordern und selbst unprätentiöser im Betrieb sind – und das ist die Reihenfolge, in der sie erstellt werden. Die Rolle der statischen Analyse, die, wie wir jetzt verstehen, in der Lage ist, nur die gröbsten Fehler auszusortieren, ist die Rolle des „Schlamm“-Gitters ganz am Anfang der Filterkaskade.

Die statische Analyse allein verbessert die Qualität des Endprodukts nicht, ebenso wenig wie ein „Schlammfilter“ Wasser nicht trinkbar macht. Und doch ist seine Bedeutung im Zusammenspiel mit anderen Elementen der Pipeline offensichtlich. Obwohl bei einem mehrstufigen Filter die Ausgangsstufen potenziell in der Lage sind, alles zu erfassen, was die Eingangsstufen tun, ist es klar, welche Konsequenzen sich daraus ergeben, wenn man versucht, nur mit Feinreinigungsstufen ohne Eingangsstufen auszukommen.

Der Zweck der „Schlammfalle“ besteht darin, nachfolgende Kaskaden davon abzuhalten, sehr grobe Mängel zu erkennen. Beispielsweise sollte die Person, die die Codeüberprüfung durchführt, zumindest nicht durch falsch formatierten Code und Verstöße gegen etablierte Codierungsstandards (wie zusätzliche Klammern oder zu tief verschachtelte Zweige) abgelenkt werden. Fehler wie NPEs sollten durch Unit-Tests erkannt werden, aber wenn der Analysator uns bereits vor dem Test anzeigt, dass ein Fehler zwangsläufig auftreten wird, beschleunigt dies seine Behebung erheblich.

Ich glaube, es ist jetzt klar, warum die statische Analyse bei gelegentlicher Anwendung die Qualität des Produkts nicht verbessert und ständig verwendet werden sollte, um Änderungen mit groben Mängeln herauszufiltern. Die Frage, ob die Verwendung eines statischen Analysegeräts die Qualität Ihres Produkts verbessert, entspricht in etwa der Frage: „Wird die Trinkwasserqualität von Wasser aus einem schmutzigen Teich verbessert, wenn es durch ein Sieb geleitet wird?“

Implementierung in ein Legacy-Projekt

Eine wichtige praktische Frage: Wie kann die statische Analyse als „Quality Gate“ in den kontinuierlichen Integrationsprozess implementiert werden? Bei automatischen Tests ist alles klar: Es gibt eine Reihe von Tests, das Scheitern eines von ihnen ist ein ausreichender Grund zu der Annahme, dass die Baugruppe das Qualitätstor nicht bestanden hat. Ein Versuch, ein Gate auf die gleiche Weise basierend auf den Ergebnissen einer statischen Analyse zu installieren, schlägt fehl: Der Legacy-Code enthält zu viele Analysewarnungen, die Sie nicht vollständig ignorieren möchten, aber es ist auch unmöglich, den Versand eines Produkts zu stoppen nur weil es Analysatorwarnungen enthält.

Bei der ersten Verwendung erzeugt der Analysator bei jedem Projekt eine Vielzahl von Warnungen, von denen die überwiegende Mehrheit nichts mit der ordnungsgemäßen Funktion des Produkts zu tun hat. Es ist unmöglich, alle diese Kommentare auf einmal zu korrigieren, und viele sind auch nicht notwendig. Schließlich wissen wir schon vor Einführung der statischen Analyse, dass unser Produkt als Ganzes funktioniert!

Daher beschränken sich viele auf die gelegentliche Verwendung der statischen Analyse oder verwenden sie nur im Informationsmodus, wenn während der Montage einfach ein Analysebericht ausgegeben wird. Dies ist gleichbedeutend mit dem Fehlen jeglicher Analyse, denn wenn wir bereits viele Warnungen haben, bleibt das Auftreten einer weiteren (egal wie schwerwiegend) beim Ändern des Codes unbemerkt.

Folgende Methoden zur Einführung von Quality Gates sind bekannt:

  • Festlegen eines Grenzwerts für die Gesamtzahl der Warnungen oder die Anzahl der Warnungen dividiert durch die Anzahl der Codezeilen. Dies funktioniert schlecht, da ein solches Tor Änderungen mit neuen Defekten ungehindert durchlässt, solange deren Grenzwert nicht überschritten wird.
  • Zu einem bestimmten Zeitpunkt werden alle alten Warnungen im Code als ignoriert korrigiert und die Erstellung wird verweigert, wenn neue Warnungen auftreten. Diese Funktionalität wird von PVS-studio und einigen Online-Ressourcen, beispielsweise Codacy, bereitgestellt. Ich hatte keine Gelegenheit, in PVS-Studio zu arbeiten, da das Hauptproblem meiner Erfahrung mit Codacy darin besteht, dass die Bestimmung, was ein „alter“ und was ein „neuer“ Fehler ist, ein ziemlich komplexer Algorithmus ist, der nicht immer funktioniert korrekt, insbesondere wenn Dateien stark verändert oder umbenannt werden. Meiner Erfahrung nach konnte Codacy neue Warnungen in einem Pull-Request ignorieren und gleichzeitig einen Pull-Request aufgrund von Warnungen, die nicht mit Änderungen im Code einer bestimmten PR in Zusammenhang standen, nicht weiterleiten.
  • Die meiner Meinung nach effektivste Lösung wird im Buch beschrieben Kontinuierliche Liefer „Ratschenmethode“. Die Grundidee besteht darin, dass die Anzahl der statischen Analysewarnungen eine Eigenschaft jeder Version ist und nur Änderungen zulässig sind, die die Gesamtzahl der Warnungen nicht erhöhen.

Ratsche

Es funktioniert so:

  1. In der Anfangsphase wird in den Metadaten über die Veröffentlichung die Anzahl der von den Analysatoren gefundenen Warnungen im Code erfasst. Wenn Sie also Upstream erstellen, schreibt Ihr Repository-Manager nicht nur „Release 7.0.2“, sondern „Release 7.0.2 mit 100500 Checkstyle-Warnungen“. Wenn Sie einen erweiterten Repository-Manager (z. B. Artifactory) verwenden, ist das Speichern solcher Metadaten zu Ihrer Veröffentlichung einfach.
  2. Jetzt vergleicht jeder Pull-Request beim Erstellen die Anzahl der resultierenden Warnungen mit der Anzahl der in der aktuellen Version verfügbaren Warnungen. Wenn PR zu einer Erhöhung dieser Zahl führt, passiert der Code das Qualitätstor für die statische Analyse nicht. Wenn die Anzahl der Warnungen abnimmt oder sich nicht ändert, wird die Prüfung bestanden.
  3. Beim nächsten Release wird die neu berechnete Anzahl der Warnungen erneut in den Release-Metadaten erfasst.

Nach und nach, aber stetig (z. B. wenn eine Ratsche funktioniert), tendiert die Anzahl der Warnungen gegen Null. Natürlich kann das System getäuscht werden, indem man eine neue Warnung einführt, aber die Warnung eines anderen korrigiert. Das ist normal, denn über eine große Distanz führt es zu Ergebnissen: Warnungen werden in der Regel nicht einzeln, sondern in einer Gruppe einer bestimmten Art auf einmal korrigiert und alle leicht entfernbaren Warnungen werden recht schnell beseitigt.

Diese Grafik zeigt die Gesamtzahl der Checkstyle-Warnungen für einen sechsmonatigen Betrieb einer solchen „Ratsche“. eines unserer OpenSource-Projekte. Die Zahl der Warnungen ist um eine Größenordnung zurückgegangen, und das geschah natürlich parallel zur Produktentwicklung!

Implementieren Sie statische Analysen in den Prozess, anstatt sie zum Auffinden von Fehlern zu verwenden

Ich verwende eine modifizierte Version dieser Methode, bei der Warnungen nach Projektmodul und Analysetool separat gezählt werden. Das Ergebnis ist eine YAML-Datei mit Build-Metadaten, die etwa so aussieht:

celesta-sql:
  checkstyle: 434
  spotbugs: 45
celesta-core:
  checkstyle: 206
  spotbugs: 13
celesta-maven-plugin:
  checkstyle: 19
  spotbugs: 0
celesta-unit:
  checkstyle: 0
  spotbugs: 0

In jedem fortschrittlichen CI-System kann Ratchet für alle statischen Analysetools implementiert werden, ohne auf Plugins und Tools von Drittanbietern angewiesen zu sein. Jeder Analysator erstellt seinen eigenen Bericht in einem einfachen Text- oder XML-Format, der leicht zu analysieren ist. Jetzt muss nur noch die nötige Logik in das CI-Skript geschrieben werden. Wie dies umgesetzt wird, können Sie in unseren Open-Source-Projekten auf Basis von Jenkins und Artifactory sehen hier oder hier. Beide Beispiele sind bibliotheksabhängig ratchetlib: Methode countWarnings() zählt XML-Tags in von Checkstyle und Spotbugs generierten Dateien auf die übliche Weise und compareWarningMaps() implementiert die gleiche Ratsche und löst einen Fehler aus, wenn die Anzahl der Warnungen in einer der Kategorien zunimmt.

Eine interessante Implementierung des „Ratchet“ ist zur Analyse der Rechtschreibung von Kommentaren, Textliteralen und Dokumentationen mit Aspell möglich. Wie Sie wissen, sind bei der Rechtschreibprüfung nicht alle im Standardwörterbuch unbekannten Wörter falsch; sie können dem Benutzerwörterbuch hinzugefügt werden. Wenn Sie ein benutzerdefiniertes Wörterbuch zu einem Teil des Quellcodes des Projekts machen, kann das Qualitätstor für die Rechtschreibung folgendermaßen formuliert werden: Ausführen von aspell mit einem Standard- und einem benutzerdefinierten Wörterbuch sollte nicht finde keine Rechtschreibfehler.

Über die Wichtigkeit, die Version des Analysegeräts zu reparieren

Zusammenfassend ist festzuhalten, dass unabhängig davon, wie Sie die Analyse in Ihre Lieferpipeline implementieren, die Version des Analysators festgelegt sein muss. Wenn Sie zulassen, dass der Analysator spontan aktualisiert wird, können beim Zusammenstellen der nächsten Pull-Anfrage neue Fehler „auftauchen“, die nicht mit Codeänderungen zusammenhängen, sondern damit zusammenhängen, dass der neue Analysator einfach mehr Fehler finden kann – und dies wird Ihren Prozess der Annahme von Pull-Anfragen unterbrechen. Das Aufrüsten eines Analysegeräts sollte eine bewusste Maßnahme sein. Allerdings ist die starre Fixierung der Version jeder Baugruppenkomponente im Allgemeinen eine notwendige Anforderung und ein Thema für eine gesonderte Diskussion.

Befund

  • Durch die statische Analyse werden für Sie keine Fehler gefunden und die Qualität Ihres Produkts wird nicht durch eine einzige Anwendung verbessert. Eine positive Auswirkung auf die Qualität kann nur durch den kontinuierlichen Einsatz im Lieferprozess erzielt werden.
  • Das Finden von Fehlern ist überhaupt nicht die Hauptaufgabe der Analyse; die allermeisten nützlichen Funktionen sind in Open-Source-Tools verfügbar.
  • Implementieren Sie Quality Gates basierend auf den Ergebnissen der statischen Analyse in der allerersten Phase der Lieferpipeline, indem Sie eine „Ratsche“ für Legacy-Code verwenden.

Referenzen

  1. Kontinuierliche Liefer
  2. A. Kudryavtsev: Programmanalyse: Wie erkennt man, dass man ein guter Programmierer ist? Bericht über verschiedene Methoden der Code-Analyse (nicht nur statisch!)

Source: habr.com

Kommentar hinzufügen