Integrieren Sie die statische Analyse in den Prozess und suchen Sie nicht mit ihrer Hilfe nach Bugs.

Die große Menge an Materialien über statische Analyse, die mir ständig ins Auge fallen, hat mich dazu inspiriert, diesen Artikel zu schreiben. Erstens ist das PVS-studio Blog, der sich aktiv auf Habrahabr mit Hilfe von Berichten über Fehler, die mit ihrem Werkzeug in Open-Source-Projekten gefunden wurden, präsentiert. Kürzlich haben die PVS-studio Unterstützung für Javarealisiert, und natürlich konnten die Entwickler von IntelliJ IDEA, deren integrierter Analyzer wahrscheinlich der fortschrittlichste für Java ist, nicht abseits stehen. Wenn man solche Berichte liest, hat man das Gefühl, es gehe um einen Zaubertrank: Drücke einen Knopf, und voilà – eine Liste von Defekten steht vor dir. Es scheint, dass mit der Verbesserung der Analyzer immer mehr Bugs automatisch gefunden werden, und die Produkte, die von diesen Robotern gescannt werden, werden immer besser, ganz ohne Mühe unsererseits..

Aber Zaubertränke gibt es nicht. Ich möchte über das sprechen, was in Beiträgen wie „was unser Roboter finden kann“ normalerweise nicht erwähnt wird: was Analyzer nicht können, welche Rolle und welchen Platz sie im Software-Lieferprozess haben und wie man sie richtig implementiert.

Eine der Herausforderungen (Quelle:

Integrieren Sie die statische Analyse in den Prozess und suchen Sie nicht mit ihrer Hilfe nach Bugs.
Wikipedia Was statische Analyzer niemals können werden).

Was ist aus praktischer Sicht die Analyse von Quellcode? Wir reichen einige Quellcodes ein, und nach kurzer Zeit (deutlich kürzer als die Durchführung von Tests) erhalten wir einige 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, ist die

Haltbarkeit der Berechnung : Es handelt sich um einen Satz, der beweist, dass es unmöglich ist, einen allgemeinen Algorithmus zu entwickeln, der aufgrund des Quellcodes eines Programms feststellen kann, ob es in eine Endlosschleife gerät oder innerhalb einer endlichen Zeit beendet wird. Eine Erweiterung dieses Satzes ist dieRais-Satz Rais-Theorem, die für jede nicht-triviale Eigenschaft berechenbarer Funktionen die Bestimmung, ob ein beliebiges Programm eine Funktion mit dieser Eigenschaft berechnet, eine algorithmisch unlösbare Aufgabe ist. Zum Beispiel ist es unmöglich, einen Analysator zu schreiben, der für jeden Quellcode bestimmt, ob das analysierte Programm eine Implementierung eines Algorithmus ist, der beispielsweise die Quadratzahl einer ganzen Zahl berechnet.

Daher hat die Funktionalität statischer Analysatoren unüberwindbare Einschränkungen. Ein statischer Analysator wird niemals in der Lage sein, in jedem Fall solche Dinge zu bestimmen, wie zum Beispiel das Auftreten einer "Nullzeiger-Ausnahme" in Programmiersprachen, die null-Werte zulassen, oder in jedem Fall das Auftreten von "Attribut nicht gefunden" in dynamisch typisierten Sprachen zu bestimmen. Alles, was der perfekteste statische Analysator tun kann, ist, Sonderfälle zu identifizieren, deren Anzahl im Vergleich zu allen möglichen Problemen mit Ihrem Quellcode ohne Übertreibung ein Tropfen auf den heißen Stein ist.

Statische Analyse ist kein Fehlerfinden

Aus dem Vorangegangenen ergibt sich die Schlussfolgerung: Statische Analyse ist kein Mittel zur Verringerung der Anzahl der Defekte im Programm. Ich wage zu behaupten: Wenn sie zum ersten Mal auf Ihr Projekt angewendet wird, wird sie "interessante" Stellen im Code finden, aber wahrscheinlich keine Defekte, die die Qualität der Funktionalität Ihres Programms beeinflussen.

Die Beispiele für Defekte, die automatisiert von Analysatoren gefunden wurden, sind beeindruckend, aber man sollte nicht vergessen, dass diese Beispiele durch das Scannen eines großen Sets großer Codebasen gefunden wurden. Nach dem gleichen Prinzip finden Hacker, die mehrere einfache Passwörter auf vielen Konten ausprobieren, schließlich die Konten, auf denen ein einfaches Passwort steht.

Bedeutet das, dass statische Analyse nicht angewendet werden sollte? Natürlich nicht! Und genau aus dem gleichen Grund, aus dem man jedes neue Passwort auf die Aufnahme in die Liste der "einfachen" Passwörter überprüfen sollte.

Statische Analyse ist mehr als nur Fehlerfinden

In der Tat sind die durch Analyse praktisch lösbaren Aufgaben viel breiter. Denn im Allgemeinen ist die statische Analyse jede Überprüfung des Quellcodes, die vor dessen Ausführung durchgeführt wird. Hier sind einige Dinge, die man tun kann:

  • Überprüfung des Programmierstils im weitesten Sinne. Dazu gehört sowohl die Überprüfung des Formats als auch die Suche nach der Verwendung von überflüssigen/falschen Klammern, das Setzen von Schwellenwerten für Metriken wie die Anzahl der Zeilen/komplexe zyklomatische Komplexität einer Methode usw. - alles, was die Lesbarkeit und Wartbarkeit des Codes potenziell erschwert. In Java ist ein solches Werkzeug Checkstyle, in Python - flake8. Programme dieser Art werden normalerweise als „Linter“ bezeichnet.
  • Nicht nur der ausführbare Code kann analysiert werden. Ressourcendateien wie JSON, YAML, XML, .properties können (und sollten!) automatisch auf Gültigkeit geprüft werden. Es ist besser, frühzeitig bei der automatischen Überprüfung eines Pull Requests zu erfahren, dass aufgrund ungerader Anführungszeichen die Struktur von JSON verletzt ist, als während der Testausführung 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 bedeutet Kompilierung nicht nur die Kompilierung des ausführbaren Codes. Wenn Sie beispielsweise Dokumentation im Format AsciiDoctor, haben, kann der AsciiDoctor-Prozessor (Maven-Plugin) bei der Umwandlung in HTML/PDF Warnungen ausgeben, beispielsweise über gebrochene interne Links. Und das ist ein gewichtiger Grund, einen Pull Request mit Änderungen an der Dokumentation abzulehnen.
  • Rechtschreibprüfung ist ebenfalls eine Form der statischen Analyse. Das Werkzeug aspell kann die Rechtschreibung nicht nur in der Dokumentation, sondern auch im Quellcode von Programmen (in Kommentaren und Literalen) in verschiedenen Programmiersprachen, einschließlich C/C++, Java und Python, überprüfen. Ein Rechtschreibfehler in der Benutzeroberfläche oder Dokumentation ist ebenfalls ein Defekt!
  • Konfigurationstests (was das ist, siehe diesen und diesen Berichte), obwohl sie in einer Umgebung für modulare Tests wie pytest ausgeführt werden, sind tatsächlich auch eine Form der statischen Analyse, da sie den Quellcode während ihrer Ausführung nicht ausführen.

Wie wir sehen, spielt die Fehlersuche in dieser Liste die geringste Rolle, während alles andere durch die Nutzung kostenloser Open-Source-Werkzeuge verfügbar ist.

Welche dieser Arten der statischen Analyse sollten in Ihrem Projekt verwendet werden? Natürlich alle, je mehr, desto besser! Wichtig ist, dass dies richtig implementiert wird, worum es weiter gehen wird.

Die Lieferkette als mehrstufiger Filter und die statische Analyse als deren erster Kaskade

Eine klassische Metapher der kontinuierlichen Integration ist ein Pipeline, durch die Änderungen fließen – von der Änderung des Quellcodes bis zur Bereitstellung in der Produktion. Der Standardablauf dieser Pipeline sieht folgendermaßen aus:

  1. statische Analyse
  2. Kompilierung
  3. modular Tests
  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 genau so und nicht anders? In dem Teil der Pipeline, der das Testen betrifft, erfahren Tester von der weithin bekannten Testpyramide.

Integrieren Sie die statische Analyse in den Prozess und suchen Sie nicht mit ihrer Hilfe nach Bugs.
Testpyramide. Quelle: Artikel Martin Fowler.

Im unteren Teil dieser Pyramide befinden sich Tests, die leichter zu schreiben sind, schneller ausgeführt werden und keine Neigung zu Fehlalarmen haben. Deshalb sollte ihre Anzahl größer sein, sie sollten mehr Code abdecken und zuerst ausgeführt werden. In der oberen Hälfte der Pyramide ist es umgekehrt, weshalb die Anzahl der Integrations- und UI-Tests auf ein notwendiges Minimum reduziert werden sollte. Der Mensch in dieser Kette ist die teuerste, langsamste und unzuverlässigste Ressource, deshalb steht er am Ende und führt die Arbeit nur aus, wenn die vorherigen Phasen keine Defekte festgestellt haben. Die Pipeline wird jedoch nach denselben Prinzipien aufgebaut, auch in Teilen, die nicht direkt mit dem Testen zu tun haben!

Ich möchte eine Analogie in Form eines mehrstufigen Wasserfilters vorschlagen. Uns wird schmutziges Wasser (Änderungen mit Defekten) zugeführt, und am Ende sollten wir sauberes Wasser erhalten, aus dem alle unerwünschten Verunreinigungen herausgefiltert wurden.

Integrieren Sie die statische Analyse in den Prozess und suchen Sie nicht mit ihrer Hilfe nach Bugs.
Mehrstufiger Filter. Quelle: Wikimedia Commons

Wie bekannt, werden Reinigungssysteme so konzipiert, dass jede nachfolgende Stufe zunehmend feinere Fraktionen von Verunreinigungen abfiltern kann. In diesem Sinne haben die Stufen der groben Reinigung eine höhere Durchsatzleistung und niedrigere Kosten. In unserer Analogie bedeutet dies, dass die Eingangs-Qualitätsschranken eine höhere Effizienz haben, weniger Aufwand für den Start erfordern und selbst weniger wählerisch im Betrieb sind — und genau in dieser Reihenfolge sind sie angeordnet. Die Rolle der statischen Analyse, die, wie wir nun verstehen, nur die gröbsten Fehler herausfiltern kann, ist die Rolle eines „Schmutzsiebes“ zu Beginn der Filterstufen.

Statische Analyse verbessert für sich genommen nicht die Qualität des Endprodukts, ähnlich wie ein Schmutzsieb das Wasser nicht trinkbar macht. Dennoch ist ihre Bedeutung in Kombination mit anderen Elementen der Pipeline offensichtlich. Obwohl die Ausgangsstufen eines mehrstufigen Filters potenziell in der Lage sind, alles zu erfassen, was auch die Eingangsfilter filtern, ist klar, zu welchen Folgen der Versuch führen wird, sich ausschließlich auf die feinen Filterstufen zu verlassen, ohne die Eingangsfilter.

Das Ziel des „Schmutzsiebes“ ist es, die nachfolgenden Stufen von der Erfassung der wirklich groben Fehler zu entlasten. Zum Beispiel sollte mindestens derjenige, der einen Code-Review durchführt, sich nicht mit falsch formatiertem Code und Verstößen gegen die festgelegten Kodierungsnormen (wie überflüssigen Klammern oder zu tief verschachtelten Verzweigungen) ablenken lassen. Bugs wie NPE sollten durch Unit-Tests erfasst werden, aber wenn der Analyzer uns bereits vor dem Test darauf hinweist, dass ein Bug unvermeidlich auftreten wird, beschleunigt das seine Behebung erheblich.

Ich denke, jetzt ist klar, warum die statische Analyse die Qualität des Produkts nicht verbessert, wenn sie episodisch angewendet wird, und sie kontinuierlich eingesetzt werden sollte, um Änderungen mit groben Fehlern herauszufiltern. Die Frage, ob der Einsatz eines statischen Analysetools die Qualität Ihres Produkts verbessert, ist etwa gleichbedeutend mit der Frage „Wird die Trinkqualität von Wasser aus einem schmutzigen Gewässer verbessert, wenn man es durch ein Sieb gießt?“

Implementierung in ein Legacy-Projekt

Wichtige praktische Frage: Wie implementiert man statische Analyse als „Qualitätsgate“ im Prozess der kontinuierlichen Integration? Bei automatisierten Tests ist alles offensichtlich: Es gibt eine Reihe von Tests, und das Scheitern eines einzelnen von ihnen ist ein ausreichender Grund, anzunehmen, dass die Build-Qualitätsanforderung nicht bestanden wurde. Der Versuch, ein Gate basierend auf den Ergebnissen der statischen Analyse auf die gleiche Weise festzulegen, scheitert: Bei Legacy-Code gibt es zu viele Analysewarnungen, die vollständig zu ignorieren man nicht möchte, aber es ist auch unmöglich, die Produktlieferung nur zu stoppen, weil es in ihm Warnungen vom Analyzer gibt.

Wenn der Analyzer erstmalig angewendet wird, gibt er in jedem Projekt eine enorme Anzahl von Warnungen aus, die überwiegende Mehrheit davon hat jedoch nichts mit der ordnungsgemäßen Funktion des Produkts zu tun. Es ist unmöglich, sofort all diese Anmerkungen zu beheben, und viele müssen nicht einmal behoben werden. Schließlich wissen wir, dass unser Produkt insgesamt funktioniert, und das sogar bevor die statische Analyse eingeführt wurde!

Infolgedessen beschränken sich viele auf sporadische Nutzung der statischen Analyse oder verwenden sie lediglich im Informationsmodus, wenn beim Build einfach der Bericht des Analyzers ausgegeben wird. Das entspricht dem Fehlen jeglicher Analyse, denn wenn wir bereits eine Vielzahl von Warnungen haben, bleibt das Auftreten einer weiteren (so ernst sie auch sein mag) beim Ändern des Codes unbemerkt.

Die folgenden Methoden zur Einführung von Qualitätsgates sind bekannt:

  • Festlegung eines Limits für die Gesamtzahl der Warnungen oder für die Anzahl der Warnungen, geteilt durch die Anzahl der Codezeilen. Dies funktioniert schlecht, da ein solches Gate Änderungen mit neuen Fehlern problemlos passieren lässt, solange das Limit nicht überschritten wird.
  • Die Erfassung aller alten Warnungen im Code zu einem bestimmten Zeitpunkt als ignoriert und die Ablehnung des Builds bei neuen Warnungen. Diese Funktionalität bietet PVS-Studio und einige Online-Ressourcen wie Codacy. Ich hatte noch nicht das Vergnügen, mit PVS-Studio zu arbeiten, was Codacy betrifft, so liegt ihr Hauptproblem darin, dass es ziemlich kompliziert ist, zu bestimmen, was ein „alter“ und was ein „neuer“ Fehler ist – ein Algorithmus, der nicht immer korrekt funktioniert, insbesondere wenn sich die Dateien stark ändern oder umbenannt werden. Meiner Erinnerung nach konnte Codacy neue Warnungen im Pull-Request übersehen, während gleichzeitig der Pull-Request aufgrund von Warnungen, die nicht mit den Änderungen dieses PR zu tun hatten, nicht genehmigt wurde.
  • Meiner Meinung nach ist die effektivste Lösung die in dem Buch beschriebene Continuous Delivery „Ratschenmethode“ („ratcheting“). Die Grundidee ist, dass die Eigenschaft jeder Version die Anzahl der Warnungen der statischen Analyse ist, und es sind nur solche Änderungen erlaubt, die die Gesamtanzahl der Warnungen nicht erhöhen.

Ratsche

Das funktioniert folgendermaßen:

  1. In der Anfangsphase wird im Metadaten über die Version die Anzahl der im Code gefundenen Warnungen durch die Analysewerkzeuge aufgezeichnet. So wird bei der Erstellung des Hauptzweigs in Ihrem Repository-Manager nicht einfach „Version 7.0.2“ aufgezeichnet, sondern „Version 7.0.2, die 100500 Checkstyle-Warnungen enthält“. Wenn Sie einen fortschrittlichen Repository-Manager (wie Artifactory) verwenden, ist es einfach, solche Metadaten über Ihre Version zu speichern.
  2. Jetzt vergleicht jeder Pull-Request bei der Erstellung die Anzahl der erhaltenen Warnungen mit der Anzahl, die in der aktuellen Version vorhanden ist. Wenn der PR zu einer Erhöhung dieser Zahl führt, besteht der Code den Quality Gate des statischen Analyseprozesses nicht. Wenn die Anzahl der Warnungen verringert oder unverändert bleibt, wird er genehmigt.
  3. Bei der nächsten Version wird die neu berechnete Anzahl der Warnungen erneut in den Metadaten der Version aufgezeichnet.

So wird die Anzahl der Warnungen langsam, aber stetig (wie bei der Arbeit mit einem Ratschensystem) gegen Null tendieren. Natürlich kann das System überlistet werden, indem eine neue Warnung durch das Korrigieren einer bestehenden eingefügt wird. Das ist in Ordnung, da es auf lange Sicht Ergebnisse liefert: Warnungen werden in der Regel nicht einzeln, sondern gemeinsam in Gruppen eines bestimmten Typs behoben, und alle leicht behebbaren Warnungen werden relativ schnell behoben.

