Mein Lieblingsteil bei der statischen Code-Analyse besteht darin, Hypothesen über mögliche Fehler im Code aufzustellen und diese dann zu überprüfen.
Beispiel einer Hypothese:
strpos .
Es besteht jedoch die Möglichkeit, dass eine solche Diagnose selbst bei einigen Millionen Codezeilen nicht "schießt", sodass Sie nicht viel Zeit mit erfolglosen Hypothesen verbringen möchten.
Heute werde ich zeigen, wie man mit dem Dienstprogramm phpgrep die einfachste statische Analyse durchführt, ohne Code zu schreiben.
Hintergrund
Seit einigen Monaten unterstütze ich den NoVerify PHP-Linter (lesen Sie dazu den NoVerify- Artikel : Linter für PHP vom VKontakte-Team ).
Von Zeit zu Zeit erscheinen im Team Ideen für neue Diagnosen. Es kann viele Ideen geben, aber ich möchte alles überprüfen, insbesondere wenn die vorgeschlagene Überprüfung darauf abzielt, kritische Fehler zu identifizieren.
Zuvor habe ich Go-Kritiker aktiv entwickelt und die Situation war ähnlich, mit dem einzigen Unterschied, dass die Quellcodes in Go und nicht in PHP analysiert wurden. Als ich von dem Gogrep- Dienstprogramm erfuhr , stellte sich meine Welt auf den Kopf. Wie der Name schon sagt, hat dieses Dienstprogramm etwas mit grep gemeinsam. Nur die Suche wird nicht durch reguläre Ausdrücke, sondern durch Syntaxmuster durchgeführt (ich werde später erklären, was dies bedeutet).
Ich wollte nicht ohne kluges phpgrep
leben, also beschloss ich eines Abends, mich phpgrep
und phpgrep
schreiben.
Analysierter Fall
Um Spaß zu haben, tauchen wir sofort in die Anwendung ein. Wir werden eine kleine Reihe von ziemlich bekannten und großen PHP-Projekten analysieren, die auf GitHub verfügbar sind.
Unser Kit enthielt folgende Projekte:
Für Leute, die planen, was wir planen, ist dies ein sehr appetitliches Set.
Also lass uns gehen!
Zuweisung als Ausdruck verwenden
Wenn die Zuweisung als Ausdruck verwendet wird, gilt Folgendes:
- Der Kontext erwartet das Ergebnis einer logischen Operation (logische Bedingung) und
- Die rechte Seite des Ausdrucks hat keine Nebenwirkungen und ist konstant.
Es handelt sich höchstwahrscheinlich um einen Fehler im Code.
Nehmen wir zunächst die folgenden Konstruktionen für den "logischen Kontext":
- Ausdruck innerhalb von "
if ($cond)
". - Die Bedingung des ternären Operators lautet: "
$cond ? $x : $y
". - Fortsetzungsbedingungen für Schleifen "
while ($cond)
" und " for ($init; $cond; $post)
".
Auf der rechten Seite der Zuordnung erwarten wir Konstanten oder Literale.
Warum brauchen wir solche Einschränkungen? Beginnen wir mit (1):
Hier sehen wir 4 Muster, deren einziger Unterschied der zugewiesene Ausdruck (RHS) ist. Beginnen wir mit dem ersten.
Die Vorlage " if ($_ = []) $_
" erfasst ein if
, dem jedem Ausdruck ein leeres Array zugewiesen ist. $_
entspricht einem Ausdruck oder einer Anweisung.
(RHS) | if ($_ = []) $_ | | | if', , {} LHS
In den folgenden Beispielen werden komplexere const- , str- und num- Gruppen verwendet. Im Gegensatz zu $_
beschreiben sie Einschränkungen für kompatible Vorgänge.
const
ist eine benannte Konstante oder Klassenkonstante.str
ist ein String-Literal eines beliebigen Typs.num
ist ein numerisches Literal eines beliebigen Typs.
Diese Muster reichen aus, um mehrere Operationen an dem Fall durchzuführen.
⎆ Moodle / Blocks / rss_client / viewfeed.php # L37 :
if ($courseid = SITEID) { $courseid = 0; }
Der zweite Auslöser in Moodle war die ADOdb- Abhängigkeit. In der Upstream-Bibliothek ist das Problem weiterhin vorhanden.
⎆ ADOdb / drivers / adodb-odbtp.inc.php # L741 :

