Facebook
Twitter
Google+
Kommentare
8

Die PHP_CodeSniffer Odyssee

Heute möchte ich euch eine kleine Geschichte erzählen. Die Geschichte vom Nils, der versucht hat einen Sniff zu schreiben, weil es ja gar nicht so schwer sein kann. Ihr ahnt schon, dass das nicht so einfach ist, wie man es meinen könnte.

Was der PHP_CodeSniffer für ein Tool ist, solltet ihr nach meiner Sniffer Woche ja alle wissen. Ich wollte mal wieder einen Sniff schreiben. Einen Sniff, der nach Variablen sucht, die nie initialisiert und trotzdem verwendet wurden. Koingt anfänglich ein wenig kompliziert. Aber wenn man mal einen Algorithmus gefunden hat, dann sollte es ganz einfach sein.

Meine Grundidee war:

  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.
// gültig
$test = 12;
$zahl = $test;

//ungültig
$zahl2 = $test2;

Der kleine naive Nils ist natürlich ein Freund des Testens. Regel in einen Sniff gegossen und auf ein Projekt losgelassen. Pustekuchen. Großes Projekt und gleich hunderte von False-Positives (also Stellen, an denen meiner Sniffer anschlägt, die aber korrekt sind). Gut so einfach war es dann doch nicht. Ich hatte Funktionen und Parameter vergessen.Die Grundidee musste also eingepasst werden:

  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Meinen kleinen Codinstandard wieder laufen lassen und statt hunderten Fehlern waren es jetzt nur noch underte Fehler. Die Anzahl ging zwar runter, aber gelöst war das Problem immer noch nicht. Was hatte ich denn jetzt schon wieder vergessen? Klar. Globale Variablen. Also die Grundidee anpassen:

  • Wenn eine Variable über global $var in den Scope geladen wurde, gilt sie als definiert.
  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Super, jetzt habe ich sie bestimmt bald alle. Kann ja nicht sein, dass mich PHP so oft reinlegen kann. Statische Codeanalyse soll ja Spass machen. Also wieder laufen lassen und … natürlich immer noch Fehler, die eigentlich keine sind. Ihr denkt jetzt natürlich, dass ich einfach mal alle Fehlermeldungen anschauen sollte, aber vertraut mir, es waren echt viele. Wieder zurück. Ich hatte die Superglobalen vergessen. Ihr ahnt es schon Grundidee anpassen:

  • Wenn eine Variable den Namen $_POST, $_GET, $_SERVER, $_SESSION, … hat, dann gilt sie als definiert
  • Wenn eine Variable über global $var in den Scope geladen wurde, gilt sie als definiert.
  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Tja mittlerweile wurde aus meinem kleinen Sniff schon ein Monsterding, denn jede Regel schlug sich in einer Menge Zeilen Code nieder. Aber zum Glück war ich ja bald am Ende. Aber leider am Ende meiner Nerven. Denn natürlich hatte ich wieder was vergessen. Ich hatte zwar Funktionen fast komplett abgedeckt, aber Methoden noch nicht. Was ist denn wenn ich Attribute verwende? Wieder anpassen:

  • Wenn die Variable mit $this oder self anfängt, dann prüfe, ob sie im Klassenkopf defniert wurde
  • Wenn eine Variable den Namen $_POST, $_GET, $_SERVER, $_SESSION, … hat, dann gilt sie als definiert
  • Wenn eine Variable über global $var in den Scope geladen wurde, gilt sie als definiert.
  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Von einer Regel auf fünf, nicht schlecht, für einen Vormittag. Denkste. Natürlich kommt immer noch eine Regel hinzu. Was ist denn mit Exceptions? Exceptions werden durch eine andere Syntax gefangen. Also wieder zurück ans Reißbrett:

  • Wenn eine Variable neben einem Klassennamen steht und „davor“ ein catch kommt, gilt sie als definiert.
  • Wenn die Variable mit $this oder self anfängt, dann prüfe, ob sie im Klassenkopf defniert wurde
  • Wenn eine Variable den Namen $_POST, $_GET, $_SERVER, $_SESSION, … hat, dann gilt sie als definiert
  • Wenn eine Variable über global $var in den Scope geladen wurde, gilt sie als definiert.
  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Jetzt lief mein „kleiner“ Sniff fast bis zum Ende ohne Fehler durch. Dummerweise denkt man an den schwersten Fall immer erst sehr spät. Pass by Reference. Ich übergebe eine „leere“ Variable eine Methode und die füllt mir die dann mit Leben. preg_match wäre ein solcher Kandidat. Die Variable die reingeht, muss nicht vorher definiert worden sein, denn das passiert mehr oder weniger in der Methode selbst. PHP hat zum Glück nicht viele Funktionen, die so reagieren, diese gilt es also in eine Liste zu packen, um wenigstens diese zu erkennen. Im Userland sieht das ganz anders aus. Jeder kann solche Methoden programmieren. Es ist aber fast unmöglich über statische Codeanalyse rauszufinden, ob eine Methode Pass By Reference verwendet. Hier gilt es also eine neue Vorbedingung zu schaffen. Eine Variable, die per Pass By Reference übergeben wird, muss vorher mit dem NULL Wert initiiert worden sein. Dann greift nämlich auch die erste Regel und wir sind glücklich. Wir haben also einen neun Sniff zu schreiben, der erstmal prüft, ob alle PBR Variablen  vorher geNULLt wurden. Danach können wir unsere Regeln aktualisieren:

  • Wenn die Variable als Referenz an eine native Pass-By-Reference Funktion übergeben wird, gilt sie als definiert.
  • Wenn eine Variable neben einem Klassennamen steht und „davor“ ein catch kommt, gilt sie als definiert.
  • Wenn die Variable mit $this oder self anfängt, dann prüfe, ob sie im Klassenkopf definiert wurde
  • Wenn eine Variable den Namen $_POST, $_GET, $_SERVER, $_SESSION, … hat, dann gilt sie als definiert
  • Wenn eine Variable über global $var in den Scope geladen wurde, gilt sie als definiert.
  • Falls eine Variable ein Funktionsparameter ist, gilt sie als definiert.
  • Falls eine Variable rechts vom Gleichheitszeichen steht, muss sie mindestens einmal im gleichen Scope auch links von einem Gleichheitszeichen gestanden haben.

