Pièges connus
Chaque piège ci-dessous a été rencontré sur de vrais modules. Le symptôme y est souvent trompeur : c'est pourquoi chaque section commence par lui.
Analyser tout le dépôt avec paths
Symptôme : l'analyse dure plusieurs minutes, consomme plus d'un gigaoctet, finit par Allowed memory size exhausted in FileFinder.php, ou signale des erreurs absurdes comme Call to an undefined method MyObject::countThirdparties() sur une méthode qui existe.
Cause : paths: - . fait parcourir tout le dossier du module à PHPStan, vendor/ compris. Avec les stubs installés par Composer, cela fait 15 versions de Dolibarr à lire. Chaque version contient le modèle MyObject du modulebuilder, et PHPStan peut retenir l'une de ces classes à la place de la vôtre.
Mesuré sur un module de deux fichiers, stubs installés par Composer :
| Configuration | Durée | Mémoire |
|---|---|---|
paths: - . sans exclusion |
plus de 5 minutes, interrompu | 1,5 Go |
paths: - . et excludePaths.analyse: vendor/* |
17 s, avec des erreurs fausses | 270 Mo |
paths: - . et excludePaths.analyseAndScan: vendor/* |
5 s | 170 Mo |
paths qui liste les dossiers sources |
3 s | 140 Mo |
Remède : listez vos dossiers et fichiers sources dans paths.
excludePaths.analyse ne fait que masquer les erreurs des fichiers exclus : PHPStan continue de les lire pour y chercher des classes. excludePaths.analyseAndScan les écarte vraiment, mais PHPStan énumère quand même toute l'arborescence avant d'appliquer l'exclusion. Si le dépôt contient un lien symbolique qui revient sur lui-même (un environnement de test qui expose le module dans un faux htdocs/custom/, par exemple), cette énumération tourne en boucle et l'analyse meurt avant d'avoir regardé les exclusions. Seule une liste explicite dans paths évite ce cas.
paths n'accepte pas de motifs : - *.php donne Path *.php does not exist. Pour ne pas oublier les nouveaux fichiers PHP à la racine du module, faites développer la liste par make et passez-la en ligne de commande, où elle remplace celle du fichier de configuration :
PHPSTAN_PATHS := $(wildcard admin ajax class core lib tpl) $(wildcard *.php)
phpstan:
vendor/bin/phpstan analyse --memory-limit=512M $(PHPSTAN_PATHS)
$(wildcard ...) ignore les dossiers qui n'existent pas : la même ligne sert à tous les modules.
Un cache partagé entre deux versions de PHPStan
Symptôme : PHPStan 2 s'arrête au démarrage avec une erreur interne du type ResultCacheManager ... Argument #1 ($byFile) must be of type array, null given.
Cause : le même répertoire de cache a été utilisé auparavant par PHPStan 1.
Remède : un répertoire de cache par version de PHPStan, et par projet, avec tmpDir dans la configuration :
parameters:
tmpDir: /tmp/phpstan-mymodule
Un dossier de cache qui appartient à quelqu'un d'autre
Symptôme : sur une machine partagée par plusieurs comptes, PHPStan échoue pour tous les utilisateurs sauf un, avec une erreur d'écriture qui ne nomme pas la cause.
Cause : sans tmpDir, PHPStan écrit dans /tmp/phpstan. Le premier compte qui l'utilise crée ce dossier à son nom, et les autres ne peuvent plus y écrire.
Remède : un tmpDir propre à chaque utilisateur, dans un fichier de configuration non versionné : tmpDir: /tmp/phpstan-mymodule-%env.USER%. Écrivez bien %env.USER% : PHPStan n'interprète pas $USER, qui donnerait un dossier nommé littéralement $USER, partagé par tout le monde.
Un TMPDIR qui n'existe pas
Symptôme : Failed creating temp file for stdout, écrit sur la sortie d'erreur. Si vous comptez les erreurs en masquant cette sortie (2>/dev/null | grep -c ...), vous obtenez 0 et concluez à tort que tout va bien.
Cause : la variable d'environnement TMPDIR pointe vers un dossier absent. PHPStan l'utilise pour communiquer avec ses processus parallèles.
Remède : créez le dossier avant de lancer l'analyse, et ne masquez pas la sortie d'erreur quand vous mesurez.
llxHeader invoked with 2 parameters, 0 required
Symptôme : chaque appel à llxHeader() est signalé sur les versions anciennes de Dolibarr, puis plus du tout à partir de la 21.
Cause : jusqu'à Dolibarr 20, plusieurs pages du coeur (document.php, viewimage.php, des pages publiques) redéclarent llxHeader() sans paramètres, et PHPStan peut retenir l'une de ces déclarations.
Remède : un ignore ciblé, qui ne doit pas être signalé comme inutile sur les versions récentes (PHPStan 2) :
parameters:
ignoreErrors:
-
message: '#^Function llxHeader invoked with \d+ parameters?, 0 required\.$#'
reportUnmatched: false
Un test sur DOL_VERSION déclaré toujours faux
Symptôme : Comparison operation ">=" between 18 and 20 is always false sur une ligne comme if ((int) DOL_VERSION >= 20), et la branche correspondante signalée comme du code mort.
Cause : chaque dossier de stubs définit DOL_VERSION avec sa valeur ('18.0.4' pour Dolibarr 18). PHPStan remplace la constante par cette valeur et calcule le résultat de la comparaison.
Remède : déclarez la constante comme variable :
parameters:
dynamicConstantNames:
- DOL_VERSION
version_compare(DOL_VERSION, '20.0.0', '>=') n'est pas concerné : PHPStan ne calcule pas le résultat de version_compare().
Une version de stubs absente du clone
Symptôme : Path .../dolibarr-21 does not exist au démarrage.
Cause : le clone partiel ne contient que les versions demandées à git sparse-checkout set.
Remède : git sparse-checkout set phpstan dolibarr-18 dolibarr-21, avec la liste complète des versions voulues.
Des fonctions documentées mais vues comme non typées
Symptôme : PHPStan signale l'absence de type sur une fonction dont le bloc de commentaire décrit pourtant les paramètres et le retour.
Cause : le bloc commence par /*** ou /****. PHPStan ne lit comme PHPDoc que les blocs qui commencent exactement par /**.
Remède : remplacez l'ouverture par /**. Toute la documentation existante redevient utile d'un coup.
isset() sur un paramètre
Symptôme : Variable $param might not be defined plus bas dans une fonction, alors que $param est un paramètre.
Cause : un if (isset($param)) sur un paramètre qui a une valeur par défaut. Le paramètre existe toujours, mais PHPStan en déduit qu'il peut ne pas exister dans la branche où isset() est faux.
Remède : supprimez le isset(), ou remplacez-le par un test sur la valeur ($param !== null).
|= sur un include_once
Symptôme : aucun, et c'est le problème. Le motif vient du modulebuilder, on le trouve dans la plupart des modules :
$mybool |= @include_once $dir.$file;
...
if ($mybool === false) {
Cause : |= transforme le booléen en entier. Le test === false ne peut plus jamais être vrai, et un fichier de numérotation manquant passe sans un mot. PHPStan le signale à partir du niveau 4 (comparaison toujours fausse).
Remède, celui du coeur de Dolibarr récent :
$mybool = ((bool) @include_once $dir.$file) || $mybool;
...
if (!$mybool) {
La suite : Comment les stubs sont produits.