Titelbild zum Artikel

So erstelle ich einen eigenen Coding Standard für den PHP CodeSniffer

Avatar von Jürgen Lesny

An sich ist es dem Computer egal, wie hübsch und strukturiert der Code formatiert ist, er wird ihn so oder so ausführen, wenn die Syntax und Semantik passt. Der Mensch tut sich jedoch erheblich leichter, wenn der Quelltext einem einheitlichen Programmierstil folgt, der das Wichtige vom Unwichtigen trennt und schnell Aufschluss darüber gibt, was wo wie passiert.

Eine Frage des guten Stils

So haben sich für diverse Programmiersprachen und Frameworks verschiedenere Coding Standards etabliert, die sich zum Teil widersprechen. Daher ist es unabdingbar, dass im Rahmen eines Projekts alle Entwickler nach dem gleichen Programmierstil entwickeln, damit jeder sich gleichermaßen im Quelltext zurechtfindet. Ferner muss die IDE entsprechend konfiguriert werden, damit eine Datei nicht nach jeder Bearbeitung komplett neu formatiert wird. In diesem Beitrag möchte ich beschreiben wie wir einen eigenen Coding Standard für den PHP CodeSniffer erstellt haben.

PHP CodeSniffer

Der PHP CodeSniffer, kurz PHPCS, ist ein flexibler PHP-Tokenizer, der prüft, ob der in PHP geschriebene Code einem bestimmten Coding Standard genügt. Wenn nicht, meldet er, in welcher Zeile welche Regelverletzung gefunden wurde. Somit ist er ein sehr wertvolles Werkzeug für die statische Code Analyse.

Typischerweise wird der CodeSniffer in die IDE, sowie Continous Intergration eingebunden. Von Haus aus unterstützt er eine Reihe von Coding Standards, z.B. Zend Framework, PEAR, PSR2. Auf GitHub finden sich weitere Regelsätze, z.B. für Symfony.

Der Befehl

% phpcs -i

gibt Auskunft darüber, welche Standards verwendet werden können.

Mittels

% phpcs --standard=PEAR .

werden rekursiv alle Dateien im aktuellen Verzeichnis auf die Konformität zum PEAR-Standard geprüft.

Nach eigenen Regeln

Gelegentlich gehen die Anforderungen eines Teams über bestehende Coding Standards hinaus. Ohne angepassten Code Sniffer zum Beispiel, läuft man Gefahr, dass die eigenen Regeln nicht wirklich beherzigt werden, beziehungsweise, dass viele manuelle Eingriffe nötig sind, um den Standard einzuhalten.

Wie schon Agent Smith in der Matrix zurecht sagte:

Nimm nie einen Menschen, wenn du eine Maschine dafür nehmen kannst.

 

Im Rahmen des Kundenprojekts MO4 wurde die bestehende Implementierung eines Symfony Regelsatzes für PHPCS  vervollständigt, sowie ein eigener Standard entwickelt.

Der erste Schritt zum einem eigenen Regelsatz ist einfach. Man klont das PHP CodeSniffer Projekt von GitHub und wechselt in das  Verzeichnis „PHP_CodeSniffer/CodeSniffer/Standards„:

% git clone https://github.com/squizlabs/PHP_CodeSniffer.git
% cd PHP_CodeSniffer/CodeSniffer/Standards

Dort findet man die mitgelieferten Implementierungen zu verschiedenen Coding Standards. Weitere Standards müssen hier abgelegt werden, damit PHPCS sie findet.

Jeder Standard benötigt eine Datei namens „ruleset.xml„. Diese sollte mindestens den Namen des Standards beinhalten, Groß- und Kleinschreibung ist durchaus relevant. Darüber hinaus können Regeln – im Jargon Sniff genannt – aus anderen Standards importiert werden.

<?xml version="1.0"?>
<ruleset name="MO4">
    <description>The MO4 Symfony PSR-2 coding standard.</description>

    <!-- Include the whole Symfony PSR-2 standard -->
    <rule ref="Symfony">
        <!-- exclude sniffs that are extended by this standard -->
        <exclude name="PEAR.Functions.FunctionCallSignature"/>
        <exclude name="PSR2.Namespaces.UseDeclaration"/>
        <exclude name="Generic.Formatting.MultipleStatementAlignment"/>
    </rule>

    <rule ref="Generic.Formatting.SpaceAfterCast"/>