Das wars. Ich war total am Ende, aber der Sniff scheint zu funktionieren (wahrscheinlich fallen euch gleich nich ein paar Punkte auf, die ich vergessen habe). Was ich aber verdeutlichen wollte ist, nichts so einfach ist, wie es aussieht. Angefangen, als eine kleine Methode, hatte ich am Ende ein Monster an Klasse. Nach so einer Odysse bleibt man das nächste mal doch ein wenig länger am Design und prüft nochmal alles durch, bevor man einfach drauf loslegt.

Wer übrigens Interesse an dem Sniff hat, den muss ich noch eine Weile vertrösten, denn ich bin gerade ein wenig am Testen und Säubern. Danach stelle ich ihn gerne zur Verfügung. Lasst euch aber nicht den Mut nehmen selbst mal einen Sniff zu schreiben. Es gibt auch viele einfache Sniffs, die es noch zu schreiben gilt.

Mist jetzt wo ich fertig bin, fällt mir ein, ich habe Klassenkonstanten vergessen. Tja soviel dazu!

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

8 Comments

  1. Selbst „einfache“ Sniffs bedeuten im Endeffekt schon recht viel Logikcode, das ist mir auch schon mehrmals aufgefallen. Eventuell wäre es einfacher gewesen wenn du für diesen Task die Bytekit Extension von Stefan Esser verwendet hättest.

    Reply
  2. Eine schöne evolutionäre Entwicklung. Allerdings funktioniert der Sniff nur solange wie der Programmierer „statisch“ programmiert, denn so was wie $$var kann ein Sniffer im Allgemeinen nicht überprüfen. Woraus sich wieder die Fragestellung ergibt: Sollte PHP nicht statisch sein? 🙂

    Reply
  3. Mir ging es in dem Fall ja gar nicht wirklich um den Sniff, der dabei raus kam. Eigentlich wollte ich mich in den Code Sniffer einarbeiten und habe mir einfach das nächste „einfache“ Beispiel gegriffen. Und ich muss sagen, dass ich jetzt doch ganz gut die Funktionsweise verstehe und auch andere Sniffs basteln könnte – was ich gerade beruflich auch mache.

    @Andre: Wie man einen $$-Verbots-Sniff schreibt, kann ich ja mal beschreiben 🙂

    Reply
  4. Die Reflection-API kann prüfen ob ein Wert per-Reference übergeben wird 😉 ReflectionParameter::isPassedByReference()

    Reply
  5. Kann ich nur zustimmen.

    Meist wird aus den Problemen die einfach erscheinen, das größte Ungetüm. Und teilweise umgekehrt.

    Daher – erst Überlegen / Planen / Dokumentieren. Dann Umsetzen.
    Eventualitäten ergeben sich ohnehin noch genug.

    Reply
  6. Globale Variablen sind sowieso böse, sie sorgen regelmäßig für starke Kopplung in unvorhergesehener Art und Weise.

    Also immer einen Sniff für global bereithalten. 😉

    Um gleich mal die Diskussion anzuheizen: Ja, einen Sniff für die Verwendung von $_POST, $_SESSION, etc. innerhalb von Funktionen oder Methoden halte ich für sinnvoll, gut, dass du mich drauf bringst. Diese Variablen sollten beim Lauf via Dependency Injection in den Code injiziert werden.

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen