L’analyse statique dans le monde PHP

Analyse statique, parseur, AST, etc. Autant de notions que nous ne manipulons que rarement, si ce n’est jamais, dans notre quotidien de développeur Web. Ces notions sont pourtant au coeur des outils que nous utilisons au quotidien : IDE, scripts, intégration continue mais aussi PHP lui-même. Envie de faire une rapide promenade dans le monde de l’analyse statique en PHP et d’apprendre comment vous pouvez analyser vous aussi du code ? Suivez le guide.

Analyse statique, kesako ?

Durant mes premières années de programmation, je suis tombé plusieurs fois sur le terme d’analyse statique. J’en comprenais vaguement l’idée mais sans vraiment pouvoir l’expliquer clairement. Pour mettre tout le monde à égalité, nous allons donc tout d’abord voir en quoi cela consiste.

Wikipédia définit l’analyse statique comme “une variété de méthodes utilisées pour obtenir des informations sur le comportement d’un programme lors de son exécution sans réellement l’exécuter”. On analyse le code statiquement, sans l’exécuter. Plutôt simple à comprendre finalement.

Cet ensemble de méthodes regroupe donc un nombre important d’outils (logiciels, bibliothèques, intégration dans des IDE) qui permettent de réaliser tout un tas d’analyses dans des buts différents :

Aide au débogage

L’analyse statique permet de vérifier l’utilisation de variables non initialisées, les typos qui apparaissent dans le code, etc.

Aide de PhpStorm

Les premiers outils auxquels nous pourrions penser sont certainement les IDE, comme dans cette capture d’écran de PhpStorm :

Calcul de la complexité du code

Une analyse statique peut permettre d’évaluer la complexité du code : nombre de chemins possibles (beaucoup de conditions), quantité de code dupliqué, paramètres non utilisés, nombre de dépendances, etc.

Exemple d’outils : PHPMD, PHP Copy/Paste Detector, PHPDepend…

Il existe également des outils en ligne qui permettent de surveiller l’évolution de la qualité (Scrutinizer-ci) ou de la dette technique (Sensiolabs Insight) de votre projet.

Respect des standards de code

Un autre grand domaine de l’analyse statique est le formatage du code. En PHP, PSR-1 et PSR-2 sont deux normes permettant de standardiser les conventions entre les différents projets.

Il existe deux outils principaux pour détecter et fixer le code PHP ne respectant pas les standards : Code Sniffer et PHP-CS-Fixer.

Détection de compatibilités

Enfin, dernière catégorie à laquelle nous nous intéresserons, c’est la détection de compatibilité. 2 exemples de projets :

  • deprecation-detector de l’équipe SensioLabs De, permet de détecter l’usage de fonctionnalités dépréciées de Symfony.
  • php7cc, dont nous reparlons à la fin de cet article, permet de tester la compatibilité de votre code avec PHP 7.

Ces catégories ne sont bien sûr pas exclusives : un outil comme phan de Rasmus (le créateur de PHP) détecte le respect des types des paramètres passés aux fonctions, certains problèmes de compatibilité avec PHP 7 mais aussi l’utilisation de variables non définies. Cependant, aussi intéressante et utile que puisse être l’analyse statique, elle n’en reste pas moins limitée.

Limites

La définition même d’analyse statique précise que le programme n’est pas exécuté. Cela implique que nous ne sommes pas capable de deviner avec certitude le fonctionnement du programme. C’est ce que l’on appelle un procédé non déterministe. Dans le cas de PHP, c’est encore pire puisque le langage est fortement dynamique (c’est à dire que beaucoup d’opérations sont effectuées au moment de l’exécution), ce qui diminue d’autant plus notre marge de manoeuvre.

Pour mieux illustrer les limites, nous allons voir quelques cas :

echo 'hello world';

Dans le bout de code précédent, il est facile d’examiner le code et de voir que la chaîne de caractère constante hello world sera envoyée à l’instruction echo. C’est moins évident avec l’exemple suivant :

$var = 'hello world';
// ...
echo $var;

En effet, on s’aperçoit que c’est désormais une variable qui est envoyée. Mais que contient cette variable ? Il faut espérer qu’elle soit déclarée dans le même scope et qu’elle soit initialisée avec une valeur constante.

$var = getHelloWorld();
// ...
echo $var;
// ...
$var = $hello . $world;
// ...
echo $var;

Dans les deux cas précédents, mis à part estimer le type de $var (phpdoc @return sur la fonction getHelloWorld ou résultat de la concaténation), il est probablement impossible de déterminer le contenu de la variable $var. Voilà les limites de l’analyse statique : nous ne pouvons pas remonter très haut dans la logique du code (appel de fonction, opérations sur les variables, etc).

Par exemple, comment s’assurer qu’une méthode qui est appelée sur un objet existe réellement si nous ne connaissons même pas la classe de cet objet (typage faible du langage) ? Heureusement, il est quand même toujours possible de pallier certaines “faiblesses” du langage comme utiliser les type hints declarations ou les phpdoc pour détecter le type d’une variable et s’assurer qu’elle est utilisée correctement.

Scrutinizer analyse les types de retour

Maintenant que nous avons vu ce qu’était l’analyse statique, ses principes mais aussi ses limites, il est temps de mettre les mains dans le cambouis.

Comment ça marche ?

Bon ok, c’est cool, un programme te donne des conseils sur le code que t’as écrit. Mais comment fait-il ?

Regex powa

Il existe plusieurs façons d’analyser du code. Le plus intuitif serait probablement d’appliquer des expressions régulières. C’est d’ailleurs comme cela que fonctionnait la première version de PHP-CS-Fixer. Par exemple, la regex suivante permet d’extraire les déclarations de fonction :

$regex = '~
    function                # function keyword
    \s+                     # any number of whitespaces 
    (?P<function_name>.*?)  # function name itself
    \s*                     # optional white spaces
    (?P<parameters>\(.*?\)) # function parameters
    \s*                     # optional white spaces
    (?P<body>\{.*?\})       # body of a function
~six';

Une fois les fonctions récupérées, nous allons pouvoir procéder à d’autres analyses, suivant ce que nous souhaitons checker / modifier / whatever. Par exemple :

  • Est-ce que le nom de la fonction respecte les standards ?
  • Est-ce qu’il y a des paramètres non utilisés dans la fonction ?
  • Est-ce que la fonction semble inutilisée dans le reste du code ?

Les expressions régulières, aussi rapides et puissantes soient-elles, vont vite devenir compliquées à maintenir pour réellement analyser statiquement notre code de tous les jours. Pour pallier ça, nous allons plutôt nous inspirer du fonctionnement même de PHP.

Parseur et tokens

Pour que l’article reste accessible et pour ne pas s’écarter du sujet, on va simplifier un peu le fonctionnement interne de notre langage préféré. Quand on demande à PHP d’exécuter un script, voici ce qu’il se passe :

  • Analyse lexicale : On transforme les séquences de caractères qui composent notre code source en séquences de token ;
  • Analyse syntaxique : On analyse les séquences de token pour comprendre la structure grammaticale du code (“ici on crée une classe”, “ici on appelle la méthode X sur l’objet Y”, etc) ;
  • Génération des bytecodes (ou zend opcodes dans le vocabulaire PHP) à partir de ce que l’on a compris du code source. L’intérêt des opcodes est qu’ils sont plus bas niveau que notre code et donc beaucoup plus simple à faire exécuter par la machine ;
  • Exécution des bytecodes \o/

Nous allons nous intéresser à la première étape, l’analyse lexicale. En effet, c’est elle qui va nous permettre d’avoir une vision légèrement différente de notre code source, mais surtout, une vision plus facilement exploitable pour y effectuer notre analyse statique. Par exemple, le code suivant :

<?php

class Foo
{
    public $bar = 2;
}

sera transformé en :

[
    [ T_OPEN_TAG, '<?php \n', 1 ],
    [ T_WHITESPACE, '\n', 2 ],
    [ T_CLASS, 'class', 3 ],
    [ T_WHITESPACE, ' ', 3 ],
    [ T_STRING, 'Foo', 3 ],
    [ T_WHITESPACE, '\n', 3 ],
    '{',
    [ T_WHITESPACE, '\n', 4 ],
    [ T_PUBLIC, 'public', 5 ],
    [ T_WHITESPACE, ' ', 5 ],
    [ T_VARIABLE, '$bar', 5 ],
    [ T_WHITESPACE, ' ', 5 ],
    '=',
    [ T_WHITESPACE, ' ', 5 ],
    [ T_LNUMBER, '2', 5 ],
    ';',
    [ T_WHITESPACE, '\n', 5 ],
    '}',
    [ T_WHITESPACE, '\n', 6 ],
]

Comme vous pouvez l’apercevoir, les tokens vont permettre d’analyser le code plus efficacement. Il suffit d’itérer sur le tableau de tokens… Oui mais comment obtenir ce tableau ? Bonne question, jeune padawan ! Comment, depuis un script PHP, parser du code PHP et obtenir la séquence de tokens correspondante ? C’est très simple, il suffit d’appeler la fonction token_get_all() en lui passant en paramètre la chaîne de caractères contenant le code.

Chaque élément du tableau que nous recevons est soit une chaîne de caractères (par exemple ‘{‘, ‘=’, ‘,’, etc.) soit un tableau dont :

  • le premier élément est un entier représentant le type du token ;
  • le deuxième élément est la représentation textuelle de ce token ;
  • le troisième et dernier élément est le numéro de la ligne où apparaît le token.

Par exemple, si nous voulons vérifier que le nom de nos classes sont bien en StudlyCaps, nous pourrions procéder de cette manière :

Voilà, nous avons fait notre première analyse statique ! \o/ Vous vous en doutez, cela reste très basique. Nous avons dû récupérer le token contenant le nom de la classe à partir de la position du token contenant le mot-clé class. Vous imaginez si nous devions effectuer des vérifications sur tous les paramètres de toutes les méthodes de toutes les classes ? Il faudrait parcourir le tableau de tokens plusieurs fois, trouver ceux qui nous intéressent, sauter aux tokens liés, etc. Que les choses soient claires, c’est tout à fait faisable. Avec divers helpers pour naviguer facilement dans une collection de tokens, PHP-CS-Fixer fonctionne exactement de cette manière et parvient à faire des choses assez complexes :

  • détecter et supprimer les imports de classes inutiles (unused use statements)
  • placer correctement les accolades ;
  • transformer les constructeurs PHP 4 en __construct() ;
  • etc.

Seulement, c’est probablement compliqué à maintenir et pas très intuitif à utiliser. Nous ne sommes pas perdus pour autant : il existe une autre représentation du code, encore plus simple à utiliser.

Abstract Syntax Tree (AST)

Voilà notre solution miracle : l’AST (ou arbre syntaxique abstrait en bon français). En quoi est-elle spéciale ? Elle nous donne une représentation encore plus simple à manipuler. Concrètement, l’AST est un arbre dont les noeuds sont les opérateurs et les feuilles sont des variables ou des constantes. Pour bien comprendre le concept, regardons le code suivant :

$b = $a + 5;

L’AST correspondant ressemblerait à ceci :

Assignment (
    var: Variable (
        name: b
    )
    expression: Binary Operation Plus (
        left: Variable (
            name: a
        )
        right: Scalar Left Value (
            value: 5
        )
    )
)

En image, on comprend mieux la structure de l’arbre :

Représentation de l'AST

A partir de l’opérateur assignment, il est aisé de parcourir l’arbre pour “visiter” les variables et constantes impliquées. Un autre avantage de cette représentation est qu’elle permet d’abstraire certaines syntaxes redondantes, par exemple, le fait qu’une variable PHP puisse s’écrire $foo, $$bar, ${'foobar'} ou encore ${!${''}=barfoo()}.

La notion d’AST n’est pourtant pas nouvelle. Elle est au coeur de tous les compilateurs existants : C, Java, python, etc. Pour changer, PHP fait exception. Du moins faisait, avant sa version 7. En effet, Nikita Popov, core-developer du projet PHP, a récemment introduit l’AST durant la phase de compilation. Mais ce n’est pas tout. Bien avant ça, il a créé PHP-Parser, une librairie qui permet justement de parser du PHP en PHP et générer l’AST correspondant. C’est ce projet que nous allons utiliser dans les dernières parties de cet article.

Des exemples concrets : vérifier la compatibilité avec PHP 7

Arrivé à ce stade, vous devriez être capable de comprendre comment faire de l’analyse statique en PHP en utilisant un AST. Mais nous ne pouvons pas se quitter comme ça. Parce que vérifier le nommage d’une classe ou analyser une simple addition, c’est bien, mais je veux vous montrer quelques cas réalistes d’analyse statique et comment il est très simple de manipuler un AST avec le PHP-Parser.

Pour cela, je vais m’inspirer de php7-checker. Cet outil en ligne de commande vérifie si une application utilise des fonctionnalités dépréciées ou supprimées de PHP 7. C’est d’ailleurs ce projet qui m’a inspiré cet article. Nous nous apprêtons à réaliser, ensemble, quelques analyses statiques de compatibilités avec la dernière version de notre langage préféré. Vous êtes prêt ? C’est parti !

Création et parcours de l’AST

Premièrement, on installe le package PHP-Parser :

composer require nikic/php-parser

Au moment de l’écriture de cet article, la dernière version stable est la 1.4.1. Attention si vous utilisez la v2, l’instanciation du parseur a changé, référez-vous à la documentation officielle.

$parser = new \PhpParser\Parser(new \PhpParser\Lexer());

PHP-Parser propose un moyen classique pour parcourir un arbre : le design pattern Visiteur. Ce pattern permet de découpler notre “algorithme” de la structure des données à traverser, ici l’AST. Nos analyses seront donc effectuées par des implémentations de PhpParser\NodeVisitor. C’est ce que nous verrons plus tard. Pour le moment, nous créons un NodeTraverser qui stockera nos visiteurs et les appellera au moment du parcours de nos arbres :

$traverser = new \PhpParser\NodeTraverser();

PHP-Parser fournit un NodeVisitor très utile, le NameResolver. Ce dernier ajoute sur les noeuds de l’AST représentant les classes / fonctions / interfaces / etc leur nom complet, c’est à dire contenant le namespace. Nous n’aurons pas besoin d’aller chercher nous-même si un namespace est présent dans le fichier. Nous allons donc ajouter ce visiteur dès maintenant, il nous sera utile plus tard :

$traverser->addVisitor(new \PhpParser\NodeVisitor\NameResolver());

Nous allons maintenant enregistrer nos propres visiteurs, ceux que nous allons créer dans les parties suivantes de cet article :

$traverser->addVisitor(...);

Il ne nous reste plus qu’à mettre en place la récupération des fichiers, leur parsing et la traversée des AST générés :

foreach (glob('src/*.php') as $file) {
    try {
        $content = file_get_contents($file);

        if (!empty($content)) {
            echo sprintf('Parsing du fichier "%s"', $file);

            // Parsing du code contenu dans le fichier
            $stmts = $parser->parse($content);

            // Traversée des AST générés
            $traverser->traverse($stmts);
        }
    } catch (\PhpParser\Error $e) {
        echo 'Parse Error: ', $e->getMessage();
    }
}

Voilà la base est en place, nous allons pouvoir nous intéresser au coeur de nos analyses statiques.

Usage de type réservé

PHP 7 interdit l’usage de certains types en tant que classe, trait ou interface, dans un alias de use ainsi que dans l’utilisation de class_alias() : int, float, bool, etc. Nous allons donc créer un visiteur TypeReserved :

class TypeReserved extends \PhpParser\NodeVisitorAbstract
{
    private static $reservedTypes = [
        'int',
        'float',
        'bool',
        'string',
        'true',
        'false',
        'null',
    ];

    /**
     * @param int    $line
     * @param string $name
     * @param string $message
     */
    private function check($line, $name, $message)
    {
        $name = strtolower($name);

        if (in_array($name, self::$reservedTypes)) {
            echo '    ' . sprintf($message, $line, $name);
        }
    }
}

L’interface \PhpParser\NodeVisitor définit 4 méthodes :

public function beforeTraverse(array $nodes);
public function enterNode(\PhpParser\Node $node);
public function leaveNode(\PhpParser\Node $node);
public function afterTraverse(array $nodes);

Je vous laisse regarder la documentation pour plus d’informations mais leur utilité devrait être assez compréhensible. Pour éviter d’implémenter les méthodes qui nous sont inutiles, j’ai choisi d’étendre NodeVisitorAbstract qui implémente juste ces 4 méthodes vides. Nous n’avons plus qu’à mettre notre logique dans la méthode leaveNode() :

Il ne faut pas oublier d’enregistrer notre visiteur dans le traverser, comme vu précédemment :

$traverser->addVisitor(new TypeReserved());

Et voilà, nous n’avons plus qu’à faire exécuter notre script sur du code existant pour vérifier sa compatibilité avec les nouveaux noms interdits dans PHP 7. Ce n’était pas très compliqué, n’est-ce pas ? En fait, pour s’en sortir, il ne faut surtout pas hésiter à dumper les nodes que nous recevons. Bien au contraire, c’est même nécessaire pour bien comprendre la structure de l’arbre et trouver où sont cachés les éléments dont nous avons besoin.

Un dernier petit exemple ?

Constructeurs PHP 4 dépréciés

PHP 7 a déprécié (erreur E_DEPRECATED) l’usage des constructeurs PHP 4. Si cela ne vous parle pas, sachez qu’avant PHP 5, le constructeur d’une classe devait porter le même nom que la classe (méthode Foo() dans la classe Foo). Cette fonctionnalité a été conservée jusqu’à la version 7 pour laisser le temps aux projets de migrer sur la nouvelle syntaxe, à savoir la méthode __construct(). Conservée, certes, mais limitée aux cas existants avant PHP 5. Ainsi, pour que la méthode soit reconnue comme constructeur PHP 4, il est nécessaire qu’aucun constructeur PHP 5+ ne soit présent et que la classe ne soit dans aucun namespace (ajouté en PHP 5.3).

C’est donc exactement ce cas là que nous allons cibler :

Comme avant, on n’oublie pas d’enregistrer notre nouveau visiteur :

$traverser->addVisitor(new Php4Constructor());

Et c’est tout ! Nous sommes désormais capable de détecter les constructeurs PHP 4 dans n’importe quelle classe PHP. Plutôt impressionnant avec si peu de lignes de code, non ?

Conclusion

Voilà, vous êtes maintenant des pros de l’analyse statique. Comme vous l’avez compris, l’analyse statique regroupe en fait un nombre important de techniques qui vont de l’aide au développement (typo, debug, etc) à la recherche d’incompatibilités en passant par le respect des standards de code. Certaines limites, inhérentes à cette pratique, nous empêchent bon nombre d’opérations dynamiques, comme connaître le contenu d’une variable par exemple.

Nous avons vu qu’il existait plusieurs façons d’analyser du code mais qu’il valait mieux manipuler un arbre syntaxique abstrait (AST en anglais). En effet, en plus de nous masquer différentes syntaxes pour une même expression, l’AST nous permet surtout une navigation beaucoup plus aisée dans le code source.

En PHP, nous disposons de l’excellent PHP-Parser pour parser du PHP et générer l’AST correspondant. Pour information, cette bibliothèque permet également de générer du code PHP en modifiant directement l’AST.

Enfin, avant de se quitter, quelques mots sur php7-checker, un projet que j’ai entamé en Août 2015. Il détecte beaucoup de changements apportés par PHP 7 qui pourraient impacter vos applications dans le but de les migrer plus facilement sur la dernière version du langage. Lors du développement, je me suis rendu compte qu’un autre projet similaire avait débuté : php7cc. Ce projet semblait bien plus robuste que le mien. Plutôt que de dupliquer nos efforts, j’ai décidé de déprécier mon projet en faveur de celui-ci et j’y ai porté les fonctionnalités manquantes. Jetez-y un oeil si vous voulez vérifier la comptabilités de vos applications avec PHP 7.

Merci de votre lecture.

Sources :

blog comments powered by Disqus