Aller au contenu

Des types stricts partout

Spec: ISO 32000-2, §7.5.5 Evidence: Code-backed PHPStan: 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 proposed New or modified engine code.
  2. Level 10 analysis Strictest PHPStan level over src/, treatPhpDocTypesAsCertain on.
  3. Zero-error gate No source baseline ; unmatched ignores also fail.
  4. Strict profile level : max ; no new ignore entries permitted.
  5. Redesign, not suppress If 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.

Cette page est Evidence: Code-backed . La configuration en apporte la preuve :

  • phpstan.neon.dist définit level: 10, phpVersion: 80400, analyse src, et ne contient aucune clé baseline: — il n’existe aucun phpstan-baseline.neon pour l’analyse des sources.
  • Le même fichier définit treatPhpDocTypesAsCertain: true et reportUnmatchedIgnoredErrors: true, avec une note en ligne précisant que l’analyse des sources en L10 est verrouillée à zéro erreur et que toute régression doit faire échouer la CI.
  • Les ignoreErrors restantes sont chacune limitées par identifier et souvent par path, avec des commentaires expliquant la logique de dépendance faible et les cibles de réflexion — ce n’est pas une baseline générée en masse.
  • phpstan-strict.neon.dist hérite de cette configuration, porte le niveau à max et gèle la liste d’exceptions de sorte qu’aucune nouvelle entrée ne puisse être ajoutée sous le profil strict.

Côté normes, le lien est direct. Le moteur doit produire des fichiers qu’un lecteur peut parcourir depuis le trailer et la table de références croisées conformément à Spec: ISO 32000-2, §7.5.5 . Les décalages d’octets exacts sont d’abord un problème de typage, avant d’être un problème de sérialisation. Un décalage est un entier qui ne doit jamais devenir silencieusement autre chose. Un pipeline qui est propre du point de vue des types en Level 10 a déjà éliminé la plupart des façons dont l’arithmétique peut déraper en silence.

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
Edition Availability
Core Les sources du cœur sont analysées en Level 10 sans baseline sur les sources.
Pro Pro repose sur la même discipline de sources en Level 10.
Enterprise Enterprise 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.