Integrieren Sie statische Analyse in den Prozess, anstatt sie zur Fehlersuche zu verwenden.

Der Anstoß zum Verfassen dieses Artikels war die zunehmende Anzahl von Materialien über statische Analysen, die mir immer häufiger begegneten. Erstens handelt es sich um den PVS-Studio Blog, der sich aktiv auf Habr mit Berichten über Fehler präsentiert, die mit ihrem Tool in Open-Source-Projekten gefunden wurden. Kürzlich hat PVS-Studio Java-Unterstützungimplementiert und natürlich konnten die Entwickler von IntelliJ IDEA, deren integrierter Analysator heute vielleicht der fortschrittlichste für Java ist, nicht untätig bleiben..

Beim Lesen solcher Berichte hat man das Gefühl, es ginge um einen magischen Zaubertrank: Drücke einen Knopf und voilà – eine Liste von Defekten erscheint vor deinen Augen. Es scheint, dass mit der Weiterentwicklung der Analysatoren immer mehr Fehler automatisch gefunden werden, und die Produkte, die von diesen Tools scannen, immer besser werden, ohne dass wir dafür irgendwelche Anstrengungen aufbringen müssen.

Aber magische Zaubertränke gibt es nicht. Ich möchte darüber sprechen, was in Beiträgen wie „Das kann unser Roboter finden“ normalerweise nicht erwähnt wird: Was Analysatoren nicht können, welche ihre tatsächliche Rolle und ihren Platz im Softwarebereitstellungsprozess ist und wie man sie richtig implementiert.

Integrieren Sie statische Analyse in den Prozess, anstatt sie zur Fehlersuche zu verwenden.
Kratzer (Quelle: Wikipedia).

Was statische Analysatoren nie leisten können

Was bedeutet es aus praktischer Sicht, Quellcode zu analysieren? Wir geben einige Quellcodes ein und erhalten in kurzer Zeit (deutlich kürzer als bei Testläufen) bestimmte Informationen über unser System. Eine 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 — das Halteproblem: Dies ist ein Theorem, das beweist, dass es unmöglich ist, einen allgemeinen Algorithmus zu entwickeln, der anhand des Quellcodes eines Programms bestimmt, ob es sich in eine Endlosschleife begibt oder in endlicher Zeit endet. Eine Erweiterung dieses Theorems ist das Rice-Theorem, die besagt, dass die Bestimmung, ob ein beliebiges Programm eine Funktion mit einem nichttrivialen Eigenschaften berechnet, algorithmisch unlösbar ist. Zum Beispiel ist es unmöglich, einen Analyzer zu schreiben, der für jeden Quellcode bestimmt, ob das analysierte Programm eine Implementierung eines Algorithmus ist, der beispielsweise eine Ganzzahl quadriert.

Daher hat die Funktionalität statischer Analyzer unüberwindbare Einschränkungen. Ein statischer Analyzer kann niemals in allen Fällen Dinge wie das Auftreten einer „Null-Pointer-Exception“ in Sprachen, die null zulassen, oder das Auftreten von „Attribut nicht gefunden“ in dynamisch typisierten Sprachen bestimmen. Alles, was der perfekteste statische Analyzer kann, ist, spezielle Fälle hervorzuheben, deren Anzahl im Vergleich zu allen möglichen Problemen mit Ihrem Quellcode, gelinde gesagt, ein Tropfen auf den heißen Stein ist.

Statische Analyse ist nicht die Suche nach Bugs

Aus dem Vorstehenden lässt sich folgern: statische Analyse ist kein Mittel zur Verringerung der Anzahl von Fehlern im Programm. Ich wage zu behaupten: Wenn sie erstmals auf Ihr Projekt angewendet wird, wird sie in Ihrem Code "interessante" Stellen finden, aber wahrscheinlich keine Fehler, die die Qualität Ihrer Anwendung beeinträchtigen.

Die Beispiele für Fehler, die von Analysewerkzeugen automatisch gefunden wurden, sind beeindruckend, aber man sollte nicht vergessen, dass diese Beispiele durch das Scannen großer Mengen umfangreicher Codebasen gefunden wurden. Nach dem gleichen Prinzip finden Hacker, die in der Lage sind, mehrere einfache Passwörter auf einer Vielzahl von Konten zu testen, letztendlich die Konten, die ein einfaches Passwort verwenden.

Bedeutet das, dass statische Analyse nicht angewendet werden sollte? Natürlich nicht! Und genau aus dem gleichen Grund, aus dem jedes neue Passwort auf seine Zugehörigkeit zur Liste "einfacher" Passwörter überprüft werden sollte.

Statische Analyse ist mehr als nur Fehlersuche.

In der Tat sind die durch Analysen lösbaren Aufgaben viel umfassender. Grundsätzlich ist eine statische Analyse jede Überprüfung des Quellcodes, die vor seiner Ausführung stattfindet. Hier sind einige Dinge, die man tun kann:

  • Überprüfung des Codestils im weitesten Sinne. Dies umfasst sowohl die Formatierung als auch die Suche nach leeren oder überflüssigen Klammern, die Festlegung von Schwellenwerten für Metriken wie die Anzahl der Zeilen oder die zyklomatische Komplexität einer Methode usw. — alles, was die Lesbarkeit und Wartbarkeit des Codes potenziell erschwert. In Java ist ein solches Tool Checkstyle, in Python flake8. Programme dieser Art werden üblicherweise als 'Linter' bezeichnet.
  • Nicht nur der ausführbare Code kann analysiert werden. Ressourcen-Dateien wie JSON, YAML, XML, .properties sollten (und müssen!) automatisch auf Validität geprüft werden. Es ist besser, frühzeitig bei der automatischen Überprüfung eines Pull Requests zu erfahren, dass die Struktur von JSON aufgrund ungerader Anführungszeichen verletzt wurde, als beim Ausführen von Tests oder zur Laufzeit. Entsprechende Werkzeuge sind vorhanden: Zum Beispiel YAMLlint, JSONLint.
  • Die Kompilierung (oder das Parsen für dynamische Programmiersprachen) ist ebenfalls eine Form der statischen Analyse. In der Regel sind Compiler in der Lage, Warnungen auszugeben, die auf Probleme mit der Qualität des Quellcodes hinweisen, und diese sollten nicht ignoriert werden.
  • Manchmal ist die Kompilierung nicht nur die Erstellung ausführbaren Codes. Wenn Sie beispielsweise Dokumentation im Format AsciiDoctor, haben, kann der AsciiDoctor-Prozessor (Maven-Plugin) beim Umwandeln in HTML/PDF Warnungen ausgeben, etwa zu verletzten internen Links. Das ist ein legitimer Grund, einen Pull Request mit Änderungen an der Dokumentation abzulehnen.
  • Die Rechtschreibprüfung ist ebenfalls eine Form der statischen Analyse. Das Tool aspell kann die Rechtschreibung nicht nur in der Dokumentation, sondern auch im Quellcode von Programmen (Kommentaren und Literalen) in verschiedenen Programmiersprachen wie C/C++, Java und Python überprüfen. Ein Rechtschreibfehler in der Benutzeroberfläche oder in der Dokumentation ist ebenfalls ein Mangel!
  • Konfigurationstests (was das ist, siehe diese und diese Berichte, obwohl sie in der Ausführungsumgebung von Modultests wie pytest durchgeführt werden, sind tatsächlich eine Form der statischen Analyse, da sie den Quellcode während ihrer Ausführung nicht ausführen.

Wie wir sehen, spielt das Finden von Bugs in dieser Liste die unwichtigste Rolle, während alles andere über die Nutzung kostenloser Open-Source-Tools zugänglich ist.

Welche dieser Arten von statischer Analyse sollten Sie in Ihrem Projekt anwenden? Natürlich alle, je mehr, desto besser! Es ist wichtig, dass Sie dies richtig implementieren, worüber wir später sprechen werden.

Die Lieferpipeline als mehrstufiger Filter und die statische Analyse als ihr erster Sprung.

Eine klassische Metapher für kontinuierliche Integration ist die Pipeline, durch die Änderungen fließen – vom Änderungen des Quellcodes bis zur Bereitstellung in der Produktion. Die Standardabfolge der Schritte in dieser Pipeline sieht folgendermaßen aus:

  1. statische Analyse
  2. Kompilierung
  3. Modultests
  4. Integrationstests
  5. UI-Tests
  6. manuelle Überprüfung

Änderungen, die in der N-ten Phase der Pipeline abgelehnt werden, gelangen nicht zur Phase N+1.

Warum ist das so und nicht anders? Im Teil des Prozesses, der das Testing betrifft, lernen Tester die weit verbreitete Testpyramide kennen.

Integrieren Sie statische Analyse in den Prozess, anstatt sie zur Fehlersuche zu verwenden.
Testpyramide. Quelle: Artikel Martin Fowler.

In der unteren Hälfte dieser Pyramide befinden sich Tests, die leichter zu schreiben sind, schneller ausgeführt werden können und weniger anfällig für Fehlalarme sind. Daher sollte es mehr von ihnen geben, sie sollten mehr Code abdecken und zuerst ausgeführt werden. In der oberen Hälfte der Pyramide ist es genau umgekehrt, weshalb 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, weshalb er am Ende angestellt wird und nur dann arbeitet, wenn die vorherigen Schritte keine Mängel entdeckt haben. Doch nach denselben Prinzipien wird auch der Prozess in Bereichen aufgebaut, die nicht direkt mit dem Testing verbunden sind!

Ich möchte eine Analogie in Form eines mehrstufigen Wasserfilters vorschlagen. Im Eingangsbereich wird schmutziges Wasser (Änderungen mit Mängeln) eingespeist, und am Ausgang sollten wir reines Wasser erhalten, aus dem alle unerwünschten Verunreinigungen herausgefiltert sind.

Integrieren Sie statische Analyse in den Prozess, anstatt sie zur Fehlersuche zu verwenden.
Mehrstufiger Filter. Quelle: Wikimedia Commons

Wie bekannt, sind Reinigungssysteme so konzipiert, dass jede nachfolgende Stufe immer feinere Fraktionen von Verunreinigungen herausfiltern kann. Dabei haben die groben Filterstufen eine höhere Durchflussrate und niedrigere Kosten. In unserer Analogie bedeutet das, dass die eingehenden Qualitätspunkte eine höhere Leistungsfähigkeit aufweisen, weniger Aufwand für den Betrieb benötigen und selbst in der Handhabung unkomplizierter sind – und genau in dieser Reihenfolge sind sie aufgebaut. Die Rolle der statischen Analyse, die, wie wir jetzt verstehen, lediglich die grobsten Mängel herausfiltern kann, entspricht der Funktion eines „Schmutzgitters“ zu Beginn der Filterstufen.

Statische Analysen allein verbessern nicht die Qualität des Endprodukts, ebenso wie ein Grobfilter Wasser nicht trinkbar macht. Dennoch ist ihre Bedeutung im Zusammenspiel mit anderen Elementen des Prozesses offensichtlich. Obwohl in einem mehrstufigen Filter die Ausgangsstufen theoretisch alles fangen können, was auch die Eingangs-stufen erfassen, sind die Konsequenzen einer ausschließlichen Abhängigkeit von nur feinen Filter-stufen ohne Eingangs-stufen klar.

Das Ziel eines Grobfilters ist es, die nachfolgenden Stufen von der Erfassung wirklich grober Fehler zu entlasten. Zum Beispiel sollte jemand, der eine Code-Überprüfung durchführt, nicht durch schlecht formatierten Code oder Verstöße gegen die festgelegten Codierungsstandards (wie überflüssige Klammern oder zu tief verschachtelte Verzweigungen) abgelenkt werden. Bugs wie NPE sollten durch Modultests erfasst werden, aber wenn der Analysator bereits vor dem Test darauf hinweist, dass der Bug unvermeidlich ist – beschleunigt das erheblich dessen Behebung.

Es sollte nun klar sein, warum statische Analysen die Produktqualität nicht verbessern, wenn sie sporadisch angewendet werden. Sie müssen kontinuierlich eingesetzt werden, um Änderungen mit groben Fehlern auszusondern. Die Frage, ob der Einsatz eines statischen Analysetools die Qualität Ihres Produkts verbessert, ist in etwa so sinnvoll wie die Frage: „Wird sich die Trinkqualität von Wasser aus einem schmutzigen Teich verbessern, wenn man es durch ein Sieb gießen?“

Integration in ein Legacy-Projekt

Eine wichtige praktische Frage: Wie integrieren wir statische Analysen in den kontinuierlichen Integrationsprozess als „Qualitätscheck“? Bei automatisierten Tests ist alles klar: Es gibt eine Reihe von Tests, und das Scheitern eines dieser Tests reicht aus, um zu sagen, dass der Build den Qualitätscheck nicht bestanden hat. Der Versuch, einen Qualitätscheck anhand der Ergebnisse der statischen Analyse einzuführen, scheitert: Bei Legacy-Code gibt es zu viele Warnungen aus der Analyse. Man möchte sie nicht vollständig ignorieren, aber die Produktbereitstellung nur aufgrund von Warnungen des Analysetools zu stoppen, ist ebenfalls nicht möglich.

Bei der ersten Anwendung zeigt der Analyzer eine enorme Anzahl von Warnungen an, von denen die überwiegende Mehrheit nicht mit der ordnungsgemäßen Funktion des Produkts zusammenhängt. Es ist unmöglich, all diese Anmerkungen sofort zu beheben, und viele sind es auch nicht wert. Schließlich wissen wir, dass unser Produkt insgesamt funktioniert, und das sogar vor der Einführung der statischen Analyse!

Infolgedessen beschränken sich viele auf eine sporadische Nutzung der statischen Analyse oder verwenden sie lediglich im Informationsmodus, bei dem beim Zusammenbau einfach ein Bericht des Analyzers ausgegeben wird. Das ist gleichbedeutend mit dem Fehlen jeglicher Analyse, denn wenn wir bereits zahlreiche Warnungen haben, bleibt das Auftreten einer weiteren (wie auch immer schwerwiegenden) Änderung im Code unbemerkt.

Die folgenden Methoden zur Einführung von Quality Gates sind bekannt:

  • Setzen eines Limits für die Gesamtzahl der Warnungen oder der Anzahl der Warnungen im Verhältnis zur Anzahl der Codezeilen. Das funktioniert schlecht, da ein solches Gate Veränderungen mit neuen Defekten ohne weiteres durchlässt, solange das Limit nicht überschritten wird.
  • Die Fixierung aller alten Warnungen im Code zu einem bestimmten Zeitpunkt als ignoriert und das Abbrechen der Build-Prozesse bei neuen Warnungen. Diese Funktionalität bietet PVS-Studio und einige Online-Dienste wie Codacy an. Ich hatte noch nicht die Gelegenheit, mit PVS-Studio zu arbeiten, aber meiner Erfahrung mit Codacy nach, besteht ihr größtes Problem darin, dass die Erkennung, was als "alte" und was als "neue" Fehlermeldung gilt, ein recht komplexer und nicht immer korrekt funktionierender Algorithmus ist, besonders wenn Dateien stark verändert oder umbenannt werden. Soweit ich mich erinnere, konnte Codacy in Pull-Requests neue Warnungen übersehen und gleichzeitig einen Pull-Request aufgrund von Warnungen ablehnen, die sich nicht auf die Änderungen im Code dieses PR bezogen.
  • Meiner Meinung nach ist die effektivste Lösung die in dem Buch beschriebene Continuous Delivery „Ratschenmethode“ („ratcheting“). Die Hauptidee besteht darin, dass jede Version eine bestimmte Anzahl statischer Analysewarnungen aufweist und nur solche Änderungen zulässig sind, die die Gesamtanzahl der Warnungen nicht erhöhen.

Ratsche

Das funktioniert folgendermaßen:

  1. In der ersten Phase werden Metadaten über die Anzahl der im Code gefundenen Warnungen, die von den Analysewerkzeugen erkannt wurden, erfasst. So wird beim Build des Hauptzweigs in Ihrem Repository-Manager nicht nur "Release 7.0.2" aufgezeichnet, sondern "Release 7.0.2, das 100500 Checkstyle-Warnungen enthält". Wenn Sie einen fortschrittlichen Repository-Manager verwenden (wie Artifactory), ist es einfach, solche Metadaten über Ihr Release zu speichern.
  2. Jetzt vergleicht jeder Pull Request beim Build die Anzahl der hervorgebrachten Warnungen mit der Anzahl, die im aktuellen Release vorhanden ist. Wenn der PR zu einer Erhöhung dieser Zahl führt, besteht der Code den Quality Gate des statischen Codes nicht. Wenn die Anzahl der Warnungen sinkt oder gleich bleibt, wird er akzeptiert.
  3. Beim nächsten Release wird die neu berechnete Anzahl der Warnungen erneut in den Release-Metadaten festgehalten.

So geht es Schritt für Schritt voran (wie bei der Arbeit mit einem Kranz), die Anzahl der Warnungen wird gegen Null tendieren. Natürlich kann man das System täuschen, indem man eine neue Warnung einfügt und dabei eine andere korrigiert. Das ist in Ordnung, denn auf lange Sicht erzielt es Ergebnisse: Warnungen werden in der Regel nicht einzeln, sondern in Gruppen eines bestimmten Typs behoben, und alle leicht behebbaren Warnungen sind recht schnell beseitigt.

In diesem Diagramm ist die Gesamtzahl der Checkstyle-Warnungen über ein halbes Jahr der Arbeit eines solchen "Kranzes" an einem unserer OpenSource-Projekte. Die Anzahl der Warnungen hat sich um eine Größenordnung reduziert, und das geschah ganz natürlich, parallel zur Produktentwicklung!

Integrieren Sie statische Analyse in den Prozess, anstatt sie zur Fehlersuche zu verwenden.

Ich verwende eine modifizierte Version dieser Methode und zähle die Warnungen separat nach Modulen des Projekts und Analysetools. Die dabei generierte YAML-Datei mit Metadaten über den Build sieht etwa so aus:

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 ein „Ratschen“ für alle Werkzeuge der statischen Analyse implementiert werden, ohne auf Plugins oder externe Tools angewiesen zu sein. Jeder Analyzer gibt seinen Bericht in einem einfachen Text- oder XML-Format aus, das leicht analysiert werden kann. Es bleibt nur die notwendige Logik im CI-Skript zu schreiben. Sie können sehen, wie dies in unseren Open-Source-Projekten auf Basis von Jenkins und Artifactory umgesetzt ist. hier oder hier. Beide Beispiele hängen von der Bibliothek ratchetlib: die Methode countWarnings() zählt ganz normal die XML-Tags in den von Checkstyle und Spotbugs erstellten Dateien, während compareWarningMaps() das eigentliche Ratschen implementiert, indem es einen Fehler ausgibt, wenn die Anzahl der Warnungen in einer der Kategorien steigt.

Eine interessante Möglichkeit zur Implementierung eines "Ratschenmechanismus" besteht darin, die Rechtschreibung von Kommentaren, Textliteralen und Dokumentationen mithilfe von aspell zu analysieren. Wie bekannt ist, sind nicht alle Wörter, die dem Standardwörterbuch unbekannt sind, falsch; sie können in ein Benutzerwörterbuch aufgenommen werden. Wenn das Benutzerwörterbuch Teil des Quellcodes des Projekts wird, könnte das Qualitätssicherungssystem für die Rechtschreibung wie folgt formuliert werden: Ausführung von aspell mit dem Standard- und Benutzerwörterbuch. darf nicht keine Rechtschreibfehler finden.

Über die Wichtigkeit der Versionierung des Analysetools

Zusammenfassend lässt sich sagen: Unabhängig davon, wie Sie die Analyse in Ihre Bereitstellungspipeline integrieren, muss die Version des Analysetools festgelegt sein. Wenn das Analysetool unkontrolliert aktualisiert wird, können beim Zusammenstellen eines Pull-Requests neue Fehler auftauchen, die nicht mit dem Codewechsel zusammenhängen, sondern damit, dass das neue Analysetool einfach in der Lage ist, mehr Fehler zu erkennen – und das kann Ihren Prüfprozess für Pull-Requests stören. Ein Upgrade des Analysetools sollte eine bewusste Entscheidung sein. Eine strikte Festlegung der Version jedes Bestandteils der Assembly ist im Allgemeinen erforderlich und ein Thema für sich.

Fazit

  • Statische Analysen finden keine Bugs und verbessern die Qualität Ihres Produkts nicht durch eine einmalige Anwendung. Ein positiver Effekt auf die Qualität ergibt sich nur durch die kontinuierliche Anwendung im Bereitstellungsprozess.
  • Die Fehlersuche ist überhaupt nicht das Hauptziel der Analyse; die überwiegende Mehrheit der nützlichen Funktionen ist in Open-Source-Tools verfügbar.
  • Implementieren Sie Quality Gates basierend auf den Ergebnissen der statischen Analyse bereits in der ersten Phase der Bereitstellungspipeline und nutzen Sie dabei einen „Zahnradmechanismus“ für Legacy-Code.

Links

  1. Continuous Delivery
  2. A. Kudryavtsev: Analyse von Programmen: Wie erkennt man, dass man ein guter Programmierer ist Vortrag über verschiedene Methoden der Code-Analyse (nicht nur statische!)

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster