严格类型,无处不在
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— 一项设置,当某个忽略项目不再匹配任何项目时就使构建失败,以防止过时的抑制。- 设计压力 — 一种约束所产生的效果,它迫使代码以特定方式编写,而不只是像检查那样衡量代码。