Przejdź do głównej zawartości

Ścisłe typy wszędzie

Spec: ISO 32000-2, §7.5.5 Evidence: Code-backed PHPStan: 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 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.
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.

Ta strona jest Evidence: Code-backed . Dowodem jest sama konfiguracja:

  • phpstan.neon.dist ustawia level: 10, phpVersion: 80400, analizuje src i nie zawiera klucza baseline: — dla analizy źródła nie istnieje phpstan-baseline.neon.
  • Ten sam plik ustawia treatPhpDocTypesAsCertain: true oraz reportUnmatchedIgnoredErrors: true, z wbudowaną adnotacją, że analiza źródła na L10 jest zablokowana na zerową liczbę błędów, a każda regresja musi powodować niepowodzenie CI.
  • Pozostałe ignoreErrors są ograniczone przez identifier, a często także path, z komentarzami wyjaśniającymi uzasadnienie miękkiej zależności oraz użycia refleksji — nie są zbiorczo wygenerowanym bazowym plikiem wyłączeń.
  • phpstan-strict.neon.dist dziedziczy tę konfigurację, podnosi poziom do max i zamraża listę wyłączeń, tak aby w profilu restrykcyjnym nie można było dodać żadnego nowego wpisu.

Związek ze standardami jest bezpośredni. Silnik musi wytwarzać pliki, które czytnik może przetwarzać, zaczynając od trailera i tablicy odsyłaczy, zgodnie z Spec: ISO 32000-2, §7.5.5 . Dokładne przesunięcia bajtowe są problemem typów, zanim staną się problemem serializacji. Przesunięcie to liczba całkowita, która nigdy nie może po cichu stać się czymkolwiek innym. Potok, który jest czysty pod względem typów na poziomie 10, usunął już większość scenariuszy, w których arytmetyka może po cichu zawieść.

Ś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
Edition Availability
Core Źródło Core jest analizowane na poziomie 10 bez bazowego pliku wyłączeń dla źródła.
Pro Pro jest zbudowane na tej samej dyscyplinie źródła na poziomie 10.
Enterprise Enterprise 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.