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



Badoo gibt es seit über 12 Jahren. Wir haben viel PHP-Code (Millionen von 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 laden den Code zweimal täglich hoch und jedes Layout enthält ungefähr 10-20 Aufgaben. Darüber hinaus können Programmierer dringende Patches veröffentlichen - kleine Änderungen. Und am Tag solcher Patches gewinnen 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 Qualität des Codes zu verbessern. Eines Tages beschlossen wir, eine statische Code-Analyse zu implementieren. Was dabei herauskam, lesen Sie unter dem Schnitt.

Strenge Typen: Warum verwenden wir es noch nicht?


Einmal begann eine Diskussion in unserem PHP-Chat für Unternehmen. Einer der neuen Mitarbeiter erzählte, wie er am vorherigen Arbeitsplatz die obligatorischen Hinweise für strict_types + scalar type für den gesamten Code eingeführt hat - und dies hat die Anzahl der Fehler in der Produktion erheblich reduziert.

Die meisten Chat-Oldtimer waren gegen eine solche Innovation. Der Hauptgrund war, dass PHP keinen Compiler hat, der alle Typen im Code zum Zeitpunkt der Kompilierung überprüft. Wenn Sie den Code nicht zu 100% mit Tests abdecken, besteht immer das Risiko, dass Fehler in der Produktion auftreten, was wir nicht tun wollen erlauben.

Natürlich findet strict_types einen bestimmten Prozentsatz von Fehlern, die durch Typinkongruenz verursacht werden, und wie PHP Typen "stillschweigend" konvertiert. Viele erfahrene PHP-Programmierer wissen jedoch bereits, wie das Typsystem in PHP funktioniert, nach welchen Regeln die Typkonvertierung erfolgt, und in den meisten Fällen schreiben sie korrekten Arbeitscode.

Aber die Idee, ein bestimmtes System zu haben, das anzeigt, wo im Code eine Typinkongruenz vorliegt, hat uns gefallen. Wir haben über Alternativen zu strict_types nachgedacht.

Zuerst wollten wir sogar PHP patchen. Wir wollten, dass TypeError (was an sich eine Ausnahme darstellt) nicht ausgelöst wird, wenn die Funktion einen Skalartyp (z. B. int) verwendet und ein anderer Skalartyp (wie float) eingeht, sondern eine Typkonvertierung erfolgt. sowie das Protokollieren dieses Ereignisses in error.log. Dies würde es uns ermöglichen, alle Stellen zu finden, an denen unsere Annahmen über Typen falsch sind. Aber ein solcher Patch schien uns riskant, und selbst es könnte Probleme mit externen Abhängigkeiten geben, die für ein solches Verhalten nicht bereit sind.

Wir haben die Idee des Patchens von PHP aufgegeben, aber mit der Zeit fiel alles mit den ersten Versionen des statischen Phan-Analysators zusammen, die ersten Commits, die von Rasmus Lerdorf selbst vorgenommen wurden. Deshalb kamen wir auf die Idee, statische Code-Analysatoren auszuprobieren.

Was ist statische Code-Analyse?


Statische Code-Analysatoren lesen einfach den Code und versuchen, Fehler darin zu finden. Sie können sehr einfache und offensichtliche Überprüfungen durchführen (z. B. auf das Vorhandensein von Klassen, Methoden und Funktionen sowie auf schwierigere (z. B. nach Typinkongruenzen, Rassenbedingungen oder Schwachstellen im Code). Der Schlüssel ist, dass Analysatoren keinen Code ausführen - sie Analysieren Sie den Programmtext und überprüfen Sie 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 Aufrufe von Funktionen, Methoden, Fehlanpassungen von Parametertypen usw. hervorgehoben. PHPStorm führt Ihren PHP-Code jedoch nicht aus, sondern analysiert ihn nur.

Ich stelle fest, dass es sich in diesem Artikel um Analysatoren handelt, die nach Fehlern im Code suchen. Es gibt eine andere Klasse von Analysatoren - sie überprüfen den Schreibstil des Codes, die zyklomatische Komplexität, die Methodengrößen, die Zeilenlängen usw. Wir betrachten solche Analysatoren hier nicht.

Obwohl nicht alles, was die von uns in Betracht gezogenen Analysegeräte finden, genau ein Fehler ist. Aus Versehen meine ich den Code, den Fatal in der Produktion erstellt. Sehr oft ist es wahrscheinlicher, dass Analysatoren eine Ungenauigkeit feststellen. Beispielsweise kann in PHPDoc ein falscher Parametertyp angegeben werden. Diese Ungenauigkeit wirkt sich nicht auf den Betrieb des Codes aus, aber anschließend entwickelt sich der Code weiter - ein anderer Programmierer kann einen Fehler machen.

Bestehende PHP-Code-Analysatoren


Es gibt drei beliebte PHP-Code-Analysatoren:

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

Und da ist Exakat , das wir nicht ausprobiert haben.

Auf der Benutzerseite sind alle drei Analysatoren gleich: Sie installieren sie (höchstwahrscheinlich über Composer) und konfigurieren sie. Anschließend können Sie die Analyse des gesamten Projekts oder der gesamten Dateigruppe starten. In der Regel kann der Analysator die Ergebnisse in der Konsole schön anzeigen. Sie können die Ergebnisse auch im JSON-Format ausgeben und in CI verwenden.

Alle drei Projekte entwickeln sich jetzt aktiv weiter. Ihre Betreuer reagieren sehr aktiv auf Probleme mit GitHub. Oft reagieren sie am ersten Tag nach dem Erstellen eines Tickets zumindest darauf (kommentieren oder setzen Sie ein Tag wie Bug / Enhancement). Viele der gefundenen Fehler wurden innerhalb weniger Tage behoben. Besonders gut gefällt mir aber, dass Projektbetreuer aktiv miteinander kommunizieren, Fehler untereinander melden und Pull-Anfragen senden.

Wir haben alle drei Analysegeräte implementiert und eingesetzt. Jeder hat seine eigenen Nuancen, seine eigenen Fehler. Die gleichzeitige Verwendung von drei Analysatoren erleichtert jedoch das Verständnis, wo das eigentliche Problem liegt und wo das falsch positive ist.

Was Analysatoren können


Analysatoren haben viele gemeinsame Funktionen. Wir werden uns also zunächst ansehen, was sie alle können, und dann zu den Funktionen der einzelnen Funktionen übergehen.

Standardprüfungen


Natürlich führen Analysatoren alle Standardcodeprüfungen durch, um Folgendes festzustellen:

  • Der Code enthält keine Syntaxfehler.
  • Alle Klassen, Methoden, Funktionen und Konstanten existieren.
  • Variablen existieren;
  • In PHPDoc sind die Hinweise wahr.

Außerdem überprüfen Parser den Code auf nicht verwendete Argumente und Variablen. Viele dieser Fehler führen zu echten Todesfällen im Code.

Auf den ersten Blick scheint es, dass gute Programmierer solche Fehler nicht machen, aber manchmal haben wir es eilig, manchmal kopieren und einfügen, manchmal sind wir einfach unaufmerksam. In solchen Fällen sparen diese Überprüfungen viel.

Datentypprüfungen


Selbstverständlich führen statische Analysegeräte auch Standardprüfungen hinsichtlich der Datentypen durch. Wenn in dem Code geschrieben ist, dass die Funktion beispielsweise int akzeptiert, prüft der Analysator, ob es Stellen gibt, an denen ein Objekt an diese Funktion übergeben wird. Bei den meisten Analysatoren können Sie den Schweregrad des Tests konfigurieren und strict_types simulieren: Stellen Sie sicher, dass keine Zeichenfolgen oder Booleschen Werte an diese Funktion übergeben werden.

Neben Standardprüfungen haben Analysatoren noch viel zu tun.

Unionstypen

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

/** * @var string|int|bool $yes_or_no */ function isYes($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 string|int|bool ist wichtig. Das heißt, die Variable $yes_or_no ist entweder eine Zeichenfolge oder eine Ganzzahl oder ein Boolean $yes_or_no .

Mit PHP kann diese Art von Funktionsparameter nicht beschrieben werden. In PHPDoc ist dies jedoch möglich, und viele Editoren (wie PHPStorm) verstehen es.

In statischen Analysatoren wird dieser Typ als Vereinigungstyp bezeichnet , und sie können solche Datentypen sehr gut überprüfen. Wenn wir zum Beispiel die obige Funktion wie folgt geschrieben haben (ohne nach Boolean zu Boolean ):

 /** * @var string|int|bool $yes_or_no */ function isYes($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 Boolescher Wert zu strtoupper kommen und einen Fehler zurückgeben könnten. Sie können keinen Booleschen Wert an strtoupper übergeben.

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

 // load()  null   \User $User = UserLoader::load($user_id); $User->getName(); 

Im Fall eines solchen Codes teilt Ihnen der Analysator mit, dass die Variable $User hier null und dieser Code zu schwerwiegenden Folgen führen kann.

Geben Sie false ein

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

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

Formal scheint hier alles wahr zu sein: fopen gibt entweder resource oder false (was vom Typ Boolean ). Wenn wir jedoch sagen, dass eine Funktion einen Datentyp zurückgibt, bedeutet dies, dass sie einen beliebigen Wert aus einer Menge zurückgeben kann, die zu diesem Datentyp gehört. In unserem Beispiel bedeutet dies für den Analysator, dass fopen() true . Und zum Beispiel im Fall eines solchen Codes:

 $fp = fopen('some.file','r'); if($fp === false) {     return false; } fwrite($fp, "some string"); 

Die Analysatoren würden sich beschweren, dass fwrite die erste Parameterressource akzeptiert, und wir übergeben bool (weil der Analysator fwrite , dass eine echte Option möglich ist). Aus diesem Grund verstehen alle Analysatoren einen solchen „künstlichen“ Datentyp als false , und in unserem Beispiel können wir @return false|resource schreiben. PHPStorm versteht auch diese Typbeschreibung.

Array-Formen

Sehr oft werden Arrays in PHP als record verwendet - eine Struktur mit einer übersichtlichen Liste von Feldern, wobei jedes Feld seinen eigenen Typ hat. Natürlich verwenden viele Programmierer bereits Klassen dafür. Aber wir haben viel Legacy-Code in Badoo und Arrays werden dort aktiv verwendet. Und es kommt auch vor, dass Programmierer zu faul sind, um eine separate Klasse für eine einmalige Struktur zu erstellen, und an solchen Stellen werden häufig auch Arrays verwendet.

Das Problem bei solchen Arrays besteht darin, dass der Code keine eindeutige Beschreibung dieser Struktur (eine Liste der Felder und ihrer Typen) enthält. Programmierer können bei der Arbeit mit einer solchen Struktur Fehler machen: Vergessen Sie die erforderlichen Felder oder fügen Sie "linke" Tasten hinzu, was den Code weiter verwirrt.

Mit Analysatoren können Sie eine Beschreibung solcher Strukturen eingeben:

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

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

Wenn Sie die Typen nicht beschreiben, versuchen die Analysatoren, die Struktur des Arrays zu „erraten“, aber wie die Praxis zeigt, sind sie mit unserem Code nicht wirklich erfolgreich. :) :)

Dieser Ansatz hat einen Nachteil. Angenommen, Sie haben eine Struktur, die im Code aktiv verwendet wird. Sie können einen Pseudotyp nicht an einer Stelle deklarieren und dann überall verwenden. Sie müssen PHPDoc mit der Beschreibung des Arrays überall im Code registrieren, was sehr unpraktisch ist, insbesondere wenn das Array viele Felder enthält. Es wird auch problematisch sein, diesen Typ später zu bearbeiten (Felder hinzufügen und entfernen).

Beschreibung der Array-Schlüsseltypen

In PHP können Array-Schlüssel Ganzzahlen und Zeichenfolgen sein. Typen können manchmal für die statische Analyse wichtig sein (und auch für Programmierer). 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 mit PHPDoc einen Hinweis hinzugefügt, dass die Schlüssel im Array $users ganzzahlige Ints und die Werte Objekte der Klasse \User . Wir könnten den Typ als \ User [] beschreiben. Dies würde dem Analysator mitteilen, dass sich Objekte in der Klasse \User im Array befinden, aber nichts über den Schlüsseltyp aussagen.

PHPStorm unterstützt dieses Format zur Beschreibung von Arrays ab Version 2018.3.

Ihr Namespace in PHPDoc

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

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

Aber PHPStorm versteht ihn nicht. Aber wir können so 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 */ function showUrl($parsed_url) { … } 

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

PHP-Funktionsprüfungen


Diese Art von Test lässt sich am besten anhand eines Beispiels veranschaulichen.

Wissen wir alle, was die Funktion explode () zurückgeben kann? Wenn Sie sich die Dokumentation ansehen, scheint sie ein Array zurückzugeben. Wenn Sie es jedoch genauer untersuchen, werden wir feststellen, dass es auch falsch zurückgeben kann. Tatsächlich kann es sowohl null als auch einen Fehler zurückgeben, wenn Sie die falschen Typen übergeben. Die Übergabe des falschen Werts mit dem falschen Datentyp ist jedoch bereits ein Fehler, sodass diese Option für uns derzeit nicht interessant ist.

Wenn aus Sicht des Analysators eine Funktion false oder ein Array zurückgeben kann, sollte der Code höchstwahrscheinlich auf false prüfen. Die Funktion explode () gibt jedoch nur dann false zurück, wenn das Trennzeichen (erster Parameter) einer leeren Zeichenfolge entspricht. Oft wird es explizit in den Code geschrieben, und Analysatoren können überprüfen, ob es nicht leer ist. Dies bedeutet, dass an dieser Stelle die Funktion explode () ein Array genau zurückgibt und keine falsche Prüfung erforderlich ist.

PHP hat so einige Funktionen. Analysatoren fügen nach und nach geeignete Überprüfungen hinzu oder verbessern sie, und wir Programmierer müssen uns nicht mehr an all diese Funktionen erinnern.

Wir wenden uns der Beschreibung spezifischer Analysatoren zu.

PHPStan


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

Um PHPStan verwenden zu können, benötigen Sie:

  1. Installieren Sie es (der einfachste Weg, dies zu tun, ist über Composer).
  2. (optional) Konfigurieren.
  3. Führen Sie im einfachsten Fall einfach Folgendes aus:

vendor/bin/phpstan analyse ./src

(Anstelle von src möglicherweise eine Liste bestimmter Dateien, die Sie überprüfen möchten.)

PHPStan liest den PHP-Code aus den übertragenen Dateien. Wenn er auf unbekannte Klassen stößt, wird er versuchen, diese mit Autoload und durch Reflexion zu laden, um ihre Schnittstelle zu verstehen. Sie können auch den Pfad zur Bootstrap Datei übertragen, über die Sie das automatische Laden konfigurieren, und einige zusätzliche Dateien anhängen, um die PHPStan-Analyse zu vereinfachen.

Hauptmerkmale:

  1. Es ist möglich, nicht die gesamte Codebasis zu analysieren, sondern nur einen Teil - unbekannte Klassen PHPStan versucht, das Autoload zu laden.
  2. Wenn sich einige Ihrer Klassen aus irgendeinem Grund nicht im Autoload befinden, kann PHPStan sie nicht finden und gibt einen Fehler aus.
  3. Wenn Sie über __call / __get / __set aktiv magische Methoden __call / __get / __set , können Sie ein Plugin für PHPStan schreiben. Es gibt bereits Plugins für Symfony, Doctrine, Laravel, Mockery usw.
  4. Tatsächlich führt PHPStan das automatische Laden nicht nur für unbekannte Klassen durch, sondern im Allgemeinen für alle. Wir haben viel alten Code geschrieben, bevor anonyme Klassen angezeigt werden, wenn wir eine Klasse in einer Datei erstellen, sie dann sofort instanziieren und möglicherweise sogar einige Methoden aufrufen. Das automatische Laden (Einschließen) solcher Dateien führt zu Fehlern, da der Code in einer normalen Umgebung nicht ausgeführt wird.
  5. Konfigurationen im Neonformat (ich habe nie gehört, dass ein solches Format woanders verwendet wurde).
  6. Es gibt keine Unterstützung für ihre PHPDoc-Tags wie @phpstan-var, @phpstan-return usw.

Ein weiteres Merkmal ist, dass Fehler Text enthalten, aber keinen Typ haben. Das heißt, der 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 handelt es sich bei beiden Fehlern im Wesentlichen um dasselbe: Die Methode muss einen Typ zurückgeben, in Wirklichkeit jedoch den anderen. Die Texte der Fehler sind jedoch unterschiedlich, wenn auch ähnlich. Wenn Sie Fehler in PHPStan herausfiltern möchten, tun Sie dies daher nur durch reguläre Ausdrücke.

Zum Vergleich haben Fehler in anderen Analysatoren einen Typ. In Phan ist ein solcher Fehler beispielsweise vom Typ PhanPossiblyNullTypeReturn , und Sie können in der Konfiguration angeben, dass Sie nicht nach solchen Fehlern PhanPossiblyNullTypeReturn müssen. Mit der Art des Fehlers ist es beispielsweise auch möglich, leicht Statistiken über Fehler zu sammeln.

Da wir keine Laravel-, Symfony-, Doctrine- und ähnliche Lösungen verwenden und in unserem Code selten magische Methoden verwenden, stellte sich heraus, dass das Hauptmerkmal von PHPStan für uns nicht beansprucht wurde. ; (Darüber hinaus funktioniert die Analyse aufgrund der Tatsache, dass PHPStan alle zu prüfenden Klassen enthält, manchmal einfach nicht auf unserer Codebasis.

PHPStan bleibt uns jedoch nützlich:

  • Wenn Sie mehrere Dateien überprüfen müssen, ist PHPStan deutlich schneller als Phan und etwas (20-50%) schneller als Psalm.
  • PHPStan-Berichte erleichtern das Auffinden von false-positive in anderen Analysegeräten. Wenn der Code explizit fatal ist, wird er normalerweise von allen Analysatoren (oder mindestens zwei der drei) angezeigt.


Update:
Der Autor von PHPStan Ondřej Mirtes las auch unseren Artikel und sagte uns, dass PhpStan wie Psalm eine Website mit einer „Sandbox“ hat: https://phpstan.org/ . Dies ist sehr praktisch für Fehlerberichte: Sie reproduzieren den Fehler in und geben einen Link in GitHub an.

Phan


Entwickelt von Etsy. Erste Commits von Rasmus Lerdorf.

Von den drei fraglichen ist Phan der einzige echte statische Analysator (in dem Sinne, dass er keine Ihrer Dateien ausführt - er analysiert Ihre gesamte Codebasis und analysiert dann, was Sie sagen). Selbst für die Analyse mehrerer Dateien in unserer Codebasis werden etwa 6 GB RAM benötigt, und dieser Vorgang dauert vier bis fünf Minuten. Dann dauert eine vollständige Analyse der gesamten Codebasis etwa sechs bis sieben Minuten. Zum Vergleich analysiert Psalm es in wenigen zehn Minuten. Und mit PHPStan konnten wir überhaupt keine vollständige Analyse der gesamten Codebasis erzielen, da diese Include-Klassen enthält.

Die Phan-Erfahrung ist zweifach. Einerseits ist es der qualitativ hochwertigste und stabilste Analysator, es findet viel und es gibt weniger Probleme damit, wenn es notwendig ist, die gesamte Codebasis zu analysieren. Auf der anderen Seite hat es zwei unangenehme Eigenschaften.

Unter der Haube benutzt Phan die PHP-Ast-Erweiterung. Dies ist anscheinend einer der Gründe dafür, dass die Analyse der gesamten Codebasis relativ schnell ist. Aber php-ast zeigt 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 */ function doSomething($type) {   /** @var \My\Object $obj **/   $obj = MyFactory::createObjectByType($type);   … } 

Dann gibt es innerhalb des AST-Baums Informationen über das externe PHPDoc für die Funktion doSomething() , aber es gibt keine PHPDoc-Hilfeinformationen innerhalb der Funktion. Und dementsprechend weiß Phan auch nichts über sie. Dies ist die häufigste Ursache für false-positive Ergebnisse in Phan. Es gibt einige Empfehlungen zum Einfügen von QuickInfos (über Zeichenfolgen oder Assert-s), die sich jedoch leider stark von denen unserer Programmierer unterscheiden. Teilweise haben wir dieses Problem gelöst, indem wir ein Plugin für Phan geschrieben haben. Die Plugins werden jedoch weiter unten erläutert.

Das zweite unangenehme Merkmal ist, dass Phan die Eigenschaften von Objekten nicht gut analysiert. Hier ist ein Beispiel:

 class A { /** * @var string|null */ private $a; public function __construct(string $a = null) {      $this->a = $a; } public function doSomething() {      if ($this->a && strpos($this->a, 'a') === 0) {          var_dump("test1");      } } } 

In diesem Beispiel sagt Ihnen Phan, 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. Zusätzlich zu diesen beiden Arten von false-positive er fast keine Fehler oder Fehler, sondern auf einem wirklich komplexen Code. Uns hat auch gefallen, dass sich die Konfiguration in einer PHP-Datei befindet - dies gibt etwas Flexibilität. Phan weiß auch, wie man als Sprachserver arbeitet, aber wir haben diese Funktion nicht verwendet, da PHPStorm für uns ausreicht.

Plugins


Phan hat eine gut entwickelte Plugin-Entwicklungs-API. Sie können Ihre eigenen Prüfungen hinzufügen und die Typinferenz für Ihren Code verbessern. Diese API enthält Dokumentation , aber es ist besonders cool, dass bereits funktionierende Plugins vorhanden sind, die als Beispiele verwendet werden können.

Wir haben es geschafft, zwei Plugins zu schreiben. Die erste war für eine einmalige Überprüfung vorgesehen. Wir wollten bewerten, wie bereit unser Code für PHP 7.3 ist (insbesondere um herauszufinden, ob er Konstanten ohne case-insensitive ). Wir waren uns fast sicher, dass es keine solchen Konstanten gab, aber in 12 Jahren konnte alles passieren - es sollte überprüft werden. Und wir haben ein Plugin für Phan geschrieben, das schwören würde, wenn der dritte Parameter in define() .

Das Plugin ist sehr einfach
 <?php declare(strict_types=1); use Phan\AST\ContextNode; use Phan\CodeBase; use Phan\Language\Context; use Phan\Language\Element\Func; use Phan\PluginV2; use Phan\PluginV2\AnalyzeFunctionCallCapability; use ast\Node; class DefineThirdParamTrue extends PluginV2 implements AnalyzeFunctionCallCapability { public function getAnalyzeFunctionCallClosures(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,    ]; } } return new DefineThirdParamTrue(); 



In Phan können verschiedene Plugins an verschiedene Ereignisse gehängt werden. Insbesondere Plugins mit der AnalyzeFunctionCallCapability Schnittstelle AnalyzeFunctionCallCapability ausgelöst, wenn ein Funktionsaufruf analysiert wird. In diesem Plugin haben wir es so gemacht, dass beim Aufrufen der Funktion define() unsere anonyme Funktion aufgerufen wird, die überprüft, ob define() nicht mehr als zwei Argumente enthält. Dann haben wir gerade Phan gestartet, alle Stellen gefunden, an denen define() mit drei Argumenten aufgerufen wurde, und sichergestellt, dass wir keine case-insensitive- haben, case-insensitive- die case-insensitive- nicht case-insensitive- .

Mit dem Plugin haben wir auch das false-positive Problem teilweise gelöst, wenn Phan keine PHPDoc-Hinweise im Code sieht.

Wir verwenden häufig Factory-Methoden, die eine Konstante als Eingabe verwenden und daraus ein Objekt erstellen. Oft sieht der Code ungefähr 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 Objektklasse aus dem Namen der Konstante abgerufen werden, die an die create() -Methode übergeben wird. Mit Phan können Sie ein Plugin schreiben, das ausgelöst wird, wenn der Rückgabewert einer Funktion analysiert wird. Mit diesem Plugin können Sie dem Analysator mitteilen, welchen Typ die Funktion in diesem Aufruf zurückgibt.

Ein Beispiel für dieses Plugin ist komplexer. Es gibt jedoch ein gutes Beispiel für den Phan-Code in der vendor/phan/phan/src/Phan/Plugin/Internal/DependentReturnTypeOverridePlugin.php.

Insgesamt sind wir mit dem Phan-Analysegerät sehr zufrieden. Die oben aufgeführten false-positive Ergebnisse haben wir teilweise (in einfachen Fällen mit einfachem Code) zum Filtern gelernt. Danach wurde Phan ein fast Referenzanalysator. Die Notwendigkeit, die gesamte Codebasis (Zeit und viel Speicher) sofort zu analysieren, erschwert jedoch immer noch den Implementierungsprozess.

Psalm


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

Dieser Analysator ist der jüngste unserer drei. Als ich die Nachricht las, dass Vimeo Psalm veröffentlicht hat, war ich ratlos: „Warum in Psalm investieren, wenn Sie bereits Phan und PHPStan haben?“ Es stellte sich jedoch heraus, dass der Psalm seine eigenen nützlichen Eigenschaften hat.

Psalm trat in die Fußstapfen von PHPStan: Sie können ihm auch eine Liste von Dateien zur Analyse geben, die analysiert werden und Klassen, die nicht gefunden wurden, mit einem automatischen Laden verbinden. Gleichzeitig werden nur Klassen verbunden, die nicht gefunden wurden, und die Dateien, die wir zur Analyse angefordert haben, werden nicht berücksichtigt (dies unterscheidet sich von PHPStan). Die Konfiguration wird in einer XML-Datei gespeichert (für uns ist dies eher ein Minus, aber nicht sehr kritisch).

Psalm hat eine Sandbox- Site, auf 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 den Link in GitHub an. Übrigens beschreibt die Site alle möglichen Arten von Fehlern. Zum Vergleich: In PHPStan gibt es keine Typen, und in Phan gibt es keine, aber es konnte keine einzige Liste gefunden werden.

Uns hat auch gefallen, dass Psalm bei der Ausgabe 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 die benutzerdefinierten PHPDoc-Tags, mit denen Sie die Analyse (insbesondere die Definition von Typen) verbessern können. Wir listen die interessantesten von ihnen auf.

@ psalm-ignore-nullable-return


Es kommt vor, dass eine Methode formal null , aber der Code ist bereits so organisiert, dass dies niemals geschieht. In diesem Fall ist es sehr praktisch, dass Sie der Methode / Funktion einen solchen PHPDoc-Hinweis hinzufügen können - und Psalm wird berücksichtigen, dass null nicht zurückgegeben wird.

Ein ähnlicher Hinweis existiert für false: @psalm-ignore-falsable-return .

Arten zum Schließen


Wenn Sie sich jemals für funktionale Programmierung interessiert haben, haben Sie vielleicht bemerkt, dass eine Funktion häufig eine andere Funktion zurückgeben oder eine Funktion als Parameter übernehmen kann. In PHP kann dieser Stil für Ihre Kollegen sehr verwirrend sein, und einer der Gründe ist, dass PHP keine Standards für die Dokumentation solcher Funktionen hat. Zum Beispiel:

 function my_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 */ function my_filter(array $ar, \Closure $func) { … } 

Mit einer solchen Beschreibung ist bereits klar, dass Sie eine anonyme Funktion an my_filter , die ein int akzeptiert und bool my_filter . Und natürlich wird Psalm überprüfen, ob Sie genau eine solche Funktion in Ihrem Code übergeben haben.

Aufzählungen


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

 function isYes(string $yes_or_no) : bool {     $yes_or_no = strtolower($yes_or_no)     switch($yes_or_no)  {           case 'yes':                 return true;          case 'no':                 return false;           default:                throw new \InvalidArgumentException(…);     } } 

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

 /** @psalm-param 'Yes'|'No' $yes_or_no **/ function isYes(string $yes_or_no) : bool { … } 

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

Lesen Sie hier mehr über enum.

Geben Sie Aliase ein


In der Beschreibung der array shapes ich bereits erwähnt, dass Analysatoren zwar die Beschreibung der Struktur von Arrays ermöglichen, die Verwendung jedoch nicht sehr praktisch ist, da die Beschreibung des Arrays an verschiedenen Stellen kopiert werden muss. Die richtige Lösung besteht natürlich darin, Klassen anstelle von Arrays zu verwenden. Bei langjährigem Erbe ist dies jedoch nicht immer möglich.

, , , :

  • ;
  • closure;
  • union- (, );
  • enum.

, , PHPDoc , , . Psalm . alias PHPDoc alias . , : PHP-. . , Psalm.

Generics aka templates


. , :

 function identity($x) { return $x; } 

? ? ?

, , , — mixed , .

mixed — . , . , identity() / , : , . -. , :

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

(int) , , $y ( int ).

? Psalm PHPDoc-:

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

templates Psalm , / .

Psalm templates:

vendor/vimeo/psalm/src/Psalm/Stubs/CoreGenericFunctions.php ;
vendor/vimeo/psalm/src/Psalm/Stubs/CoreGenericClasses.php .

Phan, : https://github.com/phan/phan/wiki/Generic-Types .

, Psalm . , «» . , Psalm , , Phan PHPStan. .

PHPStorm


: , . , , .

. Phan, language server. PHPStorm, , .

, , PHPStorm ( ), . — Php Inspections (EA Extended). — , , . , . , scopes - scopes.

, deep-assoc-completion . .

Badoo


?

, .

, . , , git diff / , , () . , .

, : - git diff . . , . . , , , , .

, , :



false-positive . , , Phan , , . , - Phan , , .

QA


:



— , , , . :

  • 100% ( , );
  • , code review;
  • , .

strict types . , strict types , :

  • , strict types , ;
  • , (, , );
  • , PHP (, union types , PHP);
  • strict types , .

:


, . .

-, , , - , .

-, , — , , PHPDoc. — .

-, . , - , PHPDoc. :) :)

, , . , .

Source: https://habr.com/ru/post/de426605/


All Articles