Implementieren Sie statische Analysen im Prozess, suchen Sie jedoch nicht nach Fehlern

    Schreiben Sie diesen Artikel Ich wurde von einer Vielzahl von Materialien zur statischen Analyse inspiriert, die immer öfter ins Auge fielen. Dies ist zum einen der Blog des PVS-Studios , der sich aktiv in Habré mit Hilfe von Reviews von Fehlern bewirbt, die sein Tool in Open Source-Projekten gefunden hat. Vor kurzem implementierte PVS-studio Java-Unterstützung , und natürlich konnten die Entwickler von IntelliJ IDEA, deren eingebetteter Analyser wahrscheinlich der derzeit fortschrittlichste für Java ist, nicht zur Seite stehen .

    Beim Lesen solcher Rezensionen besteht das Gefühl, dass wir über ein magisches Elixier sprechen: Klicken Sie auf die Schaltfläche, und hier ist es - eine Liste von Fehlern vor Ihren Augen. Es scheint, dass mit der Verbesserung der Analysegeräte automatisch immer mehr Fehler auftreten, und die von diesen Robotern gescannten Produkte werden immer besser, ohne dass wir Anstrengungen unternehmen müssen.

    Aber magische Elixiere kommen nicht vor. Ich würde gerne darüber sprechen, was die Leute normalerweise nicht in Beiträgen wie "Dies sind die Dinge, die unser Roboter finden kann" sagen: Was Analysegeräte nicht können, welche Rolle sie im Software-Bereitstellungsprozess wirklich spielen und wie sie richtig implementiert werden.


    Ratsche (Quelle: Wikipedia ).

    Was statische Analysatoren niemals können


    Was ist aus praktischer Sicht Quellcode-Analyse? Wir geben einige Quellen in die Eingabe ein und am Ausgang in kurzer Zeit (viel kürzer als der Testlauf) erhalten wir Informationen über unser System. Die hauptsächliche und mathematisch unüberwindbare Einschränkung besteht darin, dass wir auf diese Weise nur eine recht enge Klasse von Informationen erhalten können.

    Das bekannteste Beispiel für ein Problem, das mit statischer Analyse nicht gelöst werden kann, ist das Stoppproblem : Dies ist ein Satz, der beweist, dass es nicht möglich ist, einen allgemeinen Algorithmus zu entwickeln, der anhand des Quellcodes eines Programms bestimmen könnte, ob es in einer endlichen Zeit eine Schleife durchlaufen oder abgeschlossen wird. Eine Erweiterung dieses Theorems ist der Reis-TheoremDas Feststellen, ob ein beliebiges Programm eine Funktion mit einer solchen Eigenschaft berechnet, für jede nicht-triviale Eigenschaft von berechenbaren Funktionen zu bestimmen, ist ein algorithmisch unlösbares Problem. Beispielsweise ist es nicht möglich, einen Analysator zu schreiben, der für irgendeinen Quellcode bestimmt, ob das analysierte Programm eine Implementierung eines Algorithmus ist, der eine ganze Zahl berechnet, beispielsweise quadriert.

    Daher hat die Funktionalität statischer Analysatoren unüberwindbare Einschränkungen. Der statische Analysator wird niemals in der Lage sein, Dinge wie das Auftreten einer "Nullzeigerausnahme" in Sprachen zu ermitteln, in denen Null zulässig ist, oder in allen Fällen das Auftreten eines "Attributs nicht gefunden" in Sprachen mit dynamischer Typisierung. Der fortschrittlichste statische Analysator kann nur bestimmte Fälle herausgreifen, deren Anzahl ohne Übertreibung ein Tropfen im Meer unter allen möglichen Problemen mit Ihrem Quellcode ist.

    Statische Analyse ist keine Fehlersuche.


    Aus dem Vorstehenden folgt die Schlussfolgerung: Die statische Analyse ist kein Mittel zur Verringerung der Anzahl von Fehlern in einem Programm. Ich wage zu sagen: Wenn Sie Ihr Projekt zum ersten Mal anwenden, wird es "amüsante" Stellen im Code finden, aber höchstwahrscheinlich keine Mängel, die die Qualität der Arbeit Ihres Programms beeinträchtigen.

    Beispiele für Defekte, die von Analysegeräten automatisch gefunden werden, sind beeindruckend. Man sollte jedoch nicht vergessen, dass diese Beispiele durch das Scannen einer großen Menge großer Codebasen gefunden wurden. Nach demselben Prinzip können Hacker, die die Möglichkeit haben, ein paar einfache Kennwörter für eine große Anzahl von Konten zu verwenden, schließlich die Konten finden, die über ein einfaches Kennwort verfügen.

    Bedeutet das, dass keine statische Analyse erforderlich ist? Natürlich nicht! Und aus genau demselben Grund lohnt es sich, jedes neue Passwort auf der Stop-Liste der "einfachen" Passwörter zu überprüfen.

    Statische Analyse ist mehr als die Fehlersuche.


    Tatsächlich sind die Probleme, die durch Analyse praktisch gelöst werden, viel weiter. Im Allgemeinen handelt es sich bei der statischen Analyse im Allgemeinen um die Überprüfung von Quellen, die vor ihrem Start durchgeführt werden. Hier sind einige Dinge, die Sie tun können:

    • Überprüfen Sie den Codierstil im weitesten Sinne des Wortes. Dies umfasst das Überprüfen der Formatierung sowie das Suchen nach leeren / zusätzlichen Klammern, das Festlegen von Schwellenwerten für Metriken wie die Anzahl der Zeilen / die zyklomatische Komplexität der Methode usw. All dies macht den Code möglicherweise lesbarer und wartbarer. In Java ist Checkstyle ein solches Werkzeug, in Python-flake8. Programme dieser Klasse werden normalerweise "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 geprüft werden. Immerhin ist es besser zu lernen, dass die JSON-Struktur zu Beginn der automatischen Pull-Request-Prüfung aufgrund nicht übereinstimmender Anführungszeichen gebrochen wird, als während der Ausführung von Tests oder zur Laufzeit. Geeignete Tools stehen zur Verfügung: zum Beispiel YAMLlint , JSONLint .
    • Das Kompilieren (oder Parsen für dynamische Programmiersprachen) ist auch eine Art statische Analyse. Compiler können in der Regel Warnungen ausgeben, die auf Probleme mit der Qualität des Quellcodes hinweisen, und sollten nicht ignoriert werden.
    • Manchmal wird beim Kompilieren nicht nur ausführbarer Code kompiliert. Wenn Sie beispielsweise über eine Dokumentation im AsciiDoctor- Format verfügen , kann der AsciiDoctor-Handler ( Maven-Plugin ) zum Zeitpunkt der Umwandlung in HTML / PDF Warnmeldungen ausgeben, z. B. über defekte interne Links. Dies ist ein gewichtiger Grund dafür, dass Sie bei Dokumentationsänderungen keine Pull-Anforderung akzeptieren.
    • Die Rechtschreibprüfung ist auch eine Art statische Analyse. Das Dienstprogramm aspell kann die Rechtschreibung nicht nur in der Dokumentation prüfen, sondern auch in Quellcodes von Programmen (Kommentare und Literale) in verschiedenen Programmiersprachen, einschließlich C / C ++, Java und Python. Ein Schreibfehler in der Benutzeroberfläche oder Dokumentation ist ebenfalls ein Defekt!
    • Konfigurationstests (was es ist - siehe diesen und diesen Bericht), obwohl sie in der Ausführungsumgebung von Komponententests wie pytest ausgeführt werden, sind tatsächlich auch eine Art statischer Analyse, da sie die Quellcodes während ihrer Ausführung nicht ausführen .

    Wie Sie sehen, nimmt die Suche nach Fehlern in dieser Liste die unwichtigste Rolle ein, und alles andere ist mit Hilfe von kostenlosen Open-Source-Tools verfügbar.

    Welche dieser Arten statischer Analysen sollten in Ihrem Projekt verwendet werden? Natürlich umso mehr - desto besser! Die Hauptsache ist, es richtig zu implementieren, worauf weiter eingegangen wird.

    Lieferpipeline als mehrstufiger Filter und statische Analyse als erste Stufe


    Die klassische Metapher der kontinuierlichen Integration ist die Pipeline (Pipeline), durch die sich Änderungen ergeben - von der Änderung des Quellcodes bis zur Lieferung an die Produktion. Die Standardabfolge der Schritte für diese Pipeline sieht folgendermaßen aus:

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

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

    Warum so und nicht anders? In dem Teil der Pipeline, der sich mit dem Testen befasst, erkennen die Tester die bekannte Testpyramide.


    Pyramide testen. Quelle: Martin Fowler Artikel .

    Am unteren Ende dieser Pyramide befinden sich Tests, die leichter zu schreiben sind und schneller sind und nicht zu falsch positiven Ergebnissen neigen. Daher sollte es mehr davon geben, sie sollten mehr Code abdecken und zuerst ausgeführt werden. An der Spitze der Pyramide ist alles umgekehrt, daher sollte die Anzahl der Integrations- und UI-Tests auf das notwendige Minimum reduziert werden. Die Person in dieser Kette ist die teuerste, langsamste und unzuverlässigste Ressource. Sie ist also ganz am Ende und erledigt die Arbeit nur, wenn in den vorherigen Schritten keine Mängel festgestellt wurden. Die gleichen Prinzipien werden jedoch für den Bau eines Förderers in Teilen verwendet, die nicht in direktem Zusammenhang mit der Prüfung stehen!

    Ich möchte eine Analogie in Form eines mehrstufigen Wasserfiltersystems anbieten. Schmutzwasser wird dem Einlass zugeführt (ändert sich mit Mängeln) Am Auslass müssen wir sauberes Wasser erhalten, in dem alle unerwünschten Verunreinigungen ausgesiebt werden.


    Mehrstufiger Filter. Quelle: Wikimedia Commons

    Wie Sie wissen, sind Reinigungsfilter so konzipiert, dass jede nächste Kaskade einen immer kleineren Anteil an Verunreinigungen herausfiltern kann. Gleichzeitig haben die Kaskaden der gröberen Reinigung einen höheren Durchsatz und niedrigere Kosten. In unserer Analogie bedeutet dies, dass Input-Quality-Gates schneller reagieren, weniger Laufaufwand erfordern und an sich unprätentiöser sind - und in dieser Reihenfolge werden sie gebaut. Die Rolle der statischen Analyse, die, wie wir jetzt verstehen, nur die gröbsten Defekte aussortieren kann - dies ist die Rolle des Schmutzfängers am Anfang der Filterkaskade.

    Die statische Analyse allein verbessert die Qualität des Endprodukts nicht, da ein "Schlammbecken" kein Trinkwasser erzeugt. Wie auch bei anderen Elementen der Pipeline ist deren Bedeutung jedoch offensichtlich. Obwohl bei einem mehrstufigen Filter die Ausgangsstufen potentiell in der Lage sind, alles auf die gleiche Weise wie die Eingangsstufen abzufangen, ist es klar, zu welchen Folgen der Versuch führt, wenn nur die Feinreinigungsstufen alleine und ohne Eingangsstufen ausgeführt werden.

    Der Zweck der "Gryazevika" - die nachfolgenden Kaskaden zu entladen, um sehr grobe Defekte einzufangen. Zum Beispiel sollte eine Person, die eine Codeüberprüfung durchführt, mindestens nicht durch falsch formatierten Code und die Verletzung etablierter Codierungsstandards (wie z. B. zusätzliche Klammern oder zu tief geschachtelte Zweige) abgelenkt werden. Fehler wie NPE sollten durch Komponententests erfasst werden. Wenn der Analysator jedoch bereits vor dem Test darauf hinweist, dass der Fehler unvermeidlich auftreten sollte, wird seine Korrektur erheblich beschleunigt.

    Ich nehme an, es ist jetzt klar, warum die statische Analyse die Qualität des Produkts nicht verbessert, wenn es sporadisch angewendet wird und ständig verwendet werden sollte, um Änderungen mit groben Mängeln auszusortieren. Die Frage, ob die Verwendung eines statischen Analysators die Qualität Ihres Produkts verbessert, entspricht in etwa der Frage, ob das Trinkwasser aus einem Schmutzwasserbehälter verbessert wird, wenn es durch ein Sieb passiert wird.

    Einführung in das Vermächtnisprojekt


    Eine wichtige praktische Frage: Wie kann die statische Analyse im Rahmen der kontinuierlichen Integration als „Quality Gate“ implementiert werden? Bei automatischen Tests ist alles offensichtlich: Es gibt eine Reihe von Tests, und der Fall eines Tests ist Grund genug zu glauben, dass der Build das Qualitäts-Gate nicht passiert hat. Der Versuch, das Gate durch die Ergebnisse der statischen Analyse auf die gleiche Weise festzulegen, schlägt fehl: Es gibt zu viele Analysewarnungen im Legacy-Code. Ich möchte sie nicht vollständig ignorieren, aber es ist unmöglich, die Produktauslieferung nur zu stoppen, weil sie Analysewarnungen enthält.

    Der Analysator wird bei jedem Projekt zum ersten Mal verwendet und generiert eine Vielzahl von Warnungen, von denen die überwiegende Mehrheit nicht mit der korrekten Funktion des Produkts zusammenhängt. Es ist unmöglich, alle diese Bemerkungen auf einmal zu korrigieren, und viele sind nicht notwendig. Am Ende wissen wir, dass unser Produkt als Ganzes funktioniert, und vor der Einführung der statischen Analyse!

    Infolgedessen sind viele auf die episodische Verwendung der statischen Analyse beschränkt oder verwenden sie nur im Informationsmodus, wenn die Baugruppe einfach einen Berichtsanalysator erzeugt. Dies ist gleichbedeutend mit dem Fehlen einer Analyse, denn wenn bereits viele Warnungen vorliegen, bleibt das Auftreten einer weiteren (auch schwerwiegenden) weiteren Änderung des Codes unbemerkt.

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

    • Festlegen eines Grenzwerts für die Gesamtzahl der Warnungen oder der Anzahl der Warnungen geteilt durch die Anzahl der Codezeilen. Es funktioniert schlecht, da ein solches Tor Änderungen mit neuen Fehlern bis zum Überschreiten des Grenzwerts unberücksichtigt lässt.
    • Zu einem bestimmten Zeitpunkt wurden alle alten Warnungen im Code als ignoriert und der Fehler beim Erstellen neuer Warnungen behoben. Diese Funktionalität wird von PVS-Studio und einigen Online-Ressourcen bereitgestellt, zum Beispiel Codacy. Ich hatte keine Chance im PVS-Studio zu arbeiten, da meine Erfahrung mit Codacy das Hauptproblem ist, dass die Definition von "altem" und neuem "Fehler" ein ziemlich komplizierter und nicht immer korrekt funktionierender Algorithmus ist, insbesondere wenn Dateien werden stark modifiziert oder umbenannt. In meiner Erinnerung könnte Codacy neue Warnungen in einer Pull-Anforderung verpassen und gleichzeitig eine Pull-Anforderung nicht aufgrund von Warnungen verpassen, die nicht mit Änderungen im Code dieses PR zusammenhängen.
    • Die effektivste Lösung ist meiner Meinung nach die "Ratschenmethode", die im Buch Continuous Delivery beschrieben wird . Die Grundidee ist, dass die Eigenschaft jeder Version die Anzahl der statischen Analysewarnungen ist und nur Änderungen zulässig sind, die die Gesamtanzahl der Warnungen nicht erhöhen.

    Ratsche


    Es funktioniert so:

    1. In der Anfangsphase wird in den Metadaten ein Eintrag über die Freigabe der Anzahl der Warnungen in dem von den Analysatoren gefundenen Code vorgenommen. Beim Erstellen des Hauptzweigs in Ihrem Repository-Manager wird also nicht nur "Version 7.0.2" aufgezeichnet, sondern "Version 7.0.2 mit 100500 Checkstyle-Warnungen". Wenn Sie einen erweiterten Repository-Manager (z. B. Artifactory) verwenden, ist das Speichern solcher Metadaten zu Ihrer Version einfach.
    2. Jede Pull-Anforderung vergleicht nun die Anzahl der resultierenden Warnungen mit der aktuellen Anzahl in der aktuellen Version. Wenn PR zu einer Erhöhung dieser Anzahl führt, passiert der Code das Quality Gate nicht durch statische Analyse. Wenn die Anzahl der Warnungen abnimmt oder sich nicht ändert, werden sie weitergegeben.
    3. Mit der nächsten Version wird die neu berechnete Anzahl von Warnungen in den Release-Metadaten neu aufgezeichnet.

    Nach und nach, aber stetig (wie beim Arbeiten mit einer Ratsche), tendiert die Anzahl der Warnungen gegen Null. Natürlich kann das System getäuscht werden, indem eine neue Warnung ausgegeben wird, Dies ist normal, denn auf lange Sicht ergibt sich daraus das Ergebnis: Warnungen werden in der Regel nicht nacheinander, sondern sofort von einer Gruppe eines bestimmten Typs korrigiert und alle leicht zu eliminierenden Warnungen werden schnell beseitigt.

    Diese Grafik zeigt die Gesamtzahl der Checkstyle-Warnungen für die sechsmonatige Arbeit einer solchen "Ratsche" in einem unserer OpenSource-Projekte . Die Anzahl der Warnungen hat um eine Größenordnung abgenommen, und dies geschah natürlich parallel zur Produktentwicklung!



    Ich wende eine modifizierte Version dieser Methode an, wobei separat Warnungen nach Projektmodulen und Analysewerkzeugen gezählt werden. Die YAML-Datei mit Assembly-Metadaten sieht folgendermaßen 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 erweiterten CI-System kann die Ratsche für alle statischen Analysewerkzeuge implementiert werden, ohne auf Plug-Ins und Tools von Drittanbietern zurückgreifen zu müssen. Jeder Analysator erstellt seinen Bericht in einem einfachen Text- oder XML-Format, das leicht analysiert werden kann. Es muss nur die erforderliche Logik im CI-Skript registriert werden. Wie es in unseren Open Source-Projekten auf Basis von Jenkins und Artifactory umgesetzt wird, können Sie hier oder hier erfahren . Beide Beispiele hängen von der Ratchetlib- Bibliothek ab : Die Methode countWarnings()berechnet normalerweise XML-Tags in Dateien, die von Checkstyle und Spotbugs generiert werden, und compareWarningMaps()implementiert dasselbe Ratchet. Dabei wird ein Fehler ausgegeben, wenn die Anzahl der Warnungen in einer der Kategorien steigt.

    Eine interessante Version der Implementierung der "Ratsche" ist möglich, um die Schreibweise von Kommentaren, Textliteralen und Dokumentation mit aspell zu analysieren. Wie Sie wissen, sind bei der Rechtschreibprüfung nicht alle unbekannten Wörter des Standardwörterbuchs falsch. Sie können dem Benutzerwörterbuch hinzugefügt werden. Wenn Sie ein Benutzerwörterbuch in den Quellcode des Projekts aufnehmen, kann das Rechtschreibqualitätstor folgendermaßen formuliert werden: Das Ausführen von aspell mit einem Standard- und Benutzerwörterbuch sollte keine Rechtschreibfehler finden.

    Zur Wichtigkeit der Fixierung der Analysatorversion


    Zusammenfassend ist Folgendes zu beachten: Unabhängig davon, wie Sie die Analyse in Ihrer Lieferpipeline implementieren, muss die Version des Analysators korrigiert werden. Wenn Sie eine spontane Aktualisierung des Analysators zulassen, können beim Zusammenstellen der nächsten Pull-Anfrage neue Fehler auftreten, die nicht mit der Codeänderung zusammenhängen, sondern weil der neue Analysator einfach mehr Fehler finden kann. Dadurch wird der Anforderungsprozess für die Akzeptanzanforderung unterbrochen. . Ein Analyzer-Upgrade sollte eine bewusste Aktion sein. Eine feste Fixierung der Version jeder Komponente der Baugruppe ist jedoch im Allgemeinen eine notwendige Anforderung und ein Thema für eine separate Unterhaltung.

    Schlussfolgerungen


    • Bei der statischen Analyse werden keine Fehler gefunden und die Qualität Ihres Produkts wird durch einmalige Verwendung nicht verbessert. Der einzige positive Effekt auf die Qualität ist die kontinuierliche Verwendung im Lieferprozess.
    • Suchfehler im Allgemeinen sind nicht die Hauptaufgabe der Analyse, sondern die überwiegende Mehrheit der nützlichen Funktionen, die in den OpenSource-Tools verfügbar sind.
    • Implementieren Sie Qualitätsgatter basierend auf den Ergebnissen der statischen Analyse in der ersten Phase der Lieferpipeline. Verwenden Sie dabei ein Ratschenzeichen für Legacy-Code.

    Links


    1. Kontinuierliche Lieferung
    2. A. Kudryavtsev: Programmanalyse: Verstehen, dass Sie ein guter Programmiererbericht über verschiedene Methoden der Codeanalyse sind (nicht nur statisch!)

    Jetzt auch beliebt: