L’analyse statique est la révision du code PHP, sans l’exécuter. En un mot, cela signifie lire le code et le comprendre, tout comme le font les développeurs lorsqu’ils modifient un morceau de PHP. C’est le même processus, avec des outils différents. Voici le chemin du code PHP à l’analyse statique.
Examen manuel du code
L’analyse statique trouve sa place pour les revues simples. Il vise également à faire l’examen systématique et répétitif. Une fois automatisée, une révision peut être appliquée à plusieurs reprises à la même base de code, suivant son évolution. Les humains, en revanche, sont meilleurs à des niveaux plus élevés d’abstraction et de détection de nouveaux modèles.
La charge statique
L’analyse statique est distincte de l’analyse dynamique : ce second type d’analyse nécessite l’exécution du code, et, pour cela, nécessite du code fonctionnel et une architecture valide pour le faire. Les tests unitaires, les tests de cycle de vie et la surveillance des journaux sont tous des analyses dynamiques.
En bref, l’analyse dynamique exécute le code et fonctionne avec des données réelles, fausses ou réelles. Il observe les réactions réelles du code, basées sur une large sélection de situations. Cela inclut les données et le comportement PHP.
PHP commence à partir du fichier texte : vous pourriez l’appeler code ou source, mais ce n’est initialement rien de plus qu’un fichier texte. PHP y appliquera le tokenizer : c’est la partie de PHP qui décompose le texte en jetons PHP, qui sont essentiellement des mots PHP. Ensuite, il vérifiera la syntaxe : c’est la règle qui organise les jetons en une phrase significative. Ceci est similaire à un langage naturel.
A ce stade, PHP démarre son propre processus : optimiser le code pour l’exécution, éventuellement en mettre en cache une partie, puis passer à la phase d’exécution. L’exécution a ses propres contraintes : par exemple, PHP n’exécutera pas indéfiniment une boucle infinie. Il s’arrêtera au tristement célèbre ‘Max execution time’ de 30s (par défaut), ou il trouvera une condition finale. Même si vous avez un serveur de longue durée avec une boucle infinie, il s’arrêtera probablement lorsque le matériel tombera en panne. Pas de boucle infinie pour PHP.
D’autre part, l’analyse statique gère assez bien la boucle infinie. Puisque le code n’est pas exécuté, mais simplement compris, cela signifie qu’un while(true) {} est juste une autre boucle. C’est un avantage significatif par rapport au moteur PHP.
L’autre avantage de l’analyse statique est la contrainte de temps. PHP travaille dur pour produire le contenu attendu dans le plus court laps de temps possible, tandis que l’analyse statique a un laps de temps beaucoup plus long. C’est le temps du développement, pas le temps de la production. Tout commentaire intéressant vaut un temps raisonnable.
Traitement du code avec analyse statique
Pour que l’analyse statique comprenne le code, elle doit suivre plusieurs étapes. Nous venons de voir le processus de tokenisation, qui transforme la source en éléments PHP.
L’ensemble du processus est assez simple, mais il comprend quelques situations techniques qui méritent d’être détaillées.
Les jetons atomiques
Le processus de tokenisation est le premier processus. C’est aussi le processus le plus ancien de PHP, et il existe depuis PHP 2. Il s’est développé au fil des années, et a connu plusieurs modernisations, notamment la prochaine étape : l’AST. Pourtant, il contient un certain nombre de fonctionnalités intéressantes.
Le mécanisme PHP tokenizer est disponible grâce à l’extension ext/tokenizer. Vous pouvez utiliser le jetonobtenirall() sur le contenu d’un fichier PHP, et un long tableau de jetons, comme celui-ci ci-dessous. Le script original est celui de gauche, et une partie des jetons est présentée à droite.
'EXT'
chaîne, ou la define
nom de la fonction dans les jetons assez facilement.
Les jetons sont généralement désignés par leur nom ; JCHAÎNE, TÉVAL, TVARIABLE, etc. Comme ce sont des constantes, elles apparaissent avec leur valeur constante dans le tableau de jetons. Vous pouvez utiliser un jetonget_name() pour accéder à leur nom. Ces valeurs changent d’une version à l’autre : puisque seuls les auteurs PHP et d’analyseurs statiques les utilisent, ce n’est pas un problème. Nous ne ferons référence qu’au jeton avec leur nom à partir de maintenant.
Tous les jetons ne sont pas égaux
Il est facile de se retrouver avec un million de jetons à partir d’un fichier PHP donné. Les jetons couvrent tous les aspects du script PHP : variables, constantes, commentaires, espaces, délimiteurs. Habituellement, un tiers des jetons sont des espaces blancs et des commentaires : PHP les supprimera simplement au moment de l’exécution. L’analyse statique conserve certains d’entre eux, comme le commentaire PHPdoc, mais se débarrasse de la plupart des autres.
Une source de code PHP comprend également de nombreux délimiteurs, tels que ( ) { } [ ] ' "
etc. Ceux-ci sont importants pour que PHP comprenne l’organisation du code : pourtant, ils ne sont pas importants au-delà du lexer, et sont également supprimés. Dans l’ensemble, cela signifie que les deux tiers de tout script PHP sont inutiles pour l’exécution. C’est assez surprenant à comprendre.
Un AST PHP
Les délimiteurs ne sont pas conservés, en tant que tels, pour l’exécution. Évidemment, ils sont importants pour que PHP comprenne le code. Ainsi, bien que ces délimiteurs soient supprimés, leur signification est préservée. Par exemple, les accolades pour un bloc dans une fonction sépareront le code dans le bloc de l’autre contexte.
Dans le code ci-dessus, vous pouvez voir un code PHP simple, intégré à l’AST. Les délimiteurs tels que les accolades sont transformés en un ‘lien’ appelé ‘EXPRESSION’. La parenthèse est convertie en ‘CONDITION’ (pour si alors), ou ARGUMENT (pour les paramètres de fonction). PHP réutilise les mêmes jetons pour plusieurs situations : la signification réelle d’un jeton dépend en fait de son contexte.
L’un des jetons les plus réutilisés est T_VARIABLE, qui, comme vous vous en doutez, est utilisé pour les variables ($x = 1
), mais aussi pour les paramètres (fonction foo($parameter)
), les définitions de propriétés (private C $property = 1
), les définitions globales et les désignations de membres statiques (A::$property
). Il n’y a aucun moyen de les distinguer au niveau du jeton : tout dépend du contexte.
De nos jours, l’AST est directement disponible depuis PHP lui-même. Il existe une extension C, appelée à juste titre ext/ast, qui ignore tout le travail de jeton. Il accélère et normalise les développements de l’analyse statique.
Jouons dans les arbres
Bien que l’AST soit une étape importante dans la compréhension du code, ce n’est certainement pas la dernière étape. L’état actuel décrit le code au niveau de la ligne (comme dans Ligne de code). En tant que développeur, nous comprenons beaucoup mieux le code en remarquant qu’il existe un appel de fonction (foo($a)) et une définition de fonction (fonction foo($b)) qui sont conceptuellement liés : l’un est la définition, et le l’autre est l’utilisation.
C’est une méta notion dans le monde de la programmation. PHP l’utilise beaucoup, bien que la façon dont il est implémenté dépende fortement du couple définition-utilisation. Voici une courte liste d’entre eux, pour vous donner une idée.
- Les variables sont créées par affectation (affectations explicites, passage de paramètres), et elles sont utilisées en étant invoquées.
- Les classes sont créées avec le mot-clé ‘class’ et utilisées avec le mot-clé ‘new’
- Les traits sont créés avec le mot-clé ‘trait’ et utilisés avec le mot-clé ‘use’ (sic)
- Les inclusions sont créées avec chaque nouveau fichier (c’est même externe à PHP), et utilisées avec ‘include’ et ses cousins
- Les fonctions sont créées avec le mot-clé ‘function’, et utilisées par leur nom, avec ou sans parenthèses (callbacks)
- constantes, propriétés, méthodes, classes anonymes…
Il est possible d’établir un lien entre n’importe lequel des usages mentionnés ci-dessus, et leur définition. Cela crée un raccourci dans le code, ce qui est crucial pour comprendre le déroulement de l’exécution.
Lors de l’exécution, PHP utilise de nombreux hashmaps, pour sauter d’une partie du code à l’autre. Lorsque le saut n’est pas possible, il peut produire soit un avertissement (variable indéfinie), soit une erreur fatale (fonction indéfinie). Ce traitement des erreurs est un peu dur, mais il maintient le moteur rapide.
C’est aussi le moment où l’analyse statique doit se séparer du comportement de PHP : l’analyse statique recherche les définitions manquantes. Il doit faire la différence entre l’appel de la fonction ‘foo()’ et un appel erroné à ‘goo()’ : l’un a une définition, et pas l’autre.
Comme vous pouvez le voir, l’AST a été déformé par l’ajout de liens ‘DEFINITION’, qui rapprochent une fonction et son utilisation, et des paramètres avec leur utilisation à l’intérieur du bloc fonction. Étendu à la taille complète de l’application, nous avons maintenant un nuage dense d’objets. En fait, il n’est pas raisonnable de l’afficher dans cet article, au-delà de simples exemples comme ci-dessus.
Les définitions manquantes
Le nuage de code PHP va devenir encore plus dense lorsque nous commencerons à remarquer qu’il manque une définition à certaines fonctions (ou méthodes, ou classes…). Vous m’avez bien entendu : une fonction qui n’a pas de définition, mais qui fonctionne parfaitement avec du code PHP. Il existe deux types de telles fonctions : les fonctions natives et les composants externes.
Les fonctions natives sont des fonctions telles que strtolower
, qui font partie de la distribution PHP par défaut. L’exemple précis de strtolower
est toujours compilé, bien qu’il puisse être désactivé dans le fichier php.ini. Il existe d’autres fonctions natives, telles que mb_strtolower()
, qui sont également considérés comme natifs, mais dépendent de la configuration réelle. Les extensions de PECL et d’autres (comme Xdebug) apportent toutes des constantes, des fonctions, des classes et des directives qui doivent être reconnues dans le code.
Pour ceux-ci, l’analyse statique doit utiliser une base de données locale de définitions, pour pouvoir reconnaître ces structures. La signature des fonctions est souvent suffisante pour comprendre comment pow(1,2,3)
appelle en fait une fonction PHP native, avec trop d’arguments. Chaque extension peut être documentée pour recréer toute fonction attendue.
La même stratégie est applicable aux composants externes. Ici, les composants sont un mot générique qui couvre tout code externe utilisé, mais non inventé ici. Cela signifie framework, composants composer, bibliothèques, plugins et autres modules. Ceux-ci sont généralement exclus de l’analyse, car il ne sert à rien de revoir le code d’une équipe indépendante tant qu’il n’est pas en proie à un bogue qui s’infiltre dans votre propre code.
La même stratégie de fichiers stub, qui décrit le code du composant, mais ignore les détails d’implémentation, est un bon moyen de garder l’analyse statique concentrée sur le sujet du jour : votre propre code.
Prêt à naviguer dans le code
Vous pouvez mesurer la distance que nous avons parcourue ensemble. Nous sommes partis des fichiers texte qui sont le code source, l’avons tokenisé, lexiqué, lié et complété par des connaissances externes. Ceci est assez similaire au travail que fait PHP à chaque fois que nous atteignons un serveur Web et qu’il exécute du code. C’est aussi beaucoup plus de travail que de simplement l’exécuter : comme nous l’avons vu, cette nouvelle représentation du code est capable de détecter des codes indéfinis et de nous permettre de corriger le code. C’est un avantage majeur, car nous nous apprêtons à naviguer dans cet océan d’expressions et d’opérateurs, en détectant la surcharge mentale, les codes impossibles et les complexités accumulées.
Vous pouvez voir tout cela en action, en exécutant le moteur Exakat. Jetez un œil aux rapports publics, vérifiez le moteur Exakat et emmenez-nous faire un tour avec le cloud Exakat.
Bonnes revues de code PHP !