Aller au contenu
getnextpdf.com

Des types stricts partout

Spec: ISO 32000-2, §7.5.5PHPStan: Level 10, no src baseline

NextPDF exécute PHPStan en Level 10 sur les sources du moteur, sans baseline de suppression. Cette page explique pourquoi « pas de baseline » est une décision de conception plutôt qu’un détail d’outillage, et ce que cette rigueur apporte concrètement à un pipeline dont la seule mission est de ne jamais traiter les données de manière erronée.

Dans la plupart des applications, le typage strict relève de l’hygiène. Dans un moteur PDF, il ressemble davantage à un mécanisme de correction. Le format ne pardonne rien. Un lecteur est censé localiser le contenu en lisant le fichier depuis sa fin, via le trailer et la table de références croisées ; les décalages d’octets produits côté écriture doivent donc être exacts. Imagine un type qui s’élargit discrètement en mixed, un int qui devient silencieusement une string, ou un type nullable déréférencé sans vérification. Chacun de ces cas peut produire un fichier qui s’ouvre correctement dans une visionneuse et échoue à la validation dans une autre, des semaines plus tard, sans trace d’appel qui remonte à la cause.

Dans ce domaine, les défaillances coûteuses sont celles qui restent silencieuses. Le typage strict, associé à un analyseur strict, permet au moteur de transformer toute une catégorie de défaillances silencieuses à l’exécution en échecs explicites à l’analyse.

  • Les sources du moteur sont analysées en PHPStan Level 10 — le niveau le plus strict — comme le confirme phpstan.neon.dist.
  • Il n’existe aucune baseline de suppression sur les sources. La configuration verrouille l’analyse des sources à zéro erreur. Une régression fait échouer la compilation au lieu d’être absorbée par un fichier d’exceptions en croissance continue.
  • Les rares entrées ignoreErrors restantes sont restreintes, limitées par identifiant et par chemin, et justifiées une à une dans la configuration (frontières de dépendances faibles entre paquets et points d’ancrage de test ciblant la réflexion) — pas une baseline générée en masse.
  • Un profil strict distinct s’exécute en level: max et interdit toute nouvelle entrée d’exception, si bien que le nouveau code suit une ligne de conduite encore plus stricte.
  • L’effet recherché est une pression de conception : le code qui ne peut pas être exprimé honnêtement du point de vue des types ne passe pas, il est donc reconçu plutôt que masqué.

La différence entre « nous utilisons un analyseur strict » et « nous utilisons un analyseur strict sans baseline » est essentielle ; il faut donc être précis.

Une baseline consigne chaque violation existante et indique à l’analyseur d’ignorer exactement celles-là. C’est une façon pragmatique d’adopter l’analyse statique sur une base de code héritée, mais elle a un coût. La baseline devient un registre discret de dette technique que le système de types a accepté d’ignorer. De nouvelles violations du même type peuvent se glisser à côté de celles qui ont déjà été tolérées. La promesse de l’analyseur s’affaiblit : elle passe de « ce code est propre du point de vue des types » à « ce code n’est pas pire qu’avant. »

NextPDF n’accepte pas ce compromis pour les sources du moteur. La configuration fige l’analyse des sources à zéro erreur et active reportUnmatchedIgnoredErrors, si bien que même une suppression obsolète — qui ne correspond plus à rien — fait échouer la compilation. Les rares exceptions qui subsistent sont limitées à un identifiant d’erreur et à un fichier précis. Chacune comporte une explication en ligne justifiant pourquoi la frontière est intentionnelle (par exemple, le cœur qui programme contre une interface Pro/Enterprise dont il ne dépend délibérément pas de façon concrète). Un relecteur peut lire chacune et la juger. Il n’y a pas de liste opaque dont on perdrait la trace.

Le flux qui maintient cette honnêteté :

  1. Change proposedNew or modified engine code.
  2. Level 10 analysisStrictest PHPStan level over src/, treatPhpDocTypesAsCertain on.
  3. Zero-error gateNo source baseline ; unmatched ignores also fail.
  4. Strict profilelevel : max ; no new ignore entries permitted.
  5. Redesign, not suppressIf it cannot be expressed honestly, the design changes.
Comment une modification atteint les sources du moteur : une modification malhonnête vis-à-vis des types ne peut pas franchir le portail, elle est donc reconçue plutôt que masquée.

treatPhpDocTypesAsCertain va dans le même sens. Les annotations PHPDoc sont traitées comme une source de vérité ; un @param list<T> ou un @return non-empty-string n’est donc pas un commentaire que l’analyseur ignorerait poliment. C’est une promesse vérifiée. L’annotation et le type à l’exécution doivent concorder.

Le typage strict se voit le mieux là où une règle métier est encodée sous forme de type plutôt que sous forme de vérification à l’exécution. Le discriminateur de conformité répond aux questions de spécification au moyen d’un match exhaustif ; ainsi, un cas non traité est une erreur de type, pas un PDF erroné :

declare(strict_types=1);
enum ConformanceMode: string
{
case Plain = 'plain';
case PdfUa2 = 'pdfua2';
case PdfA4 = 'pdfa4';
/** @return 2|3|4|null */
public function pdfaPart(): ?int
{
return match ($this) {
self::PdfA4 => 4,
default => null,
};
}
}

Le @return 2|3|4|null n’est pas de la documentation. Sous treatPhpDocTypesAsCertain, il est vérifié. Un appelant qui suppose que le résultat est toujours un int en est averti au moment de l’analyse, avant que le moindre octet d’un numéro de partie PDF/A non conforme ne soit écrit.

Le piège consiste à lire « pas de baseline » comme « il se trouve que le code n’a aucune violation. » C’est l’inverse. L’absence de baseline est la cause, pas un heureux hasard. Comme il n’existe aucun endroit où ranger une violation, le code qui en produirait une doit être écrit autrement. Le Level 10 sans baseline sur les sources est une contrainte qui façonne la conception, pas un constat établi après coup.

Une deuxième idée fausse : la poignée d’entrées ignoreErrors serait une baseline sous un autre nom. Ce n’est pas le cas. Une baseline est générée en masse et opaque. Celles-ci sont écrites une à une, limitées par identifiant, expliquées, et protégées par reportUnmatchedIgnoredErrors afin qu’elles ne puissent pas se dégrader sans qu’on s’en aperçoive.

Cette page porte sur l’analyse des sources du moteur. La suite de tests est analysée avec une portée et une configuration distinctes, délibérément séparées ; ici, « pas de baseline » est une affirmation portant sur src/, pas une prétention selon laquelle chaque analyse auxiliaire du dépôt serait sans baseline. PHPStan prouve la solidité des types, pas la justesse comportementale. Il ne remplace pas la pyramide des tests ; il supprime seulement une catégorie de défaillances que les tests devraient sinon traquer. Le niveau exact, les drapeaux et l’ensemble des exceptions sont exacts à la date de relecture de cette page. Les sources faisant autorité restent phpstan.neon.dist et phpstan-strict.neon.dist dans le dépôt du cœur.

L’édition ne change rien à cette discipline. Chaque édition est construite à partir des mêmes sources en Level 10 :

Level 10 source analysis — edition availability
EditionAvailability
CoreLes sources du cœur sont analysées en Level 10 sans baseline sur les sources.
ProPro repose sur la même discipline de sources en Level 10.
EnterpriseEnterprise repose sur la même discipline de sources en Level 10.
  • PHPStan Level 10 — le niveau d’analyse le plus strict, traitant les valeurs non typées et faiblement typées comme des erreurs plutôt que des avertissements.
  • Baseline — un inventaire généré des violations existantes que l’analyseur reçoit l’ordre d’ignorer. NextPDF n’en utilise aucune pour les sources du moteur.
  • treatPhpDocTypesAsCertain — un paramètre PHPStan qui traite les annotations de type PHPDoc comme des faits vérifiés, et non comme des commentaires indicatifs.
  • reportUnmatchedIgnoredErrors — un paramètre qui fait échouer la compilation lorsqu’une entrée d’exception ne correspond plus à rien, empêchant les suppressions obsolètes.
  • Pression de conception — l’effet d’une contrainte qui force le code à être écrit d’une certaine manière, par opposition à une vérification qui se contente de le mesurer.