Im statischen Analysator NoVerify wurde eine Killer-Funktion veröffentlicht: eine deklarative Methode zur Beschreibung von Inspektionen, für die keine Go-Programmierung und Code-Kompilierung erforderlich sind.
Um Sie zu faszinieren, zeige ich Ihnen eine Beschreibung einer einfachen, aber nützlichen Inspektion:
$x && $x;
Diese Überprüfung findet alle logischen &&
Ausdrücke, bei denen der linke und der rechte Operand identisch sind.
NoVerify ist ein statischer Analysator für PHP, der in Go geschrieben wurde. Sie können darüber im Artikel „ NoVerify: Linter für PHP vom VKontakte-Team “ lesen . Und in diesem Test werde ich über die neue Funktionalität sprechen und wie wir dazu gekommen sind.

Hintergrund
Wenn Sie selbst für eine einfache neue Prüfung ein paar Dutzend Codezeilen auf Go schreiben müssen, fragen Sie sich: Ist das anders möglich?
Unterwegs haben wir die Typinferenz, die gesamte Pipeline des Linter, den Metadaten-Cache und viele andere wichtige Elemente geschrieben, ohne die NoVerify nicht möglich ist. Diese Komponenten sind eindeutig, Aufgaben wie "Verbieten des Aufrufs einer Funktion X mit einer Reihe von Argumenten Y" jedoch nicht. Gerade für solch einfache Aufgaben wurde der Mechanismus dynamischer Regeln hinzugefügt.
Mit dynamischen Regeln können Sie komplexe Interna von der Lösung typischer Probleme trennen. Die Definitionsdatei kann separat gespeichert und versioniert werden. Sie kann von Personen bearbeitet werden, die nicht mit der Entwicklung von NoVerify selbst zusammenhängen. Jede Regel implementiert eine Codeüberprüfung (die wir manchmal als Überprüfung bezeichnen).
Ja, wenn wir eine Sprache zur Beschreibung dieser Regeln haben, können Sie jederzeit eine semantisch falsche Vorlage schreiben oder einige Typeinschränkungen ignorieren - und dies führt zu falsch positiven Ergebnissen. Trotzdem wird das nil
oder die Dereferenzierung des Nullzeigers durch die Sprache der Regeln nicht eingegeben.
Vorlage Beschreibung Sprache
Die Beschreibungssprache ist syntaktisch mit PHP kompatibel. Dies vereinfacht das Studium und ermöglicht das Bearbeiten von Regeldateien mit demselben PhpStorm.
Ganz am Anfang der Regeldatei wird empfohlen, eine Direktive einzufügen, die Ihre Lieblings-IDE beruhigt:
<?php
Mein erstes Experiment mit Syntax und möglichen Filtern für Vorlagen war phpgrep . Es mag für sich genommen nützlich sein, aber in NoVerify ist es noch interessanter geworden, weil es jetzt Zugriff auf Typinformationen hat.
Einige meiner Kollegen haben bereits phpgrep in ihrer Arbeit ausprobiert, und dies war ein weiteres Argument für die Wahl einer solchen Syntax .
Phpgrep selbst ist eine Gogrep-Anpassung für PHP (Sie könnten auch an cgrep interessiert sein ). Mit diesem Programm können Sie über Syntaxvorlagen nach Code suchen.
Eine Alternative wäre die strukturelle SSR-Syntax ( Search and Replace) von PhpStorm. Die Vorteile liegen auf der Hand - dies ist ein vorhandenes Format, aber ich habe diese Funktion nach der Implementierung von phpgrep kennengelernt. Sie können natürlich eine technische Erklärung geben: Es gibt eine Syntax, die nicht mit PHP kompatibel ist, und unser Parser wird sie nicht beherrschen, aber dieser überzeugende "echte" Grund wurde nach dem Schreiben des Fahrrads entdeckt.
In der Tat gab es eine andere Option
Es kann erforderlich sein, eine Vorlage mit PHP-Code fast eins zu eins anzuzeigen - oder in die andere Richtung: eine neue Sprache zu erfinden, beispielsweise mit der Syntax von S-Ausdrücken .
PHP-like Lisp-like ----------------------------- $x = $y | (expr = $x $y) fn($x, 1) | (expr call fn $x 1) : (or (expr == (type string (expr)) (expr)) (expr == (expr) (type string (expr))))
Am Ende dachte ich, dass die Lesbarkeit der Vorlagen immer noch wichtig ist und wir Filter über die phpdoc-Attribute hinzufügen können.
clang-query ist ein Beispiel für eine ähnliche Idee, verwendet jedoch eine traditionellere Syntax.
Wir erstellen und führen unsere eigene Diagnose durch!
Versuchen wir, unsere neue Diagnose für den Analysator zu implementieren.
Dazu müssen Sie NoVerify installiert haben. Nehmen Sie die Binärversion, wenn Sie keine Go-Toolchain im System haben (wenn Sie eine haben, können Sie alles aus der Quelle kompilieren).
Erklärung des Problems
PHP hat viele interessante Funktionen, eine davon ist parse_str . Ihre Unterschrift:
Sie werden verstehen, was hier falsch ist, wenn Sie sich dieses Beispiel aus der Dokumentation ansehen:
$str = "first=value&arr[]=foo+bar&arr[]=baz"; parse_str($str); echo $first;
Mmm, die Parameter aus der Zeichenfolge lagen im aktuellen Bereich. Um dies zu vermeiden, müssen wir in unserem neuen Test den zweiten Parameter der Funktion $result
, damit das Ergebnis in dieses Array geschrieben wird.
Erstellen Sie Ihre eigene Diagnose
Erstellen Sie die Datei myrules.php
:
<?php parse_str($_);
Die Regeldatei ist im Allgemeinen eine Liste von Ausdrücken auf der obersten Ebene, von denen jeder als phpgrep-Vorlage interpretiert wird. Für jede solche Vorlage wird ein spezieller PHPDOC- Kommentar erwartet. Es ist nur ein Attribut erforderlich - eine Fehlerkategorie mit einem Warnungstext.
Insgesamt gibt es jetzt vier Ebenen: error
, warning
, info
und maybe
. Die ersten beiden sind kritisch: Der Linter gibt nach der Ausführung einen Code ungleich Null zurück, wenn mindestens eine der kritischen Regeln funktioniert. Nach dem Attribut selbst gibt es einen Warnungstext, der vom Linter ausgegeben wird, falls die Vorlage ausgelöst wird.
Die Vorlage, die wir geschrieben haben, verwendet $_
- dies ist eine unbenannte Vorlagenvariable. Wir könnten es zum Beispiel $x
, aber da wir mit dieser Variablen nichts machen, können wir ihr einen "leeren" Namen geben. Der Unterschied zwischen Vorlagenvariablen und PHP-Variablen besteht darin, dass erstere mit absolut jedem Ausdruck übereinstimmen und nicht nur mit einer „wörtlichen“ Variablen. Dies ist praktisch: Wir müssen häufig nach unbekannten Ausdrücken suchen und nicht nach bestimmten Variablen.
Neue Diagnose starten
Erstellen Sie eine kleine Testdatei zum Debuggen, test.php
:
<?php function f($x) { parse_str($x);
Führen Sie als Nächstes NoVerify mit unseren Regeln für diese Datei aus:
$ noverify -rules myrules.php test.php
Unsere Warnung sieht ungefähr so aus:
WARNING myrules.php:4: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
Der Name der Standardprüfung ist der Name der Regeldatei und die Zeile, die diese Prüfung definiert. In unserem Fall ist dies myrules.php:4
.
Sie können Ihren Namen mit dem Attribut @name <name>
.
@ Name Beispiel
parse_str($_);
WARNING parseStrResult: parse_str without second argument at test.php:4 parse_str($x); ^^^^^^^^^^^^^
Benannte Regeln unterliegen den Gesetzen anderer Diagnosen:
- Kann über
-exclude-checks
deaktiviert werden -critical
kann über -critical
neu definiert werden
Arbeiten Sie mit Typen
Das vorige Beispiel ist gut für die Hallo Welt - aber oft müssen wir die Arten von Ausdrücken kennen, um die Anzahl der Diagnoseoperationen zu reduzieren
Für die Funktion in_array fragen wir beispielsweise nach dem Argument $strict=true
wenn das erste Argument ( $needle
) vom Typ String ist.
Dafür haben wir Ergebnisfilter.
Ein solcher Filter ist @type <type> <var>
. Sie können alles verwerfen, was nicht zu den aufgezählten Typen passt.
in_array($needle, $_);
Hier haben wir dem in_array
Aufruf den Namen des ersten Arguments in_array
, um einen in_array
zu binden. Eine Warnung wird nur ausgegeben, wenn der Typ der $needle
string
.
@or
können mit dem Operator @or
kombiniert werden:
$x == $y;
Im obigen Beispiel stimmt das Muster nur mit den ==
Ausdrücken überein, bei denen einer der Operanden vom Typ string
. Es kann davon ausgegangen werden, dass ohne @or
alle Filter durch @and
kombiniert werden, dies muss jedoch nicht explizit angegeben werden.
Begrenzen Sie den Umfang der Diagnose
Für jeden Test können Sie @scope <name>
angeben:
@scope all
- der Standardwert, Validierung funktioniert überall;@scope root
- nur auf der obersten Ebene starten;@scope local
- Nur innerhalb von Funktionen und Methoden ausführen.
Angenommen, wir möchten eine return
außerhalb des Funktionskörpers melden. In PHP ist dies manchmal sinnvoll - zum Beispiel, wenn eine Datei über eine Funktion verbunden ist ... Aber in diesem Artikel verurteilen wir dies.
return $_;
Mal sehen, wie sich diese Regel verhält:
<?php function f() { return "OK"; } return "NOT OK";
Ebenso können Sie eine Anforderung zur Verwendung von *_once
anstelle von require
and include
:
require $_; include $_;
Beim Abgleichen von Mustern werden Klammern nicht ganz konsistent berücksichtigt. Das Muster (($x))
findet nicht "alle Ausdrücke in doppelten Klammern", sondern einfach alle Ausdrücke, wobei die Klammern ignoriert werden. $x+$y*$z
und ($x+$y)*$z
verhalten sich jedoch so, wie sie sollten. Diese Funktion ist auf die Schwierigkeiten bei der Arbeit mit Token (
und )
. Es besteht jedoch die Möglichkeit, dass die Reihenfolge in einer der nächsten Versionen wiederhergestellt wird.
Vorlagen gruppieren
Wenn in Vorlagen doppelte PHPDOC-Kommentare angezeigt werden, ist die Möglichkeit, Vorlagen zu kombinieren, hilfreich.
Ein einfaches Beispiel zur Demonstration:
Stellen Sie sich nun vor, wie unangenehm es wäre, eine Regel im folgenden Beispiel ohne diese Funktion zu beschreiben!
{ $x > $y; $x < $y; $x >= $y; $x <= $y; $x == $y; }
Das im Artikel angegebene Aufnahmeformat ist nur eine der vorgeschlagenen Optionen. Wenn Sie an der Auswahl teilnehmen möchten, haben Sie eine solche Gelegenheit: Sie müssen +1 auf die Angebote setzen, die Sie mehr mögen als andere. Für weitere Details klicken Sie hier .
Wie dynamische Regeln integriert werden

Zum Zeitpunkt des Starts versucht NoVerify, die im Regelargument angegebene Regeldatei zu finden.
Als Nächstes wird diese Datei als reguläres PHP-Skript analysiert, und aus dem resultierenden AST wird eine Reihe von Regelobjekten mit daran gebundenen phpgrep-Vorlagen gesammelt.
Dann beginnt der Analysator die Arbeit nach dem üblichen Schema - der einzige Unterschied besteht darin, dass für einige überprüfte Codeabschnitte ein Satz gebundener Regeln gestartet wird. Wenn die Regel ausgelöst wird, wird eine Warnung angezeigt.
Erfolg wird als Übereinstimmung der phpgrep-Vorlage und der Passage mindestens eines der @or
(sie werden durch @or
getrennt).
Zu diesem Zeitpunkt verlangsamt der Regelmechanismus den Betrieb des Linters nicht wesentlich, selbst wenn viele dynamische Regeln vorhanden sind.
Matching-Algorithmus
Bei dem naiven Ansatz müssen wir für jeden AST-Knoten alle dynamischen Regeln anwenden. Dies ist eine sehr ineffiziente Implementierung, da der größte Teil der Arbeit vergeblich erledigt wird: Viele Vorlagen haben ein bestimmtes Präfix, mit dem wir die Regeln gruppieren können.
Dies ähnelt der Idee des parallelen Abgleichs , aber anstatt die NFA ehrlich aufzubauen, „parallelisieren“ wir nur den ersten Schritt der Berechnungen.
Betrachten Sie dies anhand eines Beispiels mit drei Regeln:
$_ ? $x : $x; explode("", ${"*"}); if ($_);
Wenn wir N Elemente und M Regeln haben, müssen wir mit einem naiven Ansatz N * M Operationen ausführen. Theoretisch kann diese Komplexität auf linear reduziert werden und O(N)
- wenn Sie alle Vorlagen zu einer kombinieren und die Übereinstimmung wie beispielsweise das Regexp- Paket von Go ausführen .
In der Praxis habe ich mich bisher jedoch auf die teilweise Umsetzung dieses Ansatzes konzentriert. Dadurch können die Regeln aus der obigen Datei in drei Kategorien unterteilt und den AST-Elementen, denen keine Regel entspricht, eine vierte leere Kategorie zugewiesen werden. Aus diesem Grund wird nicht mehr als eine Regel für jedes Element ausgeführt.
Wenn wir Tausende von Regeln haben und eine signifikante Verlangsamung spüren, wird der Algorithmus fertiggestellt. In der Zwischenzeit passen mir die Einfachheit der Lösung und die daraus resultierende Beschleunigung.
Die aktuelle Syntax dupliziert @var
und @var
, aber wir benötigen möglicherweise neue Operatoren, z. B. "Typ ist nicht gleich". Stellen Sie sich vor, wie es aussehen könnte.
Wir haben mindestens zwei wichtige Prioritäten:
- Die lesbare und übersichtliche Syntax von Anmerkungen.
- Höchste Unterstützung durch die IDE ohne zusätzlichen Aufwand.
Es gibt ein PHP-Annotations- Plugin für PhpStorm, das die automatische Vervollständigung, den Übergang zu Annotationsklassen und andere nützliche Funktionen für die Arbeit mit PHPDOC-Kommentaren hinzufügt.
Priorität (2) in der Praxis bedeutet, dass Sie Entscheidungen treffen, die nicht den Erwartungen der IDE und der Plugins widersprechen. Sie können beispielsweise Anmerkungen in einem Format erstellen, das das Plugin für PHP-Anmerkungen erkennen kann:
class Filter { public $value; public $type; public $text; }
Dann würde das Anwenden eines Filters auf Typen ungefähr so aussehen:
@Type($needle, eq=string) @Type($x, not_eq=Foo)
Benutzer können zur Definition von Filter
und werden mit einer Liste möglicher Parameter (Typ / Text / usw.) aufgefordert.
Alternative Aufnahmemethoden, von denen einige von Kollegen vorgeschlagen wurden:
@type string $needle @type !Foo $x @type $needle == string @type $x != Foo @type(==) string $needle @type(!=) Foo $x @type($needle) == string @type($x) != Foo @filter type($needle) == string @filter type($x) != Foo
Dann wurden wir ein wenig abgelenkt und vergaßen, dass alles in phpdoc war, und dies erschien:
(eq string (typeof $needle)) (neq Foo (typeof $x))
Obwohl die Option mit Postfix-Aufnahme zum Spaß auch geklungen hat. Eine Sprache zur Beschreibung von Typ- und Werteinschränkungen könnte als sechste bezeichnet werden:
@eval string $needle typeof = @eval Foo $x typeof <>
Die Suche nach der besten Option ist noch nicht abgeschlossen ...
Erweiterbarkeitsvergleich mit Phan
Als einer der Vorteile von Phan weist der Artikel " Statische Analyse von PHP-Code am Beispiel von PHPStan, Phan und Psalm " auf Erweiterbarkeit hin.
Folgendes wurde im Beispiel-Plugin implementiert:
Wir wollten bewerten, wie bereit unser Code für PHP 7.3 ist (insbesondere um herauszufinden, ob er Konstanten ohne Berücksichtigung der Groß- und Kleinschreibung enthält). 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 () verwendet würde.
So sieht der Plugin-Code aus (die Formatierung ist für die Breite optimiert):
<?php 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) { $def = function(CodeBase $cb, Context $ctx, Func $fn, $args) { if (count($args) < 3) { return; } $this->emitIssue( $cb, $ctx, 'PhanDefineCaseInsensitiv', 'define with 3 arguments', [] ); }; return ['define' => $def]; } } return new DefineThirdParamTrue();
Und so könnte dies in NoVerify gemacht werden:
<?php define($_, $_, $_);
Wir wollten ungefähr das gleiche Ergebnis erzielen - damit triviale Dinge so einfach wie möglich erledigt werden können.
Fazit
Links, nützliche Materialien
Hier werden wichtige Links gesammelt, von denen einige möglicherweise bereits im Artikel erwähnt wurden, aber aus Gründen der Klarheit und Bequemlichkeit habe ich sie an einem Ort gesammelt.
Wenn Sie weitere Beispiele für Regeln benötigen, die implementiert werden können, können Sie einen Blick auf NoVerify-Tests werfen .