</ruleset>

Neben der Datei ruleset.xml findet man bei den Coding Standards für PHPCS folgende Verzeichnisstruktur:

  • Sniffs beherbergt die Implementierung von Sniffs in PHP. Unterhalb dieses Ordners werden die Sniffs üblicherweise thematisch in weitere Unterverzeichnisse gruppiert.
  • in Tests finden man die Unit-Tests zu den Sniffs in anloger Struktur,
  • und Docs ist für die Dokumentation der Sniffs vorgesehen.

Im Standard „Generic“ findet man einen Grundstock an Regeln. Einige der dort angebotenen Sniffs heben sich sogar gegensätzlich auf, wie z.B. NoSpaceAfterCastSniff.php und SpaceAfterCastSniff.php. Daher ist von einem kompletten Import des Rulesets abzuraten.

Gibt es für die eigene Regel noch keinen passenden Sniff, kann jener implementiert werden. Das Framework von PHPCS unterstützt dabei das Test Driven Development. Unterhalb von Sniffs und Tests legt man die gleiche Verzeichnisstruktur an. Die Klasse für den Sniff benennt man mit dem Suffix „Sniff„, die Klasse für den Test mit dem Suffix „UnitTest„. Also z.B. für „UseArrayShortTag“ jeweils „UseArrayShortTagSniff“  und „UseArrayShortTagUnitTest„. Das Einbehalten der Namenskonvention ist absolut notwendig, da sonst die Tests bzw. die Sniffs nicht gefunden werden!

Die Verzeichnisstruktur spiegelt sich im Klassennamen wieder, wie im folgenden Beispiel für „MO4/Sniffs/Formatting/UseArrayShortTagSniff.php“ zu entnehmen ist.

/**
 * Use the array short tag [...] instead of array(...)
 */
class MO4_Sniffs_Formatting_UseArrayShortTagSniff implements
    PHP_CodeSniffer_Sniff
{
    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * @return array(int)
     */
    public function register()
    {
        return array(T_ARRAY);
    }

    /**
     * Called when one of the token types is found.
     *
     * @param PHP_CodeSniffer_File $phpcsFile The PHP_CodeSniffer file
     * @param int                  $stackPtr  token position in file
     *
     * @return void
     */
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
    {
        $phpcsFile->addError('Array short tag [ ... ] must be used', $stackPtr);
    }
}

Jeder Sniff implementiert das Interface PHP_CodeSniffer_Sniff, das die Implementierung von register() und process() vorschreibt. Die Funktion register() liefert die Liste der PHP-Tokens zurück, für die der Sniff aktiv ist. PHPCS verwendet den PHP eigenen Tokenizer, hat jedoch weitere Tokens definiert und gruppiert. Eine vollständige Liste findet man in der Datei „Tokens.php“ im PHPCS-Projekt.

Die Methode process() wird jedes Mal vom PHPCS aufgerufen wenn er auf das entsprechende Token beim Parsen gestoßen ist. Der erste Parameter ist die aktuelle Datei, dargestellt als PHP_CodeSniffer_File-Objekt, der zweite ist der Index in der Token-Liste.

Das obige Beispiel ist der denkbar einfachste Sniff den man sich vorstellen kann, da er die klassische Array Notation „array(...)“ verbietet. Man sollte stattdessen die mit PHP 5.4 eingeführte „[...]“ Notation verwenden.

Das nun folgende Beispiel zeigt eine Regel, welche besagt, dass bei Instantiierungen von Klassen, Klammern nach dem Klassennamen nicht fehlen dürfen.

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

// must instantiate a Class like "new Bla()" instead of "new Bla"
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
    $tokens = $phpcsFile->getTokens();

    $allowedTokens   = PHP_CodeSniffer_Tokens::$emptyTokens;
    $allowedTokens[] = T_STRING;
    $allowedTokens[] = T_NS_SEPARATOR;

    $next =  $phpcsFile->findNext($allowedTokens, $stackPtr + 1, null, true);

    if (($next === false) || ($tokens[$next]['code'] !== T_OPEN_PARENTHESIS)) {
        $phpcsFile->addError('parenthesis missing after class name', $stackPtr);
    }
}