In diesem Fragment steckt viel, aber für uns ist nur die erste Zeile relevant. Anstatt das Feld databaseType
zu vergleichen, führen wir eine Zuweisung durch und gehen immer in die Bedingung.
Ein weiterer interessanter Ort, an dem wir Aktionen nur für „korrekte“ Datensätze ausführen möchten, diese aber immer ausführen und außerdem jeden Datensatz als korrekt markieren möchten!
⎆ Moodle / Frage / Format / blackboard_six / formatqti.php # L598 :
Erweiterte Liste der Vorlagen für diese Prüfung Wiederholen wir, was wir gelernt haben:
- Die Vorlagen sehen aus wie der gefundene PHP-Code.
$_
steht für alles. Sie können mit vergleichen .
in regulären Ausdrücken.${"<class>"}
funktioniert wie $_
mit einer Einschränkung des AST-Elementtyps.
Hervorzuheben ist auch, dass alles außer Variablen buchstäblich abgebildet wird. Dies bedeutet, dass das Muster " array(1, 2 + 3)
" nur durch den Code erfüllt wird, der in der syntaktischen Struktur identisch ist (Leerzeichen wirken sich nicht aus). Andererseits erfüllt das Muster " array($_, $_)
" jedes Array-Literal mit zwei Elementen.
Einen Ausdruck mit sich selbst vergleichen
Die Notwendigkeit, etwas mit sich selbst zu vergleichen, ist sehr selten. Es kann eine NaN
Prüfung sein, aber mindestens die Hälfte der Zeit ist es ein Kopier- / Einfügefehler.
⎆ Wikia / app / extensions / SemanticDrilldown / includes / SD_FilterValue.php # L103 :
if ( $fv1->month == $fv1->month ) return 0;
Rechts sollte " $fv2->month
" sein.
Um doppelte Teile in einer Vorlage auszudrücken, verwenden wir Variablen mit anderen Namen als " _
". Der Wiederholungsmechanismus in einem Muster ähnelt Backlinks in regulären Ausdrücken.
Das Muster " $x == $x
" ist genau das, was das obige Beispiel findet. Anstelle von " x
" kann ein beliebiger Name verwendet werden. Es ist nur wichtig, dass die Namen identisch sind. Vorlagenvariablen mit unterschiedlichen Namen müssen beim Erfassen nicht denselben Inhalt haben.
Das folgende Beispiel wurde mit " $x <= $x
" gefunden.
⎆ Drupal / core / modules / views / tests / src / Unit / ViewsDataTest.php # L166 :
$prev = $base_tables[$base_tables_keys[$i - 1]]; $current = $base_tables[$base_tables_keys[$i]]; $this->assertTrue( $prev['weight'] <= $current['weight'] && $prev['title'] <= $prev['title'],
Doppelte Unterausdrücke
Nachdem wir die Möglichkeiten wiederholter Unterausdrücke kennen, können wir viele interessante Muster zusammenstellen.
Einer meiner Favoriten ist " $_ ? $x : $x
".
Dies ist ein ternärer Operator mit identischen True / False-Zweigen.
⎆ joomla-cms / library / src / User / UserHelper.php # L522 :
return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
Beide Zweige sind dupliziert, was auf ein potenzielles Problem im Code hinweist. Wenn wir uns den Code ansehen, können wir verstehen, was stattdessen hätte sein sollen. Aus Gründen der Lesbarkeit habe ich einen Teil des Codes ausgeschnitten und den Namen der Variablen $encrypted
auf $enc
.
case 'crypt-blowfish': return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt); case 'md5-base64': return ($show_encrypt) ? '{MD5}' . $enc : $enc; case 'ssha': return ($show_encrypt) ? '{SSHA}' . $enc : $enc; case 'smd5': return ($show_encrypt) ? '{SMD5}' . $enc : $enc; case 'sha256': return ($show_encrypt) ? '{SHA256}' . $enc : '{SHA256}' . $enc; default: return ($show_encrypt) ? '{MD5}' . $enc : $enc;
Ich würde wetten, dass der Code den folgenden Patch benötigt:
- ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted; + ($show_encrypt) ? '{SHA256}' . $encrypted : $encrypted;
Prioritäten für gefährliche Operationen in PHP
Eine gute Vorsichtsmaßnahme in PHP ist die Verwendung von Gruppierungsklammern, wo immer es wichtig ist, die richtige Reihenfolge der Berechnungen zu haben.
In vielen Programmiersprachen hat der Ausdruck " x & mask != 0
" eine intuitive Bedeutung. Wenn mask
ein Bit beschreibt, prüft dieser Code, ob dieses Bit bei x
ungleich Null ist. Leider wird dieser Ausdruck für PHP wie folgt berechnet: " x & (mask != 0)
", was fast immer nicht das ist, was Sie brauchen.
WordPress, Joomla und Moodle verwenden SimplePie .
⎆ SimplePie / library / SimplePie / Locator.php # L254
⎆ SimplePie / library / SimplePie / Locator.php # L384
⎆ SimplePie / library / SimplePie / Locator.php # L412
⎆ SimplePie / library / SimplePie / Sanitize.php # L349
⎆ SimplePie / library / SimplePie.php # L1634
$feed->method & SIMPLEPIE_FILE_SOURCE_REMOTE === 0
SIMPLEPIE_FILE_SOURCE_REMOTE
als 1
definiert, daher entspricht der Ausdruck:
$feed->method & (1 === 0) // => $feed->method & false
Wenn Sie das Thema der unerwarteten Operationsprioritäten fortsetzen, können Sie mehr über den ternären Operator in PHP lesen. Auf habr wurde sogar der Artikel gewidmet: Die Reihenfolge der Ausführung des ternären Operators .
Ist es möglich, solche Orte mit phpgrep
zu finden? Die Antwort lautet ja !
phpgrep . '$_ == $_ ? $_ : $_ ? $_ : $_' phpgrep . '$_ != $_ ? $_ : $_ ? $_ : $_'
Die Vorteile der Validierung regulärer Ausdrücke
⎆ Wikia / app / wartung / wikia / updateCentralInterwiki.inc # L95 :
if ( preg_match( '/(wowwiki.com|wikia.com|falloutvault.com)/', $url ) ) { $local = 1; } else { $local = 0; }
Wie vom Autor des Codes konzipiert, überprüfen wir die URL auf Übereinstimmung mit einer von drei Optionen. Entschuldigung Symbol .
nicht abgeschirmt, was dazu führt, dass wir anstelle von falloutvault.com
falloutvaultxcom
auf jeder Domain erhalten und den Test bestehen können.

Dies ist kein PHP-spezifischer Fehler. In jeder Anwendung, in der die Validierung durch reguläre Ausdrücke durchgeführt wird und ein Metazeichen Teil der zu überprüfenden Zeichenfolge ist, besteht die Gefahr, dass das Escape dort vergessen wird, wo es benötigt wird, und eine Sicherheitsanfälligkeit auftritt.
Sie können solche Orte finden, indem Sie phpgrep
:
phpgrep . 'preg_match(${"pat:str"}, ${"*"})' 'pat~[^\\]\.(com|ru|net|org)\b'
Wir führen das benannte pat
Untermuster ein, das jedes Zeichenfolgenliteral erfasst, und wenden dann einen Filter aus dem regulären Ausdruck darauf an.
Filter können auf jede Vorlagenvariable angewendet werden. Neben regulären Ausdrücken gibt es auch Strukturoperatoren =
und !=
. Eine vollständige Liste finden Sie in der Dokumentation .
${"*"}
erfasst eine beliebige Anzahl von Argumenten, sodass wir uns nicht um die optionalen Parameter der Funktion preg_match
müssen.
Doppelte Schlüssel im Array-Literal
In PHP erhalten Sie keine Warnung, wenn Sie diesen Code ausführen:
<?php var_dump(['a' => 1, 'a' => 2]);
Wir können solche Arrays mit phpgrep
:
[${"*"}, $k => $_, ${"*"}, $k => $_, ${"*"}]
Dieses Muster kann wie folgt entschlüsselt werden: "Ein Array-Literal, in dem sich mindestens zwei identische Schlüssel an einer beliebigen Position befinden." Die Ausdrücke ${"*"}
helfen uns, eine "beliebige Position" zu beschreiben, die 0-N Elemente vor, zwischen und nach den für uns interessanten Schlüsseln zulässt.
⎆ Wikia / app / extensions / wikia / WikiaMiniUpload / WikiaMiniUpload_body.php # L23 :
$script_a = [ 'wmu_back' => wfMessage( 'wmu_back' )->escaped(), 'wmu_back' => wfMessage( 'wmu_back' )->escaped(),
In diesem Fall ist dies kein grober Fehler, aber ich kenne Fälle, in denen das Duplizieren von Schlüsseln in großen Arrays (über 100 Elemente) zumindest ein unerwartetes Verhalten aufwies, bei dem einer der Schlüssel den Wert des anderen überlappte.
Dies schließt unsere kurze Exkursion mit Beispielen ab. Wenn Sie mehr möchten, wird am Ende des Artikels beschrieben, wie Sie alle Ergebnisse erhalten.
Was ist phpgrep?
Die meisten Editoren und IDEs verwenden die Nur-Text-Suche, um nach dem Code zu suchen (wenn es sich nicht um eine Suche nach einem Sonderzeichen wie einer Klasse oder Variablen handelt) - mit anderen Worten, so etwas wie grep.
Sie geben " $x
" ein und finden " $x
". Möglicherweise stehen Ihnen reguläre Ausdrücke zur Verfügung. Anschließend können Sie versuchen, PHP-Code mit regulären Ausdrücken zu analysieren. Manchmal funktioniert es sogar, wenn Sie nach etwas ganz Bestimmtem und Einfachem suchen - zum Beispiel "jede Variable mit einem Suffix". Wenn diese Variable mit einem Suffix jedoch Teil eines anderen zusammengesetzten Ausdrucks sein soll, treten Schwierigkeiten auf.
phpgrep ist ein Tool für die bequeme Suche nach PHP-Code, mit dem Sie nicht mit textorientierten Stammgästen, sondern mit syntaxbewussten Vorlagen suchen können.
Syntaxbewusst bedeutet, dass die Vorlagensprache die Zielsprache widerspiegelt und nicht wie reguläre Ausdrücke mit einzelnen Zeichen arbeitet. Wir machen auch keinen Unterschied, bevor wir den Code formatieren, nur seine Struktur ist wichtig.
Optionaler Inhalt: SchnellstartSchnellstart
Installation
Es gibt vorgefertigte Release-Builds für amd64 für Linux und Windows . Wenn Sie jedoch Go installiert haben, reicht ein Befehl aus, um eine neue Binärdatei für Ihre Plattform abzurufen:
go get -v github.com/quasilyte/phpgrep/cmd/phpgrep
Befindet sich $GOPATH/bin
im System $PATH
, wird der Befehl phpgrep
sofort verfügbar. Um dies zu überprüfen, führen Sie den Befehl mit dem Parameter -help
:
phpgrep -help
Wenn nichts passiert, suchen Sie, wo Go die Binärdatei installiert hat, und fügen Sie sie der Umgebungsvariablen $PATH
.
Eine alte und zuverlässige Methode, um $GOPATH
, auch wenn sie nicht explizit festgelegt ist:
go env GOPATH
Verwenden Sie
Erstellen Sie eine Testdatei hello.php
:
<?php function f(...$xs) {} f(10); f(20); f(30); f($x); f();
Führen Sie phpgrep
darauf aus:
Wir haben alle Aufrufe der Funktion f
mit einem Argument gefunden, einer Zahl, deren Wert ungleich 20 ist.
Wie funktioniert phpgrep?
Zum Parsen von PHP wird die Bibliothek github.com/z7zmey/php-parser verwendet. Es ist gut genug, aber einige der Einschränkungen von phpgrep
aus den Funktionen des verwendeten Parsers. Besonders beim Versuch, normal mit Klammern zu arbeiten, treten viele Schwierigkeiten auf.
Das Prinzip von phpgrep
ist einfach:
- AST wird aus der Eingabevorlage erstellt, Filter werden zerlegt.
- Für jede Eingabedatei wird ein vollständiger AST-Baum erstellt.
- Wir gehen den AST jeder Datei um und versuchen, solche Teilbäume zu finden, die dem Muster entsprechen.
- Für jedes Ergebnis wird eine Liste von Filtern angewendet.
- Alle Ergebnisse, die die Filter bestanden haben, werden auf dem Bildschirm gedruckt.
Am interessantesten ist, wie genau die beiden AST-Knoten auf Gleichheit abgestimmt sind. Manchmal trivial: Eins-zu-eins und Metaknoten können mehr als ein Element erfassen. Beispiele für Metaknoten sind ${"*"}
und ${"str"}
.
Fazit
Es wäre unehrlich, über phpgrep
zu sprechen, ohne die strukturelle Suche und Ersetzung (SSR) von PhpStorm zu erwähnen. Sie lösen ähnliche Probleme, und die SSR hat ihre Vorteile, zum Beispiel die Integration in die IDE, und phpgrep
rühmt sich, dass es sich um ein eigenständiges Programm handelt, das beispielsweise auf CI viel einfacher zu phpgrep
ist.
phpgrep
ist unter anderem auch eine Bibliothek, die Sie in Ihren Programmen zum Abgleichen von PHP-Code verwenden können. Dies ist besonders nützlich für die Linter- und Codegenerierung.
Ich würde mich freuen, wenn dieses Tool für Sie nützlich ist. Wenn dieser Artikel Sie nur motiviert, in Richtung der oben genannten SSR zu schauen, auch gut.

Zusätzliche Materialien
Die vollständige Liste der Muster, die für die Analyse verwendet wurden, finden Sie in der Datei patterns.txt . Neben dieser Datei befindet sich das Skript phpgrep-lint.sh
, das den Start von phpgrep
mit einer Liste von Vorlagen vereinfacht.
Der Artikel enthält keine vollständige Liste der Antworten. Sie können das Experiment jedoch reproduzieren, indem Sie alle genannten Repositorys phpgrep-lint.sh
und darauf phpgrep-lint.sh
.
Sie können sich von Testvorlagen inspirieren lassen, z. B. von PVS-Studioartikeln . Ich mochte logische Ausdrücke: Fehler von Profis , die sich in so etwas verwandeln:
# "x != y || x != z": phpgrep . '$x != $a || $x != $b' phpgrep . '$x !== $a || $x != $b' phpgrep . '$x != $a || $x !== $b' phpgrep . '$x !== $a || $x !== $b'
Sie könnten auch an der Präsentation von phpgrep: syntax-fähiger Codesuche interessiert sein.
Der Artikel verwendet Bilder von Gophern, die durch Gopherkon erstellt wurden .