So fügen Sie NoVerify Schecks hinzu, ohne eine einzige Zeile Go-Code zu schreiben

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:


/** @warning duplicated sub-expressions inside boolean expression */ $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-Teamlesen . 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 /** *      , *        PHP-. * * @noinspection ALL */ // ...  —   . 

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).


Wenn Sie NoVerify nicht installieren, können Sie weiterlesen, aber so tun, als würden Sie die aufgeführten Schritte reproduzieren und das Ergebnis bewundern!

Erklärung des Problems


PHP hat viele interessante Funktionen, eine davon ist parse_str . Ihre Unterschrift:


 //   encoded_string,     //   URL,      //   (  ,    result). parse_str ( string $encoded_string [, array &$result ] ) : void 

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; // value echo $arr[0]; // foo bar echo $arr[1]; // baz 

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 /** @warning parse_str without second argument */ 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


 /** * @name parseStrResult * @warning parse_str without second argument */ 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.


 /** * @warning 3rd arg of in_array must be true when comparing strings * @type string $needle */ 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:


 /** *     -. * * @warning strings must be compared using '===' operator * @type string $x * @or * @type string $y */ $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.


 /** * @warning don't use return outside of functions * @scope root */ return $_; 

Mal sehen, wie sich diese Regel verhält:


 <?php function f() { return "OK"; } return "NOT OK"; // Gives a warning class C { public function m() { return "ALSO OK"; } } 

Ebenso können Sie eine Anforderung zur Verwendung von *_once anstelle von require and include :


 /** * @maybe prefer require_once over require * @scope root */ require $_; /** * @maybe prefer include_once over include * @scope root */ 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:


WarEs wurde (mit Gruppierung)
 / ** @kann nicht exit verwenden oder sterben * /
 sterben ($ _);

 / ** @kann nicht exit verwenden oder sterben * /
 exit ($ _);
 / ** @kann nicht exit verwenden oder sterben * /
 {
   sterben ($ _);
   exit ($ _);
 }

Stellen Sie sich nun vor, wie unangenehm es wäre, eine Regel im folgenden Beispiel ohne diese Funktion zu beschreiben!


 /** * @warning don't compare arrays with numeric types * @type array $x * @type int|float $y * @or * @type int|float $x * @type array $y */ { $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:


 /** @warning duplicated then/else parts of ternary */ $_ ? $x : $x; /** @warning don't call explode with delim="" */ explode("", ${"*"}); /** @maybe suspicious empty body of the if statement */ 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 Qual der Wahl oder ein wenig über das @type Formular


Aufgabe: Auswahl einer guten Syntax für Filter in phpdoc-Annotationen.

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:


  1. Die lesbare und übersichtliche Syntax von Anmerkungen.
  2. 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:


 /** * Type is a filter that checks that $value * satisfies the given type constraints. * * @Annotation */ class Filter { /** Variable name that is being filtered */ public $value; /** Check that value type is equal to $type */ public $type; /** Check that value text is equal to $text */ 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 /** @warning define with 3 arguments */ 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 .

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


All Articles