Bei jedem new-Statement wird process() aufgerufen. Über die Methode getTokens() bekommen wir von der $phpcsFile die Liste aller Tokens unserer Datei als Array, $stackPointer zeigt auf das Element im Array. Jedes Element des Token-Arrays besteht aus den Feldern ‚type‘, ‚code‘, ‚content‘, ‚line‘, ‚column‘ sowie noch weiteren Feldern.

Mit der Funktion findNext() können wir bequem durch die Liste der Tokens navigieren. In aufgeführten Beispiel navigiert der Aufruf der Funktion zum nächsten Token, das weder Kommentar, Whitespace, Namespace-Trenner noch Identifier ist. Falls das gefundene Symbol keine Klammer ist, kommt ein Fehler. Damit ist z.B. „new Bla;“ unzulässig.

Neben findNext() gibt es auch findPrevious(), das in entgegengesetzte Richtung nach Tokens sucht.

Die Testklasse UseArrayShortTagUnitTest implementiert die Methoden getErrorList() und getWarningList(). Zu jedem Test werden Input-Files eingelesen, die im Verzeichnis des Tests liegen und die Endung .inc haben. Der Methoden geben jeweils die Anzahl der Fehler pro Zeile zurück, die der Sniff gefunden hat.

class MO4_Tests_Formatting_UseArrayShortTagUnitTest extends
    AbstractSniffUnitTest
{
    protected function getErrorList($testFile = '')
    {
        switch ($testFile) {
        case 'UseArrayShortTagUnitTest.pass.inc':
            return array();
        case 'UseArrayShortTagUnitTest.fail.inc':
            return array(
                3  => 1,
                4  => 1,
                8  => 1,
                10 => 1,
            );
        }

        return null;
    }

    protected function getWarningList()
    {
        return array();
    }
}

Die Testsuite wird im PHP_CodeSniffer Verzeichnis wie folgt aufgerufen:

% phpunit tests/AllTests.php

Den vollständigen Symfony Regelsatz, sowie den MO4 Conding Standard findet man unter:

Fazit

Wenn ein Projekt einem eigenen Coding Standard folgt, sollte man im Rahmen der Automatisierung ein dediziertes Regelset für PHP Code Sniffer erstellen. Dabei ist es nicht unbedingt erforderlich, das Rad neu zu erfinden. Das modulare Konzept ermöglicht bestehende Standards in Teilen oder Gänze zu importieren.

Eigene Sniffs können im Rahmen von Test Driven Development zielgerichtet implementiert werden, der eigene Projektcode dient dabei als Testbasis.

Es empfiehlt sich, zeitig den eigenen Standard auf den Entwickler-Rechner und der Continuous Integration zu installieren. Fängt man spät damit an, droht man von einer Fülle von Regelverletzungen erschlagen zu werden.

Ausblick

Eine weitere Erleichterung für die Arbeit von Entwicklern wäre die automatische Korrektur des Quelltextes anhand der eindeutigen Regelverletzungen, die der Code Sniffer meldet.

Hierzu gibt es diverse Entwicklungen, zum Beispiel das experimentelle Tool PHP-CS-Fixer. Dieses ist in der Lage, den Code gemäß PSR-1 oder PSR-2 zumindest teilweise zu fixen.

Im experimentellen Entwicklungszweig phpcs-fixer von PHP CodeSniffer wird ebenfalls an einer automatischen Korrektur der gemeldeten Fehler gearbeitet.

Avatar von Jürgen Lesny

Kommentare

2 Antworten zu „So erstelle ich einen eigenen Coding Standard für den PHP CodeSniffer“

  1. Eigener Coding Standard für PHP CodeSniffer erstellen http://t.co/TxXlTGBxHB via @mayflowerphp

  2. Lesenswert: So erstelle ich einen eigenen Coding Standard für den PHP CodeSniffer http://t.co/OFvXE6FOIV

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.