Salta ai contenuti

Tipi rigorosi, ovunque

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

NextPDF esegue PHPStan a Level 10 sul sorgente del motore senza alcuna baseline di soppressione. Questa pagina spiega perché «nessuna baseline» sia una decisione di progettazione anziché un dettaglio di tooling, e che cosa quel rigore offra in concreto a un flusso il cui compito essenziale è non gestire mai i dati in modo errato.

Nella maggior parte delle applicazioni, la tipizzazione rigorosa è igiene del codice. In un motore PDF è più vicina a un meccanismo di correttezza. Il formato non perdona. Ci si aspetta che un reader individui i contenuti leggendo il file dalla fine, attraverso il trailer e la tabella di riferimenti incrociati; per questo gli offset di byte di un writer devono essere esatti. Si consideri un tipo che si allarga in sordina a mixed, un int che diventa silenziosamente una string, oppure un valore nullable dereferenziato senza controllo. Ciascuno di questi casi può produrre un file che si apre senza problemi in un visualizzatore e non supera la validazione in un altro, settimane dopo, senza alcuno stack trace che riporti alla causa.

In questo dominio i guasti costosi sono quelli silenziosi. La tipizzazione rigorosa, unita a un analizzatore rigoroso, è il modo in cui il motore trasforma una classe di guasti silenziosi a runtime in fallimenti espliciti in fase di build.

  • Il sorgente del motore è analizzato a PHPStan Level 10 — il livello più rigoroso — verificato in phpstan.neon.dist.
  • Non esiste alcuna baseline di soppressione sul sorgente. La configurazione impone l’analisi del sorgente a zero errori. Una regressione fa fallire la build anziché essere assorbita in un file di ignore in continua crescita.
  • Le poche voci ignoreErrors esistenti sono ristrette, circoscritte per identificatore e per percorso e giustificate individualmente nella configurazione (confini di dipendenza debole tra package e giunzioni di test con target di reflection) — non costituiscono una baseline di massa.
  • Un profilo strict separato esegue level: max e vieta qualsiasi nuova voce di ignore, così il nuovo codice è tenuto a un vincolo ancora più stretto.
  • L’effetto voluto è una pressione progettuale: il codice che non può essere espresso in modo onesto tramite i tipi non passa, quindi viene riprogettato anziché soppresso.

La differenza tra «usiamo un analizzatore rigoroso» e «usiamo un analizzatore rigoroso senza baseline» è il punto essenziale, quindi vale la pena essere precisi.

Una baseline registra ogni violazione esistente e indica all’analizzatore di ignorare esattamente quelle. È un modo pragmatico per adottare l’analisi statica su una codebase legacy, ma ha un costo. La baseline diventa un registro silenzioso di debito che il sistema dei tipi ha accettato di non esaminare. Nuove violazioni dello stesso tipo possono insinuarsi accanto a quelle ereditate. La promessa dell’analizzatore si indebolisce, passando da «questo codice è pulito sui tipi» a «questo codice non è peggio di prima».

NextPDF non accetta quel compromesso per il sorgente del motore. La configurazione impone l’analisi del sorgente a zero errori e attiva reportUnmatchedIgnoredErrors, così persino una soppressione obsoleta — una che non corrisponde più a nulla — fa fallire la build. Le voci di ignore ristrette che rimangono sono circoscritte a uno specifico identificatore di errore e a un file. Ognuna riporta una spiegazione inline del perché il confine sia intenzionale (per esempio, il core che programma verso un’interfaccia Pro/Enterprise da cui deliberatamente non dipende in modo concreto). Un revisore può leggerle una per una e valutarle. Non esiste alcun elenco opaco di cui perdere il filo.

Il flusso che mantiene verificabile questa disciplina:

  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.
Come una modifica raggiunge il sorgente del motore: una modifica disonesta sui tipi non può superare il gate, quindi viene riprogettata anziché soppressa.

treatPhpDocTypesAsCertain fa parte di questo impianto. Le annotazioni PHPDoc sono trattate come fonte di verità, quindi un @param list<T> o un @return non-empty-string non è un commento che l’analizzatore può ignorare con discrezione. È una promessa verificata. Annotazione e tipo a runtime sono costretti ad allinearsi.

Questa pagina è Evidence: Code-backed . La configurazione è l’evidenza:

  • phpstan.neon.dist imposta level: 10, phpVersion: 80400, analizza src e non contiene alcuna chiave baseline: — non esiste alcun phpstan-baseline.neon per l’analisi del sorgente.
  • Lo stesso file imposta treatPhpDocTypesAsCertain: true e reportUnmatchedIgnoredErrors: true, con una nota inline secondo cui l’analisi del sorgente L10 è bloccata a zero errori e qualsiasi regressione deve far fallire la CI.
  • Gli ignoreErrors rimanenti sono ciascuno circoscritto per identifier e spesso per path, con commenti che spiegano la motivazione della dipendenza debole e del target di reflection — non sono una baseline generata in massa.
  • phpstan-strict.neon.dist eredita quella configurazione, porta il livello a max e congela l’elenco degli ignore, così nessuna nuova voce può essere aggiunta sotto il profilo strict.

Il versante normativo è diretto. Il motore deve produrre file che un reader possa navigare a partire dal trailer e dalla tabella di riferimenti incrociati secondo Spec: ISO 32000-2, §7.5.5 . Gli offset di byte esatti sono un problema di tipizzazione prima di essere un problema di serializzazione. Un offset è un intero che non deve mai diventare silenziosamente qualcos’altro. Un flusso che è pulito sui tipi a Level 10 ha già eliminato gran parte dei modi in cui l’aritmetica può andare storta in sordina.

La tipizzazione rigorosa è più visibile quando una regola di dominio è codificata come tipo anziché come controllo a runtime. Il discriminatore di conformità risponde alle domande a livello di specifica con un match esaustivo, così un caso non gestito è un errore di tipo, non un PDF errato:

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

Il @return 2|3|4|null non è semplice documentazione. Sotto treatPhpDocTypesAsCertain viene verificato. Un chiamante che presume che il risultato sia sempre un int viene avvisato in fase di analisi, prima ancora che venga scritto un singolo byte di un numero di parte PDF/A non conforme.

La trappola è leggere «nessuna baseline» come «il codice casualmente non contiene violazioni». È il contrario. L’assenza di una baseline è la causa, non un esito fortunato. Poiché non c’è alcun posto dove parcheggiare una violazione, il codice che ne produrrebbe una deve essere scritto in modo diverso. Level 10 senza baseline sul sorgente è un vincolo che plasma la progettazione, non una valutazione che la descrive a posteriori.

Un secondo equivoco è pensare che la manciata di voci ignoreErrors sia una baseline con un altro nome. Non lo sono. Una baseline è generata in massa e opaca. Queste voci sono scritte individualmente, circoscritte per identificatore, spiegate e protette da reportUnmatchedIgnoredErrors, così non possono diventare obsolete senza essere notate.

Questa pagina riguarda l’analisi del sorgente del motore. La test suite è analizzata in uno scope e con una configurazione separati e deliberatamente distinti; «nessuna baseline» qui è un’affermazione su src/, non un’asserzione che ogni analisi ausiliaria nel repository sia priva di baseline. PHPStan dimostra la solidità dei tipi, non la correttezza comportamentale. Non sostituisce la piramide di test, elimina soltanto una categoria di guasti che altrimenti i test dovrebbero inseguire. Il livello esatto, i flag e l’insieme di ignore sono accurati alla data di revisione di questa pagina. Le fonti autorevoli sono sempre phpstan.neon.dist e phpstan-strict.neon.dist nel repository core.

L’edizione non cambia questa disciplina. Ogni edizione è costruita dallo stesso sorgente Level 10:

Level 10 source analysis — edition availability
Edition Availability
Core Il sorgente Core è analizzato a Level 10 senza baseline sul sorgente.
Pro Pro è costruito sulla stessa disciplina del sorgente Level 10.
Enterprise Enterprise è costruito sulla stessa disciplina del sorgente Level 10.
  • PHPStan Level 10 — il livello di analisi più rigoroso, che tratta i valori non tipizzati e debolmente tipizzati come errori anziché come avvisi.
  • Baseline — un record generato delle violazioni esistenti che si chiede all’analizzatore di ignorare. NextPDF non ne usa alcuna per il sorgente del motore.
  • treatPhpDocTypesAsCertain — un’impostazione di PHPStan che tratta le annotazioni di tipo PHPDoc come fatti verificati, non come note consultive.
  • reportUnmatchedIgnoredErrors — un’impostazione che fa fallire la build quando una voce di ignore non corrisponde più a nulla, evitando soppressioni obsolete.
  • Pressione progettuale — l’effetto di un vincolo che obbliga a scrivere il codice in un determinato modo, in contrapposizione a un controllo che si limita a misurarlo.