In diesem Diagramm wird die Gesamtanzahl der Checkstyle-Warnungen über einen Zeitraum von sechs Monaten während der Arbeit eines solchen „Ratschensystems“ auf einem unserer OpenSource-Projekte. Die Anzahl der Warnungen hat sich um ein Vielfaches verringert, und das geschah auf natürliche Weise, parallel zur Produktentwicklung!

Integrieren Sie die statische Analyse in den Prozess und suchen Sie nicht mit ihrer Hilfe nach Bugs.

Ich wende eine modifizierte Version dieser Methode an, wobei ich die Warnungen nach Modulen des Projekts und Analysewerkzeugen getrennt zähle. 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 fortgeschrittenen CI-System kann das „Ratschensystem“ für beliebige statische Analysewerkzeuge implementiert werden, ohne auf Plugins und externe Tools angewiesen zu sein. Jeder der Analyzer gibt seinen Bericht in einem einfachen Text- oder XML-Format aus, das leicht analysierbar ist. Es bleibt nur die notwendige Logik im CI-Skript zu codieren. Man kann sehen, wie dies in unseren Open-Source-Projekten auf Basis von Jenkins und Artifactory umgesetzt wurde hier oder hier. Beide Beispiele hängen von der Bibliothek ratchetlib: die Methode countWarnings() zählt gewöhnlich die XML-Tags in den von Checkstyle und Spotbugs generierten Dateien, während compareWarningMaps() das eigentliche Ratschensystem implementiert, das einen Fehler auslöst, wenn die Anzahl der Warnungen in einer der Kategorien ansteigt.

Eine interessante Implementierungsoption für einen „Ratschenmechanismus“ ist die Analyse der Rechtschreibung von Kommentaren, Textliteralen und Dokumentationen mithilfe von aspell. Wie bekannt ist, sind bei der Rechtschreibprüfung nicht alle unbekannten Wörter im Standardwörterbuch falsch; sie können dem benutzerdefinierten Wörterbuch hinzugefügt werden. Wenn das benutzerdefinierte Wörterbuch Teil des Quellcodes des Projekts wird, kann das Qualitätsgate bezüglich der Rechtschreibung wie folgt formuliert werden: Ausführung von aspell mit dem Standard- und dem benutzerdefinierten Wörterbuch. sollte keine Rechtschreibfehler finden.

Zur Bedeutung der Versionsfixierung des Analysators

Zusammenfassend lässt sich Folgendes sagen: Egal, wie Sie die Analyse in Ihre Lieferpipeline integrieren, die Version des Analysators sollte festgelegt sein. Wenn eine spontane Aktualisierung des Analysators zugelassen wird, können beim Bauen eines neuen Pull Requests neue Fehler „auftauchen“, die nicht mit Änderungen am Code, sondern damit zusammenhängen, dass der neue Analysator einfach in der Lage ist, mehr Fehler zu finden – und das wird Ihren Prozess der Annahme von Pull Requests stören. Das Update des Analysators sollte eine bewusste Handlung sein. Im Übrigen ist die strenge Fixierung der Version jeder Komponente des Builds eine im Allgemeinen notwendige Anforderung und ein Thema für ein separates Gespräch.

Das DBMS Tarantool ist ein attraktives, zukunftsträchtiges Produkt zur Erstellung von hochbelasteten Anwendungen.

  • Statische Analysen finden Ihnen keine Bugs und verbessern die Qualität Ihres Produkts nicht durch einmalige Anwendung. Der positive Effekt auf die Qualität ergibt sich nur aus der kontinuierlichen Anwendung im Lieferprozess.
  • Die Fehlersuche ist generell nicht die Hauptaufgabe der Analyse; die überwältigende Mehrheit der nützlichen Funktionen ist in Open-Source-Tools verfügbar.
  • Implementieren Sie Qualitätsgate basierend auf den Ergebnissen der statischen Analyse bereits im frühesten Stadium der Lieferpipeline, indem Sie den „Ratschenmechanismus“ für Legacy-Code verwenden.

Links

  1. Continuous Delivery
  2. A. Kudryavtsev: Programm Analyse: Wie erkennt man, dass man ein guter Programmierer ist Vortrag über verschiedene Methoden zur Codeanalyse (nicht nur statische!)

Quelle: habr.com

60GB SSD 8Gb DDR4