Zum Inhalt springen

Strenge Typisierung überall

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

NextPDF lässt PHPStan auf Level 10 über den Engine-Quellcode laufen, ohne Unterdrückungs-Baseline. Diese Seite erklärt, warum „keine Baseline“ eine Designentscheidung und kein Werkzeugdetail ist und welchen Nutzen diese Strenge für eine Pipeline tatsächlich hat, deren einzige Aufgabe darin besteht, Daten nicht falsch zu verarbeiten.

In den meisten Anwendungen ist strenge Typisierung eine Frage der Hygiene. In einer PDF-Engine ähnelt sie eher einem Korrektheitsmechanismus. Das Format verzeiht keine Fehler. Von einem Reader wird erwartet, dass er Inhalte findet, indem er die Datei von ihrem Ende her über den Trailer und die Querverweistabelle liest. Deshalb müssen die Byte-Offsets eines Writers exakt sein. Man denke an einen Typ, der sich stillschweigend zu mixed erweitert, an einen int, der unbemerkt zu einem string wird, oder an einen Nullable-Wert, der ungeprüft dereferenziert wird. Jeder dieser Fälle kann eine Datei erzeugen, die sich in einem Viewer einwandfrei öffnet und in einem anderen die Validierung nicht besteht – Wochen später, ohne Stack Trace, der auf die Ursache zurückweist.

Die kostspieligen Fehler in diesem Bereich sind die stillen. Strenge Typisierung in Verbindung mit einem strengen Analyzer ist das Mittel, mit dem die Engine eine ganze Klasse stiller Laufzeitfehler in laute Fehler zur Build-Zeit verwandelt.

  • Der Engine-Quellcode wird auf PHPStan Level 10 analysiert – der strengsten Stufe –, wie in phpstan.neon.dist verifiziert.
  • Es gibt keine Unterdrückungs-Baseline für den Quellcode. Die Konfiguration legt die Quellcode-Analyse auf null Fehler fest. Eine Regression lässt den Build fehlschlagen, statt in einer wachsenden Ignore-Datei aufzugehen.
  • Die wenigen vorhandenen ignoreErrors-Einträge sind in der Konfiguration eng gefasst, auf Identifier und Pfad beschränkt und einzeln begründet (paketübergreifende Soft-Dependency-Grenzen und Reflection-Target-Testnahtstellen) – keine pauschale Baseline.
  • Ein separates strenges Profil läuft mit level: max und untersagt jegliche neuen Ignore-Einträge, sodass neuer Code an einen noch strengeren Maßstab gebunden ist.
  • Die beabsichtigte Wirkung ist Designdruck: Code, der sich nicht typehrlich ausdrücken lässt, kommt nicht durch und wird daher neu entworfen statt unterdrückt.

Der Unterschied zwischen „wir verwenden einen strengen Analyzer“ und „wir verwenden einen strengen Analyzer ohne Baseline“ ist entscheidend; deshalb lohnt sich Präzision.

Eine Baseline erfasst jede bestehende Verletzung und weist den Analyzer an, genau diese zu ignorieren. Das ist eine pragmatische Methode, um statische Analyse in einer Altcodebasis einzuführen, hat aber ihren Preis. Die Baseline wird zu einem stillen Schuldenregister, bei dem sich das Typsystem stillschweigend darauf einlässt, wegzusehen. Neue Verletzungen derselben Art können sich neben den im Bestand geduldeten einschleichen. Das Versprechen des Analyzers schwächt sich von „dieser Code ist typsauber“ zu „dieser Code ist nicht schlechter als zuvor“ ab.

NextPDF geht diesen Kompromiss für den Engine-Quellcode nicht ein. Die Konfiguration legt die Quellcode-Analyse auf null Fehler fest und aktiviert reportUnmatchedIgnoredErrors, sodass selbst eine veraltete Unterdrückung – eine, die auf nichts mehr zutrifft – den Build fehlschlagen lässt. Die wenigen verbleibenden, eng gefassten Ignores sind auf einen bestimmten Fehler-Identifier und eine bestimmte Datei beschränkt. Jeder enthält eine Inline-Erläuterung, warum die Grenze beabsichtigt ist (Core ist zum Beispiel gegen eine Pro-/Enterprise-Schnittstelle programmiert, von der es bewusst nicht konkret abhängt). Ein Reviewer kann jeden einzelnen lesen und beurteilen. Es gibt keine undurchsichtige Liste, über die man den Überblick verlieren könnte.

Dieser Ablauf hält das System ehrlich:

  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.
Wie eine Änderung in den Engine-Quellcode gelangt: Eine typunehrliche Änderung kann das Gate nicht passieren und wird daher neu entworfen statt unterdrückt.

treatPhpDocTypesAsCertain gehört dazu. PHPDoc-Annotationen werden als verbindliche Wahrheit behandelt, sodass ein @param list<T> oder ein @return non-empty-string kein Kommentar ist, den der Analyzer höflich ignoriert. Es ist ein geprüftes Versprechen. Annotation und Laufzeittyp müssen übereinstimmen.

Diese Seite ist Evidence: Code-backed . Die Konfiguration ist der Beleg:

  • phpstan.neon.dist setzt level: 10, phpVersion: 80400, analysiert src und enthält keinen baseline:-Schlüssel – es gibt keine phpstan-baseline.neon für die Quellcode-Analyse.
  • Dieselbe Datei setzt treatPhpDocTypesAsCertain: true und reportUnmatchedIgnoredErrors: true, mit einer Inline-Notiz, dass die L10-Quellcode-Analyse auf null Fehler festgelegt ist und jede Regression die CI fehlschlagen lassen muss.
  • Die verbleibenden ignoreErrors sind jeweils auf einen identifier und oft einen path beschränkt, mit Kommentaren, die die Soft-Dependency- und Reflection-Target-Begründung erläutern – sie sind keine pauschal generierte Baseline.
  • phpstan-strict.neon.dist erbt diese Konfiguration, hebt die Stufe auf max an und friert die Ignore-Liste ein, sodass unter dem strengen Profil kein neuer Eintrag hinzugefügt werden darf.

Der Bezug zu den Standards ist unmittelbar. Die Engine muss Dateien erzeugen, in denen ein Reader vom Trailer und der Querverweistabelle aus navigieren kann, gemäß Spec: ISO 32000-2, §7.5.5 . Exakte Byte-Offsets sind ein Typisierungsproblem, bevor sie ein Serialisierungsproblem sind. Ein Offset ist eine Ganzzahl, die niemals stillschweigend zu etwas anderem werden darf. Eine Pipeline, die auf Level 10 typsauber ist, hat bereits die meisten Wege beseitigt, auf denen Arithmetik stillschweigend schiefgehen kann.

Strenge Typisierung ist dort am sichtbarsten, wo eine Domänenregel als Typ statt als Laufzeitprüfung codiert wird. Der Konformitätsdiskriminator beantwortet Fragen auf Spezifikationsebene mit erschöpfendem match, sodass ein nicht behandelter Fall ein Typfehler ist und kein falsches PDF:

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,
};
}
}

Das @return 2|3|4|null ist keine bloße Dokumentation. Unter treatPhpDocTypesAsCertain wird es geprüft. Ein Aufrufer, der annimmt, das Ergebnis sei immer ein int, wird zur Analysezeit darauf hingewiesen – bevor auch nur ein einziges Byte einer nicht konformen PDF/A-Teilnummer jemals geschrieben wird.

Die Falle besteht darin, „keine Baseline“ als „der Code weist zufällig keine Verletzungen auf“ zu lesen. Das ist verkehrt herum gedacht. Das Fehlen einer Baseline ist die Ursache, kein glücklicher Zufall. Weil es keinen Ort gibt, an dem sich eine Verletzung parken ließe, muss Code, der eine solche erzeugen würde, anders geschrieben werden. Level 10 ohne Quellcode-Baseline ist eine Einschränkung, die das Design formt, und kein Zeugnis, das es im Nachhinein beschreibt.

Ein zweites Missverständnis lautet, die Handvoll ignoreErrors-Einträge sei eine Baseline unter anderem Namen. Das sind sie nicht. Eine Baseline wird pauschal generiert und ist undurchsichtig. Diese Einträge sind einzeln verfasst, auf einen Identifier beschränkt, erläutert und durch reportUnmatchedIgnoredErrors abgesichert, sodass sie nicht unbemerkt veralten können.

Diese Seite behandelt die Analyse des Engine-Quellcodes. Die Testsuite wird in einem separaten, bewusst abgegrenzten Scope und mit einer eigenen Konfiguration analysiert; „keine Baseline“ bezieht sich hier auf src/ und ist keine Behauptung, dass jede Hilfsanalyse im Repository baseline-frei ist. PHPStan beweist Typkorrektheit, nicht Verhaltenskorrektheit. Es ersetzt nicht die Testpyramide, sondern beseitigt nur eine Kategorie von Fehlern, denen die Tests andernfalls hinterherjagen müssten. Die genaue Stufe, die Flags und die Ignore-Menge sind zum Prüfdatum dieser Seite zutreffend. Die maßgebliche Quelle sind stets phpstan.neon.dist und phpstan-strict.neon.dist im Core-Repository.

Die Edition ändert nichts an dieser Disziplin. Jede Edition wird aus demselben Level-10-Quellcode erstellt:

Level 10 source analysis — edition availability
Edition Availability
Core Der Core-Quellcode wird auf Level 10 ohne Quellcode-Baseline analysiert.
Pro Pro baut auf derselben Level-10-Quellcode-Disziplin auf.
Enterprise Enterprise baut auf derselben Level-10-Quellcode-Disziplin auf.
  • PHPStan Level 10 – die strengste Analysestufe, die untypisierte und lose typisierte Werte als Fehler statt als Warnungen behandelt.
  • Baseline – eine generierte Liste bestehender Verletzungen, die der Analyzer ignorieren soll. NextPDF verwendet für den Engine-Quellcode keine.
  • treatPhpDocTypesAsCertain – eine PHPStan-Einstellung, die PHPDoc-Typannotationen als geprüfte Fakten behandelt, nicht als unverbindliche Kommentare.
  • reportUnmatchedIgnoredErrors – eine Einstellung, die den Build fehlschlagen lässt, wenn ein Ignore-Eintrag auf nichts mehr zutrifft, und so veraltete Unterdrückungen verhindert.
  • Designdruck – die Wirkung einer Einschränkung, die erzwingt, dass Code auf eine bestimmte Weise geschrieben wird, im Gegensatz zu einer Prüfung, die ihn nur misst.