嚴格型別,無所不在
Spec: ISO 32000-2, §7.5.5 ISO 32000-2 §7.5.5 Evidence: Code-backed PHPStan: Level 10, no src baseline
NextPDF 在引擎原始碼上以 Level 10 執行 PHPStan,且不使用任何抑制基準(baseline)。本頁說明「不使用基準」為何是一項設計決策,而非工具細節;也說明對一條唯一職責就是絕不誤處理資料的管線而言,這種嚴格究竟換來什麼。
為何這很重要
標題為「為何這很重要」的區段在大多數應用程式中,嚴格型別只是一種衛生習慣。放在 PDF 引擎裡,它更接近一種正確性機制。這個格式毫不寬容。讀取器預期從檔案的末端開始,透過 trailer 與交叉參照表(cross-reference table)來定位內容,因此寫入器的位元組偏移量必須精確無誤。想像一個悄悄放寬為 mixed 的型別、一個 int 無聲變成 string,或是一個可為 null 的值未經檢查就被解參考。這些情況中的任何一種,都可能產生一個在某個檢視器中能正常開啟、卻在數週後於另一個檢視器驗證失敗的檔案,而且沒有任何堆疊追蹤指回根本原因。
在這個領域裡,代價最高的失敗往往正是無聲無息的失敗。嚴格型別搭配嚴格的分析器,正是引擎將一整類無聲的執行階段失敗,轉換成會在建置階段明確失敗的錯誤的方式。
精簡版說明
標題為「精簡版說明」的區段- 引擎原始碼以 PHPStan Level 10(最嚴格的層級)進行分析,並在
phpstan.neon.dist中驗證。 - 並沒有使用原始碼抑制基準。該設定將原始碼分析鎖定在零錯誤。回歸(regression)會使建置失敗,而非被吸收進一個不斷膨脹的忽略檔案中。
- 現存的少數
ignoreErrors項目是範圍狹窄、受識別碼與路徑限定,且在設定中逐一說明理由的(跨套件軟相依邊界,以及反射目標的測試接縫)——而非一份批量基準。 - 另有一份獨立的嚴格設定檔以
level: max執行,並禁止新增任何忽略項目,因此新程式碼被要求遵守更嚴苛的標準。 - 預期效果是一種設計壓力:無法以型別誠實方式表達的程式碼無法通過,因此它會被重新設計,而非被抑制。
NextPDF 如何處理這件事
標題為「NextPDF 如何處理這件事」的區段「我們使用一個嚴格的分析器」與「我們使用一個不帶基準的嚴格分析器」之間的差別,正是整件事的關鍵,因此值得把話說清楚。
基準會記錄每一項現存的違規,並告訴分析器只忽略那些項目。在既有的舊程式碼基底上導入靜態分析時,這是一種務實的做法,但它有代價。基準會變成一份無聲的債務帳本,記下型別系統已同意不再檢視的部分。同一類型的新違規,可能會緊挨著那些被既往不咎的違規一同溜進來。分析器的承諾,從「這份程式碼在型別上是乾淨的」弱化為「這份程式碼不比原來更糟。」
對引擎原始碼而言,NextPDF 並不接受這樣的取捨。該設定將原始碼分析固定在零錯誤,並開啟 reportUnmatchedIgnoredErrors,因此即使是一項過時的抑制——已不再符合任何項目的抑制——也會使建置失敗。所保留的狹窄忽略項,皆限定於特定的錯誤識別碼與檔案。每一項都附帶行內說明,闡明為何該邊界是刻意為之(例如,core 針對某個它刻意不具體相依的 Pro/Enterprise 介面進行程式設計)。審查者可以逐一閱讀並加以判斷。沒有任何令人失去掌握的不透明清單。
維持這份誠實的流程如下:
- Change proposed New or modified engine code.
- Level 10 analysis Strictest PHPStan level over src/, treatPhpDocTypesAsCertain on.
- Zero-error gate No source baseline; unmatched ignores also fail.
- Strict profile level: max; no new ignore entries permitted.
- Redesign, not suppress If it cannot be expressed honestly, the design changes.
treatPhpDocTypesAsCertain 是其中的一環。PHPDoc 註解被視為基準事實,因此 @param list<T> 或 @return non-empty-string 並不是分析器可以客氣略過的註解。它是一份經過檢查的承諾。註解與執行階段型別會被強制保持一致。
證據怎麼說
標題為「證據怎麼說」的區段本頁屬於 Evidence: Code-backed 。設定本身就是證據:
phpstan.neon.dist設定了level: 10、phpVersion: 80400,分析src,且不含baseline:鍵——原始碼分析並沒有phpstan-baseline.neon。- 同一份檔案設定了
treatPhpDocTypesAsCertain: true與reportUnmatchedIgnoredErrors: true,並附上行內註解,說明 L10 原始碼分析鎖定在零錯誤,任何回歸都必須使 CI 失敗。 - 其餘的
ignoreErrors皆以identifier、且通常也以path為範圍,並附有註解說明軟相依與反射目標的理由——它們並非批量產生的基準。 phpstan-strict.neon.dist繼承該設定,將層級提升至max,並凍結忽略清單,因此在嚴格設定檔下不得新增任何項目。
從標準的角度看,這一點很直接。依據 Spec: ISO 32000-2, §7.5.5 ISO 32000-2 §7.5.5 ,引擎必須產生讀取器能從 trailer 與交叉參照表正確走訪的檔案。精確的位元組偏移量, 在成為序列化問題之前,先是一個型別問題。偏移量是一個絕不可無聲變成其他任何東西的整數。一條在 Level 10 達到型別乾淨的管線,已經消除了算術可能悄悄出錯的大多數途徑。
實務範例
標題為「實務範例」的區段當一條領域規則被編碼為型別,而非執行階段檢查時,嚴格型別的價值最為清楚。符合性判別器以窮盡的 match 回答規範層級的問題,因此未處理的情況會成為型別錯誤,而不是錯誤的 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, }; }}這個 @return 2|3|4|null 不是單純的文件說明。在
treatPhpDocTypesAsCertain 之下,它會接受檢查。若呼叫端假設結果永遠是 int,分析階段就會指出問題,早於任何不符規範的 PDF/A 部分編號被寫成位元組之前。
常見誤解
標題為「常見誤解」的區段陷阱在於把「不使用基準」解讀為「程式碼剛好沒有任何違規」。這正好說反了。沒有基準是原因,而非幸運的結果。因為沒有地方可以擱置違規,會產生違規的程式碼就必須換一種寫法。不帶原始碼基準的 Level 10 是一項形塑設計的約束,而非事後描述設計的成績單。
第二種誤解:認為那少數幾項 ignoreErrors 項目只是換個名字的基準。它們並非如此。基準是批量產生且不透明的。這些則是逐一撰寫、以識別碼為範圍、附有說明,並由 reportUnmatchedIgnoredErrors 把關,因此不會在無人察覺下腐化。
限制與邊界
標題為「限制與邊界」的區段本頁談的是引擎原始碼分析。測試套件是在一個獨立、刻意有別的範圍與設定下進行分析;此處的「不使用基準」是針對 src/ 的陳述,並非主張儲存庫中每一項輔助分析都沒有基準。PHPStan 證明的是型別健全性,而非行為正確性。它不會取代測試金字塔,只是移除了一類否則就得由測試追查的失敗。確切的層級、旗標與忽略集,皆以本頁的審閱日期為準。權威來源永遠是 core 儲存庫中的 phpstan.neon.dist 與 phpstan-strict.neon.dist。
版本不會改變這項紀律。每一個版本都建構自同一份 Level 10 原始碼:
| Edition | Availability |
|---|---|
| Core | Core 原始碼以 Level 10 進行分析,且不帶原始碼基準。 |
| Pro | Pro 建構於同一套 Level 10 原始碼紀律之上。 |
| Enterprise | Enterprise 建構於同一套 Level 10 原始碼紀律之上。 |
相關文件
標題為「相關文件」的區段- PHP 8.4 基礎 — 型別系統所倚賴的語言特性。
- 把錯誤當作一種特性 — 嚴格型別揭露的失敗會如何被處置。
- 管線模型 — 這項紀律所保護的架構。
詞彙表
標題為「詞彙表」的區段- PHPStan Level 10 — 最嚴格的分析層級,將未具型別與鬆散型別的值視為錯誤,而非警告。
- 基準(Baseline) — 一份由工具產生的現存違規記錄,用來告知分析器忽略這些項目。NextPDF 在引擎原始碼上完全不使用基準。
treatPhpDocTypesAsCertain— 一項 PHPStan 設定,將 PHPDoc 型別註解視為經過檢查的事實,而非僅供參考的註解。reportUnmatchedIgnoredErrors— 一項設定,一旦某個忽略項目不再符合任何項目,就使建置失敗,以防止過時的抑制。- 設計壓力 — 一種約束所產生的效果,它迫使程式碼以特定方式撰寫,而不只是像檢查那樣衡量它。