Statische Analyse von PHP-Code am Beispiel von PHPStan, Phan und Psalm



    Badoo gibt es seit über 12 Jahren. Wir haben eine Menge PHP-Code (Millionen Zeilen), und wahrscheinlich wurden sogar Zeilen, die vor 12 Jahren geschrieben wurden, beibehalten. Wir haben Code in den Tagen von PHP 4 und PHP 5 zurückgeschrieben. Wir veröffentlichen den Code zweimal täglich, und jedes Layout enthält etwa 10-20 Aufgaben. Außerdem können Programmierer dringende Patches veröffentlichen - kleine Änderungen. Und am Tag solcher Patches haben wir ein paar Dutzend. Im Allgemeinen ändert sich unser Code sehr aktiv.

    Wir suchen ständig nach Möglichkeiten, die Entwicklung zu beschleunigen und die Codequalität zu verbessern. Und sobald wir uns entschieden haben, statische Code-Analyse zu implementieren. Was dabei herauskam, lesen Sie unter dem Schnitt.

    Strenge Typen: Warum verwenden wir es noch nicht


    Einmal hatten wir eine Diskussion in unserem Corporate-PHP-Chat. Einer der neuen Mitarbeiter beschrieben , wie eine frühere Arbeit , die sie zwingend strict_types + Skalar umgesetzt haben Typ Hinweise für den gesamten Code - und es reduziert signifikant die Anzahl von Fehlern auf prodakshene.

    Die meisten Oldtimer-Chats waren gegen diese Innovation. Der Hauptgrund war, dass PHP keinen Compiler hat, der beim Kompilieren die Übereinstimmung aller Typen im Code überprüft. Wenn Sie den Code nicht zu 100% mit Tests abdecken, besteht immer das Risiko, dass Fehler während der Produktion auftauchen, was wir nicht tun wir wollen zulassen.

    Strict_types findet natürlich einen bestimmten Prozentsatz an Fehlern, die durch Typenkonflikt verursacht werden und durch die Art und Weise, wie PHP Typen "im Hintergrund" konvertiert. Viele erfahrene PHP-Programmierer wissen jedoch bereits, wie Typsysteme in PHP funktionieren, welche Regeln für die Typkonvertierung gelten, und in den meisten Fällen schreiben sie korrekten, funktionierenden Code.

    Uns gefiel jedoch der Gedanke, dass ein bestimmtes System zeigt, wo im Code ein Typ nicht übereinstimmt. Wir denken über Alternativen zu strict_types nach.

    Anfangs wollten wir sogar einen Patch für PHP erstellen. Wir wollten, dass die Funktion TypeError (was im Wesentlichen eine Ausnahme ist) nicht ausgelöst wird, wenn die Funktion einen Skalar-Typ (beispielsweise int) annimmt und ein anderer Skalar-Typ (z. B. float) eingeht. sowie die Protokollierung dieses Ereignisses in error.log. Dies würde uns erlauben, alle Orte zu finden, an denen unsere Annahmen über Typen falsch sind. Aber ein solcher Patch schien uns ein riskantes Geschäft zu sein, und es könnte Probleme mit externen Abhängigkeiten geben, die für ein solches Verhalten nicht bereit waren.

    Wir haben die Idee des Patchen von PHP aufgegeben, aber mit der Zeit fiel alles mit den ersten Veröffentlichungen des statischen Analysators Phan zusammen, deren erste Zusagen von Rasmus Lerdorf selbst gemacht wurden. Also kamen wir auf die Idee, statische Codeanalysatoren auszuprobieren.

    Was ist statische Code-Analyse?


    Statische Codeanalysatoren lesen einfach den Code und versuchen, Fehler darin zu finden. Sie können sowohl sehr einfache als auch offensichtliche Überprüfungen durchführen (z. B. das Vorhandensein von Klassen, Methoden und Funktionen sowie weitere schlauere (z. B. nach Typenkonflikt, Racebedingungen oder Schwachstellen von Code). Der Schlüssel besteht darin, dass Analysegeräte keinen Code ausführen Sie analysieren den Text des Programms und prüfen ihn auf typische (und nicht so)

    Fehler. Das offensichtlichste Beispiel für einen statischen PHP-Code-Analysator sind Inspektionen in PHPStorm: Wenn Sie Code schreiben, werden falsche Funktionsaufrufe, Methoden, Nichtübereinstimmung der Parametertypen usw. hervorgehoben und PHPStorm führt Ihren PHP-Code nicht aus, sondern analysiert ihn nur.

    Ich stelle fest, dass wir in diesem Artikel über Analysegeräte sprechen, die nach Fehlern im Code suchen. Es gibt eine andere Klasse von Analysegeräten - sie prüfen den Schreibstil, die Komplexität der Zyklomatik, die Größe der Methoden, die Länge der Zeilen usw. Wir betrachten solche Analysegeräte hier nicht.

    Obwohl nicht alles, was die Analysegeräte, die wir betrachten, genau der Fehler ist. Mit einem Fehler meine ich den Code, der fatal in der Produktion entsteht. Was Analysegeräte häufig feststellen, ist eher eine Ungenauigkeit. In PHPDoc kann beispielsweise ein ungültiger Parametertyp angegeben werden. Diese Ungenauigkeit wirkt sich nicht auf die Funktionsweise des Codes aus, der Code wird jedoch später weiterentwickelt - ein anderer Programmierer macht möglicherweise einen Fehler.

    Bestehende PHP-Code-Analysatoren


    Es gibt drei gängige PHP-Code-Analysatoren:

    1. PHPStan .
    2. Psalm .
    3. Phan .

    Und es gibt Exakat , das wir nicht ausprobiert haben.

    Auf Anwenderseite sind alle drei Analysatoren gleich: Sie installieren sie (am wahrscheinlichsten über Composer), konfigurieren sie, und anschließend können Sie eine Analyse des gesamten Projekts oder einer Gruppe von Dateien durchführen. In der Regel kann der Analysator die Ergebnisse in der Konsole schön darstellen. Sie können die Ergebnisse auch im JSON-Format ausgeben und in CI verwenden.

    Alle drei Projekte entwickeln sich derzeit aktiv. Ihre Betreuer reagieren sehr aktiv auf Probleme in GitHub. Oft reagieren sie am ersten Tag nach der Erstellung eines Tickets zumindest darauf (sie kommentieren oder setzen einen Fehler- / Erweiterungs-Tag). Viele Fehler, die wir fanden, wurden innerhalb weniger Tage behoben. Besonders gut gefällt mir jedoch, dass die Betreuer von Projekten aktiv miteinander kommunizieren, sich gegenseitig Fehler melden und Pull-Anfragen senden.

    Wir haben alle drei Analysatoren implementiert und verwendet. Jeder hat seine eigenen Nuancen, seine eigenen Fehler. Die gleichzeitige Verwendung von drei Analysegeräten macht es jedoch einfacher zu verstehen, wo das eigentliche Problem liegt und wo das Falsche Positiv ist.

    Was können Analysegeräte?


    Analysatoren haben viele gemeinsame Merkmale. Überlegen Sie sich zunächst, was sie alle tun können, und dann wenden wir uns den Funktionen der einzelnen Geräte zu.

    Standardprüfungen


    Natürlich führen die Analysatoren alle Standard-Code-Prüfungen durch, um Folgendes sicherzustellen:

    • Der Code enthält keine Syntaxfehler.
    • alle Klassen, Methoden, Funktionen, Konstanten existieren;
    • Variablen existieren;
    • In PHPDoc sind Hinweise zutreffend.

    Darüber hinaus prüfen die Analysatoren den Code auf nicht verwendete Argumente und Variablen. Viele dieser Fehler führen zu echtem fatalem Code.

    Auf den ersten Blick scheint es so, als würden gute Programmierer solche Fehler nicht machen, aber manchmal haben wir es eilig, manchmal kopieren und einfügen, manchmal sind wir einfach rücksichtslos. Und in solchen Fällen werden diese Prüfungen sehr gespart.

    Datentypprüfungen


    Natürlich führen statische Analysatoren auch Standardprüfungen von Datentypen durch. Wenn der Code besagt, dass die Funktion beispielsweise int akzeptiert, überprüft der Analysator, ob es Orte gibt, an denen ein Objekt an diese Funktion übergeben wird. Bei den meisten Analysatoren können Sie die Testhärte festlegen und strict_types imitieren: Stellen Sie sicher, dass keine Zeichenfolge oder kein Boolean an diese Funktion übergeben wird.

    Zusätzlich zu den Standardprüfungen haben Analysatoren noch viel zu tun:

    Unionstypen:

    Alle Analysatoren unterstützen das Konzept der Unionstypen. Angenommen, Sie haben eine Typfunktion:

    /** 	
     * @var string|int|bool $yes_or_no
     */functionisYes($yes_or_no) :bool{
        if (\is_bool($yes_or_no)) {
             return $yes_or_no;
         } elseif (is_numeric($yes_or_no)) {
             return $yes_or_no > 0;
         } else {
             return strtoupper($yes_or_no) == 'YES';
         }
    }

    Der Inhalt ist nicht sehr wichtig - der Typ des Eingabeparameters ist wichtig string|int|bool. Das heißt, die Variable $yes_or_no ist entweder eine Zeichenfolge oder eine Ganzzahl oder Boolean.

    Bei Verwendung von PHP kann dieser Funktionsparameter nicht beschrieben werden. In PHPDoc ist dies jedoch möglich, und viele Editoren (z. B. PHPStorm) verstehen es.

    In statischen Analysegeräten wird dieser Typ als Vereinigungstyp bezeichnet , und diese Datentypen können sehr gut geprüft werden. Wenn wir die obige Funktion beispielsweise so schreiben würden (ohne zu prüfen Boolean):

    /** 	
     * @var string|int|bool $yes_or_no
     */functionisYes($yes_or_no) :bool{
         if (is_numeric($yes_or_no)) {
             return $yes_or_no > 0;
         } else {
             return strtoupper($yes_or_no) == 'YES';
         }
    }

    Die Analysatoren würden sehen, dass entweder ein String oder ein Boolean zum strtoupper kommen und einen Fehler zurückgeben kann - Sie können keinen Boolean an den strtoupper übergeben.

    Diese Art der Überprüfung hilft Programmierern, Fehler oder Situationen korrekt zu behandeln, in denen eine Funktion keine Daten zurückgeben kann. Wir schreiben häufig Funktionen, die Daten zurückgeben können oder null:

    // load() возвращает null или объект \User
    $User = UserLoader::load($user_id);
    $User->getName();

    Im Falle eines solchen Codes teilt Ihnen der Analysator mit, dass die Variable $Userhier gleich sein kann nullund dieser Code zum Tod führen kann.

    Geben Sie false ein

    In der PHP-Sprache selbst gibt es eine Reihe von Funktionen, die entweder einen Wert oder einen Wert zurückgeben können. Wenn wir eine solche Funktion schreiben, wie würden wir ihren Typ dokumentieren?

              
     /** @return resource|bool */functionfopen(...){
            …
     }

    Formal scheint hier alles wahr zu sein: fopen liefert entweder eine Ressource oder einen Wert false(der einen Typ hat Boolean). Wenn wir jedoch sagen, dass eine Funktion einen Datentyp zurückgibt, bedeutet dies, dass sie einen beliebigen Wert aus der Menge dieses Datentyps zurückgeben kann. In unserem Beispiel für den Analysator bedeutet dies, dass er und zurückgeben fopen() kann true. Und zum Beispiel im Falle eines solchen Codes:

    $fp = fopen(‘some.file’,’r’);
    if($fp === false) {
          returnfalse;
    }
    fwrite($fp, "some string");

    Die Analysatoren würden sich darüber beschweren, dass fwriteder erste Ressourcenparameter verwendet wird, und wir geben ihn an bool(da der Analysator sieht, dass die Option mit true möglich ist). Aus diesem Grund verstehen alle Analysatoren einen solchen "künstlichen" Datentyp als false, und in unserem Beispiel können wir schreiben @return false|resource. PHPStorm versteht auch diese Typbeschreibung.

    Array-Shapes

    Sehr häufig werden Arrays in PHP als Typ verwendet record- eine Struktur mit einer klaren Liste von Feldern, wobei jedes Feld seinen eigenen Typ hat. Natürlich verwenden viele Programmierer bereits Klassen dafür. In Badoo gibt es jedoch viel veralteten Code, und dort werden aktiv Arrays verwendet. Es kommt auch vor, dass Programmierer zu faul sind, eine separate Klasse für eine einmalige Struktur zu beginnen, und an solchen Orten verwenden sie häufig auch Arrays.

    Das Problem bei solchen Arrays ist, dass es keine klare Beschreibung dieser Struktur (eine Liste von Feldern und deren Typen) im Code gibt. Programmierer können bei der Arbeit mit einer solchen Struktur Fehler machen: Vergessen Sie die erforderlichen Felder oder fügen Sie die "linken" Tasten ein, um den Code noch mehr zu verwirren.

    Analysatoren erlauben die Beschreibung solcher Strukturen:

    /** @param array{scheme:string,host:string,path:string} $parsed_url */functionshowUrl(array $parsed_url){ … }

    In diesem Beispiel haben wir ein Array mit drei String-Feldern beschrieben: scheme, hostund path. Wenn wir uns innerhalb der Funktion einem anderen Feld zuwenden, zeigt der Analysator einen Fehler an.

    Wenn Sie keine Typen beschreiben, werden die Analysatoren versuchen, die Struktur des Arrays zu "erraten", was sich jedoch in der Praxis mit unserem Code als sehr schlecht erweist. :)

    Dieser Ansatz hat einen Nachteil. Angenommen, Sie haben eine Struktur, die im Code aktiv verwendet wird. Es ist unmöglich, einen Pseudotyp an einer Stelle zu deklarieren und ihn dann überall zu verwenden. Sie müssen PHPDoc mit der Array-Beschreibung überall im Code schreiben, was sehr unpraktisch ist, insbesondere wenn viele Felder im Array vorhanden sind. Es wird auch problematisch sein, diesen Typ zu bearbeiten (Felder hinzufügen und löschen).

    Beschreibung der Array-Schlüsseltypen

    In PHP können Array-Schlüssel Ganzzahlen und Strings sein. Manchmal können Typen für die statische Analyse (und für Programmierer) wichtig sein. Mit statischen Analysatoren können Sie Array-Schlüssel in PHPDoc beschreiben:

    /** @var array<int, \User> $users */
    $users = UserLoaders::loadUsers($user_ids);

    In diesem Beispiel haben wir PHPDoc verwendet, um einen Hinweis hinzuzufügen, dass die $usersSchlüssel im Array Ganzzahlen sind und die Werte Klassenobjekte sind \User. Wir könnten den Typ als \ User [] beschreiben. Dies würde dem Analysator mitteilen, dass sich im Array Klassenobjekte befinden \User, aber nichts über den Schlüsseltyp.

    PHPStorm unterstützt dieses Array-Beschreibungsformat ab Version 2018.3.

    Ihr PHPDoc

    PHPStorm-Namespace (und andere Editoren) und statische Analysatoren können PHPDoc unterschiedlich verstehen. Analysegeräte unterstützen beispielsweise dieses Format:

    /** @param array{scheme:string,host:string,path:string} $parsed_url */functionshowUrl($parsed_url){ … }

    Und PHPStorm versteht es nicht. Aber wir können das schreiben:

    /** 
     * @param array $parsed_url
     * @phan-param array{scheme:string,host:string,path:string} $parsed_url
     * @psalm-param array{scheme:string,host:string,path:string} $parsed_url  
    */functionshowUrl($parsed_url){ … }

    In diesem Fall sind sowohl die Analysatoren als auch PHPStorm zufrieden. PHPStorm verwendet @param, und Analysatoren verwenden ihre PHPDoc-Tags.

    Überprüft die PHP-Funktionen


    Diese Art von Prüfungen wird am besten durch ein Beispiel veranschaulicht.

    Wissen wir alle, was die explode () - Funktion zurückgeben kann ? Wenn Sie die Dokumentation überfliegen, wird anscheinend ein Array zurückgegeben. Wenn wir genauer hinschauen, werden wir feststellen, dass dies auch falsch sein kann. In der Tat kann es sowohl Null als auch einen Fehler zurückgeben, wenn Sie die falschen Typen übergeben. Wenn Sie jedoch den falschen Wert mit dem falschen Datentyp übergeben, ist dies bereits ein Fehler. Daher ist diese Option für uns jetzt nicht interessant.

    Aus Sicht des Analysators sollte der Code, wenn die Funktion false oder ein Array zurückgeben kann, höchstwahrscheinlich den Wert false überprüfen. Die Funktion explode () gibt jedoch nur dann false zurück, wenn das Trennzeichen (der erste Parameter) eine leere Zeichenfolge ist. Oft wird es explizit im Code registriert, und Analysatoren können überprüfen, ob sie nicht leer sind. Dies bedeutet, dass die Funktion explode () an dieser Stelle ein Array genau zurückgibt und nicht auf false überprüft werden muss.

    Es gibt nicht so viele PHP-Funktionen. Analysatoren fügen nach und nach relevante Prüfungen hinzu oder verbessern sie, und wir als Programmierer müssen sich nicht mehr alle diese Merkmale merken.

    Wir fahren mit der Beschreibung bestimmter Analysatoren fort.

    PHPStan


    Entwicklung eines bestimmten Ondřej Mirtes aus der Tschechischen Republik. Aktiv entwickelt seit Ende 2016.

    Um mit PHPStan zu beginnen, benötigen Sie:

    1. Installieren Sie es (der einfachste Weg dazu ist Composer).
    2. (optional) Konfigurieren.
    3. Im einfachsten Fall einfach ausführen:

    vendor/bin/phpstan analyse ./src

    (Stattdessen gibt es srcmöglicherweise eine Liste bestimmter Dateien, die Sie überprüfen möchten).

    PHPStan liest den PHP-Code aus den übertragenen Dateien. Wenn ihm unbekannte Klassen begegnen, wird er versuchen, sie mit einem Autoload zu laden und durch Nachdenken deren Schnittstelle zu verstehen. Sie können auch den Pfad zu der BootstrapDatei übertragen, durch die Sie den automatischen Ladevorgang konfigurieren, und einige zusätzliche Dateien hinzufügen, um die PHPStan-Analyse zu vereinfachen.

    Hauptmerkmale:

    1. Sie können nicht die gesamte Codebasis analysieren, sondern nur einen Teil - PHPStan versucht, unbekannte Klassen mit Autoload zu laden.
    2. Wenn sich einige Ihrer Klassen aus irgendeinem Grund nicht im automatischen Laden befinden, kann PHPStan sie nicht finden und gibt einen Fehler aus.
    3. Wenn Sie magische Methoden aktiv einsetzen __call / __get / __set, können Sie ein Plugin für PHPStan schreiben. Es gibt bereits Plugins für Symfony, Doctrine, Laravel, Spott etc.
    4. Tatsächlich führt PHPStan Autoloads nicht nur für unbekannte Klassen aus, sondern generell für alle. Wir haben viel alten Code geschrieben, bevor anonyme Klassen erscheinen, wenn wir eine Klasse in einer Datei erstellen und dann sofort instanziieren und vielleicht sogar einige Methoden aufrufen. Autoload ( include) solcher Dateien führt zu Fehlern, da der Code in einer normalen Umgebung nicht ausgeführt wird.
    5. Konfiguriert im Neon- Format (hat dieses Format nirgendwo anders gehört).
    6. Keine Unterstützung für Ihre PHPDoc-Typ-Tags @phpstan-var, @phpstan-returnusw.

    Eine weitere Funktion ist, dass Fehler Text haben, aber es gibt keinen Typ. Das heißt, ein Fehlertext wird an Sie zurückgegeben, zum Beispiel:

    • Method \SomeClass::getAge() should return int but returns int|null
    • Method \SomeOtherClass::getName() should return string but returns string|null

    In diesem Beispiel sind beide Fehler im Allgemeinen ungefähr gleich: Die Methode muss einen Typ zurückgeben, in Wirklichkeit jedoch den anderen. Die Fehlertexte sind jedoch unterschiedlich, wenn auch ähnlich. Wenn Sie also Fehler in PHPStan herausfiltern möchten, tun Sie dies nur durch reguläre Ausdrücke.

    Zum Vergleich haben Fehler in anderen Analysatoren einen Typ. In Phan hat ein solcher Fehler beispielsweise einen Typ PhanPossiblyNullTypeReturn, und Sie können in der Konfiguration angeben, dass keine Überprüfung auf solche Fehler erforderlich ist. Mit der Art des Fehlers können Sie beispielsweise einfach Statistiken zu Fehlern erfassen.

    Da wir keine Laravel-, Symfony-, Doctrine- und ähnliche Lösungen verwenden und im Code selten magische Methoden verwenden, wurde das Hauptmerkmal von PHPStan nicht beansprucht. (Außerdem aufgrund der Tatsache, dass PHPStan alles enthältIn getesteten Klassen funktioniert die Analyse manchmal nicht auf unserer Codebasis.

    Trotzdem bleibt PHPStan für uns nützlich:

    • Wenn Sie mehrere Dateien prüfen müssen, ist PHPStan deutlich schneller als Phan und etwas (20-50%) schneller als Psalm.
    • PHPStan-Berichte erleichtern uns das Auffinden false-positivein anderen Analysegeräten. Wenn im Code expliziter Code vorhanden ist, wird er normalerweise fatalvon allen Analysatoren (oder mindestens zwei der drei) angezeigt.


    Update:
    Der Autor von PHPStan Ondřej Mirtes las auch unseren Artikel und schlug vor, dass PhpStan wie Psalm eine Sandkasten-Site hat: https://phpstan.org/ . Dies ist sehr praktisch für Fehlerberichte: Sie reproduzieren den Fehler in GitHub und geben einen Link ein.

    Phan


    Entwickelt von Etsy. Erste Zusagen von Rasmus Lerdorf.

    Von den drei genannten Faktoren ist Phan der einzige echte statische Analysator (in dem Sinne, dass er keine Ihrer Dateien ausführt - er analysiert die gesamte Codebasis und analysiert dann, was Sie sagen). Selbst für die Analyse mehrerer Dateien in unserer Codebasis sind etwa 6 GB RAM erforderlich. Dieser Vorgang dauert vier bis fünf Minuten. Eine vollständige Analyse der gesamten Codebasis dauert dann etwa sechs bis sieben Minuten. Zum Vergleich analysiert Psalm es mehrere Dutzend Minuten. Und von PHPStan aus konnten wir keine vollständige Analyse der gesamten Codebasis durchführen, da sie Klassen enthält.

    Der Eindruck von Phan ist zweifach. Einerseits ist es der qualitativ hochwertigste und stabilste Analysator. Er findet eine Menge und die geringsten Probleme damit, wenn die gesamte Codebasis analysiert werden muss. Auf der anderen Seite hat es zwei unangenehme Merkmale.

    Unter der Haube verwendet Phan die php-ast-Erweiterung. Offensichtlich ist dies einer der Gründe, warum die Analyse der gesamten Codebasis relativ schnell ist. Php-ast zeigt jedoch die interne Darstellung des AST-Baums, wie er in PHP selbst erscheint. In PHP selbst enthält der AST-Baum keine Informationen zu Kommentaren, die sich innerhalb der Funktion befinden. Das heißt, wenn Sie etwas geschrieben haben wie:

    /**
     * @param int $type
     */functiondoSomething($type){
        /** @var \My\Object $obj **/
        $obj = MyFactory::createObjectByType($type);
        …
    }

    Innerhalb des AST-Baums befinden sich Informationen zu der externen PHPDoc-Funktion für die Funktion doSomething(), aber es gibt keine PHPDoc-Hinweisinformationen, die sich in der Funktion befinden. Dementsprechend weiß auch Phan nichts über sie. Dies ist die häufigste Ursache false-positivein Phan. Es gibt einige Empfehlungen zum Einfügen von Hinweisen (über Strings oder Asserts), die sich jedoch leider sehr von denen unterscheiden, die unsere Programmierer gewohnt sind. Wir haben dieses Problem teilweise gelöst, indem wir ein Plugin für Phan geschrieben haben. Plugins werden jedoch unten beschrieben.

    Die zweite unangenehme Eigenschaft ist, dass Phan die Eigenschaften von Objekten schlecht analysiert. Hier ist ein Beispiel:

    classA{
        /**
         * @var string|null
         */private $a;
         publicfunction__construct(string $a = null){
               $this->a = $a;
         }
         publicfunctiondoSomething(){
               if ($this->a && strpos($this->a, 'a') === 0) {
                    var_dump("test1");
               }
         }
    }

    In diesem Beispiel sagt Phan Ihnen, dass Sie in strpos null übergeben können. Weitere Informationen zu diesem Problem finden Sie hier: https://github.com/phan/phan/issues/204 .

    Zusammenfassung Trotz einiger Schwierigkeiten ist Phan eine sehr coole und nützliche Entwicklung. Neben diesen beiden Typen macht false-positiveer fast keine Fehler oder Fehler, sondern etwas wirklich komplexen Code. Uns hat auch gefallen, dass sich die config in einer PHP-Datei befindet - dies gibt eine gewisse Flexibilität. Phan weiß auch, wie man als Sprachserver arbeitet, aber wir haben diese Funktion nicht verwendet, da PHPStorm für uns ausreichend ist.

    Plugins


    Phan verfügt über eine gut entwickelte Plugin-API. Sie können eigene Schecks hinzufügen und die Typinferenz für Ihren Code verbessern. Diese API verfügt über eine Dokumentation , aber es ist besonders cool, dass bereits vorgefertigte funktionsfähige Plugins vorhanden sind, die als Beispiele verwendet werden können.

    Es ist uns gelungen, zwei Plugins zu schreiben. Die erste war für eine einmalige Überprüfung vorgesehen. Wir wollten auswerten, wie unser Code für PHP 7.3 bereit ist (insbesondere, um herauszufinden, ob es case-insensitiveKonstanten gibt ). Wir waren uns fast sicher, dass es keine solchen Konstanten gab, aber in 12 Jahren hätte etwas passieren können - es hätte überprüft werden müssen. Und wir haben ein Plugin für Phan geschrieben, das bei Verwendung des define() dritten Parameters fluchen würde .

    Das Plugin ist sehr einfach.
    <?phpdeclare(strict_types=1);
    usePhan\AST\ContextNode;
    usePhan\CodeBase;
    usePhan\Language\Context;
    usePhan\Language\Element\Func;
    usePhan\PluginV2;
    usePhan\PluginV2\AnalyzeFunctionCallCapability;
    useast\Node;
    classDefineThirdParamTrueextendsPluginV2implementsAnalyzeFunctionCallCapability{
        publicfunctiongetAnalyzeFunctionCallClosures(CodeBase $code_base) : array{ 	
            $define_callback = function(	
                       CodeBase $code_base, 
                       Context $context, 
                       Func $function, 
                       array $args
             ){
                 if (\count($args) < 3) {   	
                     return;
                  }	
                  $this->emitIssue(  	
                       $code_base,
                       $context,
                       'PhanDefineCaseInsensitiv',
                       'Define with 3 arguments',
                       []
                 );
             };
             return [  	
                 'define'  => $define_callback,
             ];
         }
    }
    returnnew DefineThirdParamTrue();



    In Phan können verschiedene Plugins zu verschiedenen Ereignissen aufgehängt werden. Insbesondere AnalyzeFunctionCallCapabilityfunktionieren Schnittstellen-Plug-Ins , wenn ein Funktionsaufruf analysiert wird. In diesem Plugin haben wir es so gemacht, dass beim Aufruf einer Funktion define() unsere anonyme Funktion aufgerufen wird, die prüft, ob sie define() höchstens zwei Argumente hat. Dann haben wir gerade mit Phan angefangen, alle Orte, an denen ich define()angerufen wurde, mit drei Argumenten gefunden und dafür gesorgt, dass wir es nicht tun case-insensitive-констант.

    Mit Hilfe des Plugins haben wir das Problem auch teilweise gelöst, false-positivewenn Phan PHPDoc-Hinweise nicht im Code sieht.

    Wir verwenden häufig Factory-Methoden, die eine Konstante als Eingabe verwenden und daraus ein Objekt erstellen. Oft sieht der Code so aus:

    /** @var \Objects\Controllers\My $Object */
    $Object = \Objects\Factory::create(\Objects\Config::MY_CONTROLLER);

    Phan versteht solche PHPDoc-Hinweise nicht, aber in diesem Code kann die Klasse des Objekts aus dem Namen der Konstanten abgerufen werden, die an die Methode übergeben wird create(). Mit Phan können Sie ein Plugin schreiben, das bei der Analyse des Rückgabewerts einer Funktion ausgelöst wird. Mit diesem Plugin können Sie dem Analysator mitteilen, welche Art von Funktion die Funktion bei diesem Aufruf zurückgibt.

    Ein Beispiel für dieses Plugin ist komplizierter. Im Phan-Code gibt es jedoch ein gutes Beispiel: vendor/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php.

    Im Allgemeinen sind wir mit dem Phan-Analysegerät sehr zufrieden. Oben gelistet haben false-positivewir teilweise (in einfachen Fällen mit einem einfachen Code) das Filtern. Danach wurde Phan fast ein Standardanalysator. Die Notwendigkeit, die gesamte Codebasis (Zeit und viel Speicher) sofort zu parsen, verkompliziert jedoch immer noch den Implementierungsprozess.

    Psalm


    Psalm ist eine Vimeo-Entwicklung. Ehrlich gesagt wusste ich nicht einmal, dass Vimeo PHP benutzt, bis ich Psalm sah.

    Dieser Analysator ist der jüngste unserer Troika. Als ich die Nachricht las, dass Vimeo Psalm freigelassen hatte, war ich ratlos: "Warum sollten Sie Ressourcen in Psalm investieren, wenn Sie bereits Phan und PHPStan haben?"

    Psalm folgte den Spuren von PHPStan: Es kann auch eine Liste von Dateien zur Analyse angegeben werden, die sie analysiert und die fehlenden Klassen mit Autoload verbindet. Gleichzeitig werden nur nicht gefundene Klassen berücksichtigt, und die zu analysierenden Dateien werden nicht eingeschlossen (dies ist im Gegensatz zu PHPStan). Die Konfiguration wird in einer XML-Datei gespeichert (für uns ist dies eher ein Minus, aber nicht sehr kritisch).

    Psalm hat eine Website.mit einer Sandbox, in der Sie PHP-Code schreiben und analysieren können. Dies ist sehr praktisch für Fehlerberichte: Sie reproduzieren den Fehler auf der Site und geben einen Link in GitHub an. Die Site beschreibt übrigens alle möglichen Arten von Fehlern. Zum Vergleich: Es gibt keine Arten von Fehlern in PHPStan, aber sie sind in Phan, aber es gibt keine einzelne Liste, die gefunden werden könnte.

    Es hat uns auch gefallen, dass Psalm beim Anzeigen von Fehlern sofort die Codezeilen anzeigt, in denen sie gefunden wurden. Dies vereinfacht das Lesen von Berichten erheblich .

    Das vielleicht interessanteste Merkmal von Psalm sind jedoch seine benutzerdefinierten PHPDoc-Tags, die eine verbesserte Analyse ermöglichen (insbesondere die Definition von Typen). Wir listen die interessantesten von ihnen auf.

    @ psalm-ignore-nullable-return


    Es kommt manchmal vor, dass die Methode formal zurückgegeben werden kann null, aber der Code ist bereits so organisiert, dass er niemals auftritt. In diesem Fall ist es sehr praktisch, diesen PHPDoc-Hinweis der Methode / Funktion hinzuzufügen - und Psalm geht davon aus, dass er nullnicht zurückgegeben wird.

    Die gleiche Karte gibt und auf false: @psalm-ignore-falsable-return.

    Typen für den Verschluss


    Wenn Sie jemals an einer funktionalen Programmierung interessiert waren, haben Sie vielleicht bemerkt, dass eine Funktion oft eine andere Funktion zurückgeben oder eine Funktion als Parameter übernehmen kann. In PHP kann dieser Stil für Ihre Kollegen sehr verwirrend sein. Einer der Gründe ist, dass PHP keine Standards zur Dokumentation solcher Funktionen enthält. Zum Beispiel:

    functionmy_filter(array $ar, \Closure $func){ … }

    Wie kann ein Programmierer verstehen, welche Schnittstelle die Funktion im zweiten Parameter hat? Welche Parameter sollte es nehmen? Was soll sie zurückgeben?

    Psalm unterstützt die Syntax zur Beschreibung von Funktionen in PHPDoc:

    /**
      * @param array $ar
      * @psalm-param Closure(int):bool $func
      */functionmy_filter(array $ar, \Closure $func){ … }

    Mit dieser Beschreibung ist bereits klar, dass my_filterSie eine anonyme Funktion übergeben müssen, die ein int als Eingabe und ein Rückgabewert von bool verwendet. Und natürlich wird Psalm überprüfen, ob Sie hier im Code genau eine solche Funktion haben.

    Aufzählungen


    Angenommen, Sie haben eine Funktion, die einen Zeichenfolgenparameter akzeptiert, und nur bestimmte Zeichenfolgen können dort übergeben werden:

    functionisYes(string $yes_or_no) : bool{
          $yes_or_no = strtolower($yes_or_no)
          switch($yes_or_no)  {
                case ‘yes’:
                      returntrue;
                case ‘no’:
                      returnfalse;
                default:
                      thrownew \InvalidArgumentException(…);
          }
    }
    

    In Psalm können Sie den Parameter dieser Funktion folgendermaßen beschreiben:

    /** @psalm-param ‘Yes’|’No’ $yes_or_no **/functionisYes(string $yes_or_no) : bool{ … }

    In diesem Fall versucht Psalm zu verstehen, welche bestimmten Werte an diese Funktion übergeben werden, und gibt Fehler aus, wenn andere Werte als Yesund vorhanden sind No.

    Weitere Informationen über ENUM-ah hier .

    Geben Sie Aliase ein


    In der obigen Beschreibung habe array shapesich erwähnt, dass, obwohl Analysegeräte es uns ermöglichen, die Struktur von Arrays zu beschreiben, es nicht sehr bequem ist, dies zu verwenden, da die Beschreibung des Arrays an verschiedenen Stellen kopiert werden muss. Die richtige Lösung ist natürlich die Verwendung von Klassen anstelle von Arrays. Bei mehrjährigem Erbe ist dies jedoch nicht immer möglich.

    Tatsächlich tritt das Problem nicht nur bei Arrays auf, sondern bei jedem Typ, der keine Klasse ist:

    • Array;
    • Schließung;
    • Union-Typ (z. B. mehrere Klassen oder Klassen und andere Typen);
    • enum.

    Jeder dieser Typen muss, wenn er an mehreren Stellen verwendet wird, in PHPDoc dupliziert werden. Wenn er geändert wird, sollte er entsprechend korrigiert werden. Daher hat Psalm in dieser Hinsicht eine leichte Verbesserung. Sie können einen Alias ​​für einen Typ deklarieren und diesen dann in PHPDoc verwenden alias. Leider gibt es eine Einschränkung: Es funktioniert in einer einzigen PHP-Datei. Es vereinfacht aber bereits die Beschreibung der Typen. Nur für Psalm.

    Generics aka Vorlagen


    Betrachten Sie diese Gelegenheit am Beispiel. Angenommen, Sie haben diese Funktion:

    functionidentity($x){ return $x; }

    Wie beschreibt man den Typ dieser Funktion? Welchen Typ braucht es um einzutreten? Was kehrt sie zurück?

    Wahrscheinlich ist das erste, was mir einfällt mixed, das heißt, es kann einen beliebigen Wert als Eingabe annehmen und einen beliebigen Wert zurückgeben.

    Für einen statischen Analysator ist das Meeting mixedeine Katastrophe. Das heißt, es gibt absolut keine Informationen über den Typ und es können keine Annahmen gemacht werden. Obwohl die Funktion zwar identity()alle Typen akzeptiert / zurückgibt, hat sie jedoch die Logik: Sie gibt den gleichen Typ zurück, den sie verwendet hat. Und für einen statischen Analysator ist das schon etwas. Das bedeutet im Code:

    $i = 5; // int
    $y = identity($i);

    Analysator kann die Art der ankommenden Argumente bestimmen (int), und daher die Art der Variablen bestimmen kann , und $y(auch int).

    Aber wie geben wir diese Informationen an den Analysator weiter? In Psalm gibt es dafür spezielle PHPDoc-Tags:

    /**
      * @template T
      * @psalm-param T $x
      * @psalm-return T
      */functionidentity($x){ $return $x; }

    Das heißt, mit Vorlagen können Sie Informationen über den Psalm-Typ übergeben, wenn die Klasse / Methode mit einem beliebigen Typ arbeiten kann.

    Im Psalm gibt es gute Beispiele für das Arbeiten mit Vorlagen:

    - Vendor / Vimeo / Psalm / Src / Psalm / Stubs / CoreGenericFunctions.php ;
    - verkäufer / vimeo / psalm / src / psalm / Stubs / CoreGenericClasses.php .

    Eine ähnliche Funktionalität gibt es in Phan, sie funktioniert jedoch nur mit Klassen: https://github.com/phan/phan/wiki/Generic-Types .

    Insgesamt hat uns Psalm sehr gut gefallen. Es scheint, dass der Autor versucht, das intelligentere Typensystem und die intelligenteren und praktisch nützlichen Tipps für das Analysegerät "seitlich" anzuschrauben. Es hat uns auch gefallen, dass Psalm sofort die Codezeilen anzeigt, in denen Fehler gefunden wurden, und wir haben dies sogar für Phan und PHPStan implementiert. Aber dazu unten mehr.

    Code-Überprüfung in PHPStorm


    Die Analysatoren haben einen gemeinsamen kleinen Fehler: Sie erhalten Informationen zum Fehler nicht zum Zeitpunkt des Schreibens des Codes, sondern viel später. Normalerweise schreiben Sie Code, öffnen die Konsole, führen die Analysegeräte aus und erhalten einen Bericht.

    Es wäre für einen Programmierer praktischer, Informationen über Fehler im Code-Bearbeitungsprozess zu erhalten. Phan geht in diese Richtung, die seinen Sprachserver entwickelt. Aber wir bei PHPStorm haben es leider nicht gern.

    Glücklicherweise verfügt PHPStorm über einen eigenen hervorragenden Analysator (Code-Inspektion), der in der Qualität mit den oben beschriebenen Lösungen vergleichbar ist. Außerdem gibt es ein cooles Plugin - Php Inspections (EA Extended). Der Hauptunterschied von Analysegeräten ist die Bequemlichkeit für den Programmierer, die darin besteht, dass beim Schreiben von Code Fehler im Editor sichtbar sind. Darüber hinaus können diese Inspektionen sehr fein abgestimmt werden. Sie können beispielsweise verschiedene Bereichsdateien in einem Projekt auswählen und Prüfungen für verschiedene Bereiche unterschiedlich einrichten.

    Ich würde auch auf solch ein nützliches Plugin als Deep-Assoc-Completion hinweisen . Er kennt die Struktur von Arrays und vereinfacht die automatische Vervollständigung von Schlüsseln.

    Verwendung von Analysatoren in Badoo


    Wie funktioniert das bei uns?

    Heutzutage wird die statische Code-Analyse von mehreren Teams verwendet. Wir planen jedoch, diese Praxis in allen Bereichen umzusetzen.

    Wir analysieren nur die geänderten Dateien bis hin zu den Zeilen. Das heißt, wenn der Entwickler seine Aufgabe abgeschlossen hat, nehmen wir die Analyse git diff und führen die Analysatoren nur für geänderte / hinzugefügte Dateien aus. Aus der resultierenden Fehlerliste entfernen wir diejenigen, die zu den alten (unveränderten) Zeilen gehören. So verbergen wir dem Entwickler die Fehler, die zuvor gemacht wurden.

    Natürlich ist dieser Ansatz nicht ganz korrekt: Ein Programmierer kann mit seinem Code etwas außerhalb seines Codes brechen.git diff. Hier gehen wir Kompromisse ein. Auch in dieser Form bringt die statische Analyse ihre Früchte in Form von Fehlern, die im neuen Code gefunden werden. Und wir wollen den Programmierer nicht zwingen, alte Fehler zu korrigieren. Wenn unser Code in Zukunft aus Sicht der Analysatoren sauberer wird, werden wir diese Entscheidung natürlich noch einmal überdenken.

    Nachdem wir Berichte von drei Analysatoren erhalten haben, fassen wir sie zu einem zusammen, in dem Fehler nach Dateien und Zeilen gruppiert werden:



    Beim Erstellen dieses Berichts versuchen wir auch, einige zu entfernen false-positive. Wir erinnern uns zum Beispiel daran, dass Phan Probleme mit der Bestimmung des Typs für die Eigenschaften von Objekten hat und ungefähr wissen, welchen Typ diese Fehler haben. Wenn nur Phan sich über eine Zeile beschwert und die Art des Fehlers derjenigen ähnelt, in der er häufig Fehler macht, verbergen wir diesen Fehler vor dem Programmierer.

    Ort der Analysatoren in unserer QS


    Wir tun unser Bestes, um die Anzahl der Fehler in der Produktion zu reduzieren:



    Statische Analysatoren sind in der Tat ein weiteres Werkzeug auf dieser Liste und ergänzen es gut. Statische Analysatoren haben mehrere Vorteile:

    • Sie können 100% des Codes abdecken (im Gegensatz zu Tests, die für jeden Codeabschnitt separat geschrieben werden sollten).
    • Sie fangen häufig solche Fehler auf, die im Codeüberprüfungsprozess schwer zu bemerken sind.
    • Sie können sogar den Code analysieren, der mit manuellen Tests nicht oder nur schwer auszuführen ist.

    Die Implementierung statischer Analysen ist aus der Implementierungsidee herausgewachsen strict types. Infolgedessen haben uns statische Analysegeräte sehr viel mehr überprüft strict typesund sind flexibler:

    • Analysatoren arbeiten für den gesamten Code, und um die Fehler zu sehen strict types, müssen Sie den Code ausführen.
    • Der Fehler des Analysators kann später korrigiert werden, wenn er nicht kritisch ist (dies ist möglicherweise nicht die beste Vorgehensweise, kann jedoch in einigen Fällen nützlich sein).
    • Das Typsystem in statischen Analysatoren ist noch flexibler als in PHP selbst (zum Beispiel wird es unterstützt union types, was nicht in PHP vorhanden ist).
    • Statische Analysatoren bringen uns näher an die Implementierung strict types, da sie die gleichen strengen Kontrollen nachahmen können.

    Analyseberichte: Die Meinung der Programmierer


    Dies bedeutet nicht, dass alle Programmierer von statischen Analysatoren begeistert sind. Dafür gibt es mehrere Gründe.

    Erstens haben viele Leute Misstrauen gegenüber den Analysatoren, weil sie glauben, dass diese nur einige primitive Fehler finden können, die wir nicht zulassen.

    Zweitens sind viele Analyseberichte, wie oben erwähnt, lediglich Ungenauigkeiten, beispielsweise falsch spezifizierte Typen in PHPDoc. Einige Programmierer lehnen solche Fehler ab - der Code funktioniert.

    Drittens haben einige Programmierer hohe Erwartungen. Sie dachten, dass die Analysatoren einige knifflige Fehler finden würden, und zwangen sie stattdessen, Typprüfungen hinzuzufügen und PHPDoc zu beheben. :)

    Die Vorteile der Analysegeräte setzen jedoch alle diese geringfügigen Beschwerden außer Kraft. Was auch immer man sagen mag, dies ist eine gute Investition in den zukünftigen Code.

    Jetzt auch beliebt: