Przejdź do głównej zawartości
getnextpdf.com

Ścisłe typy wszędzie

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

NextPDF uruchamia PHPStan na poziomie 10 dla źródła silnika, bez bazowego pliku wyłączeń. Ta strona wyjaśnia, dlaczego „brak bazowego pliku wyłączeń” jest decyzją projektową, a nie szczegółem narzędziowym, oraz co ta restrykcyjność rzeczywiście daje potokowi, którego zadaniem jest zapobieganie cichej, błędnej obsłudze danych.

W większości aplikacji ścisłe typowanie jest kwestią higieny. W silniku PDF pełni raczej rolę mechanizmu poprawności. Format nie wybacza błędów. Czytnik ma odnaleźć treść, odczytując plik od końca, przez trailer i tablicę odsyłaczy, więc przesunięcia bajtowe po stronie zapisu muszą być dokładne. Weźmy typ, który po cichu rozszerza się do mixed, wartość int, która po cichu staje się string, albo wartość dopuszczającą null, która jest dereferencjonowana bez sprawdzenia. Każdy z tych przypadków może doprowadzić do powstania pliku, który otwiera się bez problemu w jednej przeglądarce, a w innej nie przechodzi walidacji — po tygodniach i bez śladu stosu wskazującego na przyczynę.

W tej dziedzinie najbardziej kosztowne są ciche awarie. Ścisłe typowanie wraz z restrykcyjnym analizatorem to sposób, w jaki silnik przekształca pewną klasę cichych awarii w czasie wykonania w głośne awarie w czasie kompilacji.

  • Źródło silnika jest analizowane na poziomie 10 PHPStan — najbardziej restrykcyjnym poziomie — co jest zweryfikowane w phpstan.neon.dist.
  • Nie istnieje żaden bazowy plik wyłączeń dla źródła. Konfiguracja wymusza analizę źródła z zerową liczbą błędów. Regresja powoduje niepowodzenie kompilacji, zamiast zostać wchłonięta przez rosnący plik wyłączeń.
  • Nieliczne istniejące wpisy ignoreErrorswąskie, ograniczone do konkretnego identyfikatora i ścieżki oraz uzasadnione indywidualnie w konfiguracji (granice miękkich zależności między pakietami oraz szwy testowe na potrzeby refleksji) — a nie zbiorczym bazowym plikiem wyłączeń.
  • Osobny profil restrykcyjny uruchamia level: max i zabrania dodawania jakichkolwiek nowych wpisów wyłączeń, więc nowy kod jest utrzymywany według jeszcze ostrzejszego standardu.
  • Zamierzonym efektem jest presja projektowa: kod, którego nie da się uczciwie wyrazić w systemie typów, nie przechodzi, więc zostaje przeprojektowany zamiast wyciszony.

Różnica między „używamy restrykcyjnego analizatora” a „używamy restrykcyjnego analizatora bez bazowego pliku wyłączeń” jest sednem sprawy, więc warto być precyzyjnym.

Każdy bazowy plik wyłączeń rejestruje każde istniejące naruszenie i każe analizatorowi ignorować dokładnie te przypadki. To pragmatyczny sposób na wprowadzenie analizy statycznej w starszej bazie kodu, ale ma swoją cenę. Bazowy plik wyłączeń staje się cichą księgą długu, którego system typów zgodził się nie widzieć. Nowe naruszenia tego samego rodzaju mogą prześlizgnąć się obok już istniejących. Obietnica analizatora słabnie z „ten kod jest czysty pod względem typów” do „ten kod nie jest gorszy niż wcześniej”.

NextPDF nie idzie na ten kompromis w przypadku źródła silnika. Konfiguracja wymusza analizę źródła z zerową liczbą błędów i włącza reportUnmatchedIgnoredErrors, więc nawet nieaktualne wyłączenie — takie, które już niczego nie dopasowuje — powoduje niepowodzenie kompilacji. Pozostałe wąskie wyłączenia są ograniczone do konkretnego identyfikatora błędu i pliku. Każde z nich zawiera wbudowane wyjaśnienie, dlaczego dana granica jest celowa (na przykład core programowane względem interfejsu Pro/Enterprise, od którego świadomie nie zależy w sposób konkretny). Recenzent może przeczytać każde z nich i ocenić. Nie ma nieprzejrzystej listy, nad którą można by stracić kontrolę.

Przepływ, który utrzymuje to w ryzach:

  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.
Jak zmiana trafia do źródła silnika: zmiana nieuczciwa typowo nie może przejść przez bramkę, więc zostaje przeprojektowana zamiast wyciszona.

treatPhpDocTypesAsCertain jest częścią tego mechanizmu. Adnotacje PHPDoc są traktowane jako stan faktyczny, więc @param list<T> albo @return non-empty-string nie jest komentarzem, który analizator po prostu ignoruje. To zweryfikowana obietnica. Adnotacja i typ w czasie wykonania muszą być zgodne.

Ścisłe typowanie jest najbardziej widoczne tam, gdzie reguła dziedzinowa jest zakodowana jako typ, a nie jako sprawdzenie w czasie wykonania. Dyskryminator zgodności rozstrzyga kwestie na poziomie specyfikacji za pomocą wyczerpującego match, więc nieobsłużony przypadek jest błędem typu, a nie błędnym plikiem 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,
};
}
}

Adnotacja @return 2|3|4|null nie jest samą dokumentacją. Przy treatPhpDocTypesAsCertain podlega sprawdzeniu. Wywołujący, który zakłada, że wynik jest zawsze typu int, dowiaduje się o tym w czasie analizy, zanim zostanie zapisany choćby jeden bajt niezgodnego numeru części PDF/A.

Pułapka polega na odczytywaniu „braku bazowego pliku wyłączeń” jako „kod akurat nie ma naruszeń”. To odwrócone rozumowanie. Brak bazowego pliku wyłączeń jest przyczyną, a nie szczęśliwym rezultatem. Ponieważ nie ma gdzie odłożyć naruszenia, kod, który by je powodował, musi zostać napisany inaczej. Poziom 10 bez bazowego pliku wyłączeń dla źródła to ograniczenie kształtujące projekt, a nie świadectwo opisujące go po fakcie.

Drugie nieporozumienie polega na tym, że garstkę wpisów ignoreErrors uznaje się za bazowy plik wyłączeń pod inną nazwą. Nie są nim. Bazowy plik wyłączeń jest generowany zbiorczo i nieprzejrzysty. Te są pisane indywidualnie, ograniczone do konkretnego identyfikatora, wyjaśnione i zabezpieczone przez reportUnmatchedIgnoredErrors, więc nie mogą zdezaktualizować się niezauważenie.

Ta strona dotyczy analizy źródła silnika. Zestaw testów jest analizowany w osobnym, celowo wydzielonym zakresie i konfiguracji; „brak bazowego pliku wyłączeń” odnosi się tutaj do src/, a nie oznacza, że każda pomocnicza analiza w repozytorium jest pozbawiona bazowego pliku wyłączeń. PHPStan dowodzi poprawności typów, a nie poprawności zachowania. Nie zastępuje piramidy testów, a jedynie usuwa kategorię awarii, którą testy musiałyby w przeciwnym razie tropić. Dokładny poziom, flagi i zestaw wyłączeń są aktualne na dzień przeglądu tej strony. Miarodajnym źródłem są zawsze phpstan.neon.dist i phpstan-strict.neon.dist w repozytorium core.

Edycja produktu nie zmienia tej dyscypliny. Każda edycja jest budowana z tego samego źródła na poziomie 10:

Level 10 source analysis — edition availability
EditionAvailability
CoreŹródło Core jest analizowane na poziomie 10 bez bazowego pliku wyłączeń dla źródła.
ProPro jest zbudowane na tej samej dyscyplinie źródła na poziomie 10.
EnterpriseEnterprise jest zbudowane na tej samej dyscyplinie źródła na poziomie 10.
  • Poziom 10 PHPStan — najbardziej restrykcyjny poziom analizy, traktujący wartości nietypowane i luźno typowane jako błędy, a nie ostrzeżenia.
  • Bazowy plik wyłączeń (baseline) — wygenerowany rejestr istniejących naruszeń, które analizator ma ignorować. NextPDF nie używa żadnego dla źródła silnika.
  • treatPhpDocTypesAsCertain — ustawienie PHPStan, które traktuje adnotacje typów PHPDoc jako sprawdzane fakty, a nie komentarze o charakterze sugestii.
  • reportUnmatchedIgnoredErrors — ustawienie, które powoduje niepowodzenie kompilacji, gdy wpis wyłączenia nie dopasowuje już żadnego błędu, dzięki czemu wyłączenia nie dezaktualizują się po cichu.
  • Presja projektowa — efekt ograniczenia, które wymusza pisanie kodu w określony sposób, w przeciwieństwie do sprawdzenia, które jedynie go mierzy.