Facebook
Twitter
Google+
Kommentare
4

PHP Code Sniffer – Eigene Regeln erstellen

Wie angekündigt schreiben wir heute eine Regel, die nichts anderes macht, als einen Klassennamen zu prüfen. Passt er in einen vorher definierten regulären Ausdruck, so ist alles wunderbar, falls nicht soll er ERROR angezeigt werden. Eigentlich wollte ich ja eine Regel schreiben, die prüft, ob eine Variable einen Wert zugewiesen bekommen hat, bevor sie verwendet wurde, habe ich sogar gemacht, das Problem ist nur, dass die Regel auf einmal 207 Zeilen lange war und noch nicht mal alle Fälle abdeckt. Trotzdem war ich stolz auf mich und werde wohl in den nächsten Tag noch mal mit prahlen. Heute wird es also um etwas einfacheres gehen.

Aber eigentlich ist es egal, ob wir eine kompliziertes oder ein leichtes Vorhaben angehen, das Herangehensmodell ist immer gleich.

  • Definieren, was die Regeln prüfen soll
  • Testfälle spezifizieren
  • Testfälle in Code gießen
  • Regel programmieren
  • Glücklich sein (oder auch nicht)

Sieht fast aus wie Test Driven Development, ist es eigentlich auch. In unseren Fall ist dies ganz einfach. Wir haben genau 2 Testfälle, eine Klasse, mit gültigen Namen und eine zweite mit „falschem“. Fangen wir also an mit den zu prüfenden Klassen und gehen wir davon aus, dass ein Klassenname immer mit „phm“ anfangen muss. Ich finde eh, dass viel mehr Klassen phphatesme gewidmet sein sollten. Aber wieder zurück zum Thema. Hier die Klassen

class phmClass1
{

}

class zendClass1
{

}

Verteilen wir das ganze am besten auf zwei Dateien, damit wir unserem Credo – pro Datei nur eine Klasse – auch hier nicht entsagen. Jetzt kommt der große nächste Schritt, wir brauchen einen Sniff. Dieser soll heißen und zum PhpHatesMe Standard gehören. Zusätzlich definieren wir noch eine Kategorie, hier NamingConventions, in dem wir die Regel ablegen. Legen wir also die folgende Datei an:

/usr/share/php/PHP/CodeSniffer/Standards/PhpHatesMe/Sniffs/NamingConventions/CheckClassName.php

Hier muss unser Sniff rein. Das folgende Grundgerüst nehmen wir mal als gottgegeben hin, obwohl … ich erklärs doch schnell mal. Eigentlich muss nur auf den Klassennamen eingegangen werden, da der rest eine einfach Implementation gegen das vorgegebene Interface ist. Wer schon mal ein wenig mit dem Zend Framework rumgespielt hat, der kennt diese Art Klassennamen. Wir verpacken einfach den Pfad der Datei mit ihm Namen, damit wir die Klasse dann bei Bedarf fganz einfach per __autoload nachladen können. Aber genügend abgeschweift, hier jetzt endlich das Grundgerüst:

class PhpHatesMe_Sniffs_NamingConventions_CheckClassNameSniff implements PHP_CodeSniffer_Sniff
{
    public $supportedTokenizers = array( );

    public function register()
    {
      return array( );
    }

   public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
   {
      return true;
   }
}

Mit diesem Klassenkonstrukt können wir jetzt schon lostesten. Es wir zwar keine Fehler finden, aber der CodeSniffer wird es jetzt schon als gültige Regel akzeptieren. Um das ganze zu testen erstellen wir uns unseren eigenen Standard und fügen diese Regeln hinzu, rufen sie mit diesem Parameter auf und fertig sind wir:

phpcs --standard=PhpHatesMe goodClassName.php

Und zack haben wir eine leere Ausgabe, die uns sagt, dass wir keine Regelverstöße haben. Ok war zu erwarten, da wir ja gar keine Regeln haben. Fangen wir jetzt aber an unsere Regeln zu spezifizieren. Dies machen wir in drei Schritten:

  • Supported Tokenizers: Mit diesem öffentlichen Attribut definieren wir, auf welche Tokens der Sniffer reagieren soll. Da PHPCS in der Lage ist sowohl PHP- als auch JavaScript-Code zu parsen wollen wir hier festhalten, dass unsere Regel nur über PHP Code laufen soll, was Rechenzeit spart.  In Code gegossen würde das wie folgt aussehen:
    public $supportedTokenizers = array( 'PHP' );

    Wenn wir wollten könnten wir dem Array noch JS hinzufügen, aber das wollen wir ja in unserem Fall nicht.
  • register( ): Jetzt können wir uns die Tokens aussuchen, aber die wir reagieren wollen, was in unserem Fall ganz einfach ist T_CLASS, mehr nicht. Das T_CLASS Token entspricht zwar „nur“ dem Schlüsselwort Class, von dort aus können wir uns dann aber zum Namen der Klasse ganz einfach vorarbeiten.
  • process( ): Der dritte Teil, der programmiert werden will, ist auch der komplexeste, denn hier definieren wir die eigentliche Regel. Aus diesem Grund widmen wir uns jetzt auch ein wenig ausführlicher diesem Prozess.

Ich bin ja in den letzten Artikeln schon drauf eingegangen wie process funktioniert, was wir als erstes tun müssen ist rauszufinden, wie die Klasse eigentlich heißt, um dann den Namen zu prüfen. Gehen wir erst mal ganz blauäugig an die Sache ran. Wir wissen, wenn der Sniffer anschlägt, dann befinden wir uns gerade im T_CLASS Token. Eigentlich müsste doch dann das nächste Token ein T_WHITESPACE sein, gefolgt von einem T_STRING, der den Namen beinhaltet. Wenn dies immer so wäre, dann würde un folgende Funktion ausreichen:

   public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
   {
      $tokens = $phpcsFile->getTokens( );  // alle Tokens holen
      $name = $tokens[$stackPtr + 2]['content']; // vom aktuellen, also dem T_CLASS Token, zwei nach "rechts" gehen
      if( substr( $name, 0, 3 ) != 'phm' ) { // Prüfen ob der Name des Tokens mit "phm" beginnt
        $phpcsFile->addError('Invalid class name', $currentPtr);
      }
   }

Die Welt ist aber schlecht und aus dem Grund klappt es nicht immer so blauäugig. In den meisten Fällen stimmt es natürlich, aber hier kommt das fiese bei den ganzen Regeln, die man verfasst: Es gibt immer eine Ausnahme. Was passiert denn, wenn wir zwei Whitespaces zwischen Class und Klassennamen haben? Dann versagt unsere Regel. In PHP kann ich ja auch ohne Probleme zwischen Class und dem Namen einen Kommentar unterbringen. Würde auch zum versagen der Regeln führen.

Wir müssen also unsere Algorithmus ein wenig anpassen. Statt dem einfachen Ansatz einfach zwei nach rechts zu wandern, wollen wir solange nach rechts rutschen, bis wir einen T_STRING erreicht haben. Was dann wie folgt aussieht:

   public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
   {
      $tokens = $phpcsFile->getTokens( );
      $currentPtr = $stackPtr;
      while ( isset( $tokens[$currentPtr] ) ) {
        if ( $tokens[$currentPtr]['code'] == T_STRING ) {
          $name = $tokens[$currentPtr]['content'];
          if( substr( $name, 0, 3 ) != 'phm' ) {
                $phpcsFile->addError('Invalid class name', $currentPtr);
          }
        }
        $currentPtr++;
      }
   }

Ihr fragt euch jetzt sicher, warum ich erst eine „falsche“ Lösung angegeben habe. Ganz einfach: Das Problem beim Regeln schreiben ist immer das gleiche, anfänglich vergisst man eine Regel. Als ich meine Regel für nicht initialisierte Variablen geschrieben habe, waren es um die sieben. Ihr seit da also nicht alleine und wenn ich gute Testfälle habt, dann sollte es auch ein „Kinderspiel“ sein.

Wenn ihr den fertigen Sniff jetzt einsetzt, dann sollte er in allen Fällen, zumindest in allen, die mir eingefallen sind, funktionieren. Spielt einfach mal ein wenig rum.

Das war auch schon die erste eigene Regel. Bestimmt könnte man das ganze noch ein wenig eleganter machen, ich denke aber, dass es für ein erstes Gefühl für den Code Sniffer reicht. Wenn Interesse besteht, dann gehe ich in einem späteren Artikel mal auf eine komplexere Regel (nicht initialisierte Variablen) ein.

Am Montag wird es weiter gehen mit einem Post zum Thema „PHP Code Sniffer in Ant und PHP Under Control“ integrieren.

Ü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

4 Comments

  1. wäre es möglich, zu so einer artikelserie in den jeweiligen artiekeln in den ersten zeilen eine art inhaltsverzeichnis mit links zu den anderen serienartikeln zu erstellen? dann kann man da einfacher hin und her navigieren.

    Reply
  2. Hey, erstmal danke für die super Einleitung in das Thema!
    Zu deiner „CheckClassName-Regel“: kann es sein, dass die Regel nach wie vor „falsch“ ist. Du gehst darin zwar vor bis ein String Token auftritt, überprüfst aber dann doch
    $name = $tokens[$stackPtr + 2][‚content‘];
    und nicht wie erwartet
    $name = $tokens[$currentPtr][‚content‘];

    Reply
  3. @Uli: Ja das ist natürlich möglich. Ich wollte am WE auch noch eine schöne Übersichtsseite für die Serien basteln, die ein wenig vom Standard Blog Layout abweicht. Vielleicht wird sie ähnlich der Interview Übersicht. Aber wer weiß das schon.

    @Rudi: Du hast natürlich Recht. Hab’s auch gleich geändert. Vielen Dank.

    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