コンテンツにスキップ

厳格な型付けを、あらゆる場所に

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

NextPDF は、抑制ベースラインを持たずに、エンジンのソース全体に対して PHPStan を Level 10 で実行します。このページでは、「ベースラインなし」がツール設定上の細部ではなく設計上の決定である理由と、その厳格さが、データを誤って扱わないことを唯一の責務とするパイプラインに実際に何をもたらすのかを説明します。

ほとんどのアプリケーションにおいて、厳格な型付けはコード衛生の一部です。PDF エンジンにおいては、正しさを支えるメカニズムにより近いものです。このフォーマットには誤差を許す余地がありません。リーダーは、ファイルを末尾から読み、トレーラーとクロスリファレンステーブルを通じてコンテンツを特定することが想定されているため、ライターのバイトオフセットは正確でなければなりません。ひそかに mixed へ広がる型、int が暗黙のうちに string になってしまうケース、チェックされずにデリファレンスされる nullable を考えてみてください。これらのいずれも、あるビューアでは問題なく開けるのに、別のビューアでは検証に失敗するファイルを生み出しかねません。しかもそれは数週間後に、原因を指し示すスタックトレースもないまま起こります。

この領域で代償が大きい失敗は、暗黙のうちに起こるものです。厳格な型付けと厳格なアナライザーの組み合わせは、暗黙の実行時失敗の一群を、明白なビルド時の失敗へと変換するためのエンジン側の手段です。

  • エンジンのソースは、最も厳格なレベルである PHPStan Level 10 で解析され、phpstan.neon.dist によって検証されます。
  • ソース抑制ベースラインは存在しません。この設定は、ソース解析をエラーゼロの状態に固定します。リグレッションは、肥大化していく ignore ファイルに吸収されるのではなく、ビルドを失敗させます。
  • 残っているわずかな ignoreErrors エントリは、設定内で 狭く、識別子およびパスでスコープ指定され、個別に根拠が示されている ものであり(パッケージ間のソフト依存境界と、リフレクションを対象とするテストの接合部)、一括生成のベースラインではありません。
  • 別個の strict プロファイルlevel: max で実行され、新たな ignore エントリを一切禁止するため、新しいコードにはさらに厳しい基準が適用されます。
  • 狙いは設計上の圧力をかけることです。型に対して正直に表現できないコードは通過しないため、抑制されるのではなく再設計されます。

「厳格なアナライザーを使っている」と「ベースラインなしの厳格なアナライザーを使っている」の違いこそが要点なので、ここは正確に述べておく価値があります。

ベースライン は、既存のすべての違反を記録し、アナライザーにそれらだけを無視するよう指示します。これはレガシーなコードベースに静的解析を導入するための現実的な手段ですが、代償を伴います。ベースラインは、型システムが見ないことに同意した負債を、ひそかに記録する台帳になります。同じ種類の新しい違反が、既存扱いとされた違反の隣に紛れ込むことがあります。アナライザーの約束は、「このコードは型的にクリーンである」から「このコードは以前より悪くなってはいない」へと弱まります。

NextPDF は、エンジンのソースについてはそのトレードオフを受け入れません。この設定はソース解析をエラーゼロに固定し、reportUnmatchedIgnoredErrors を有効にするため、もはや何にも一致しない 陳腐化した 抑制でさえビルドを失敗させます。残る狭い ignore は、特定のエラー識別子とファイルにスコープが限定されています。それぞれには、その境界が意図的である理由のインライン説明が付されています(たとえば、Core が、具体的な依存を意図的に持たない Pro/Enterprise インターフェースに対してプログラミングしているケース)。レビュアーはそれぞれを読み、判断できます。見失ってしまうような不透明なリストは存在しません。

これを正直に保つ流れです:

  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.
変更がエンジンのソースに到達するまでの流れ。型に対して不正直な変更はゲートを通過できないため、抑制されるのではなく再設計されます。

treatPhpDocTypesAsCertain はその一部です。PHPDoc アノテーションは確かな事実として扱われるため、@param list<T>@return non-empty-string は、アナライザーが丁重に無視するコメントではありません。チェック対象の約束です。アノテーションと実行時の型は、一致することが強制されます。

このページは Evidence: Code-backed です。設定そのものが証拠です:

  • phpstan.neon.distlevel: 10phpVersion: 80400 を設定し、src を解析対象にしており、baseline: キーを持ちません — ソース解析用の phpstan-baseline.neon は存在しません。
  • 同じファイルは treatPhpDocTypesAsCertain: truereportUnmatchedIgnoredErrors: true を設定し、L10 ソース解析はエラーゼロに固定されており、いかなるリグレッションも CI を失敗させなければならないことをインラインで注記しています。
  • 残る ignoreErrors は、それぞれ identifier によって、多くの場合 path によってスコープ指定され、ソフト依存とリフレクション対象の根拠を説明するコメントが付されています。これらは一括生成のベースラインではありません。
  • phpstan-strict.neon.dist はその設定を継承し、レベルを max まで引き上げ、ignore リストを凍結することで、strict プロファイルの下では 新たなエントリを一切 追加できないようにします。

標準仕様の観点からは話は単純です。エンジンは、リーダーがトレーラーとクロスリファレンステーブルからたどれるファイルを生成しなければなりません。これは Spec: ISO 32000-2, §7.5.5 に準拠したものでなければなりません。正確なバイトオフセットは、 シリアライズの問題である以前に、型付けの問題です。オフセットは、 決して暗黙のうちに別の何かになってはならない整数です。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 パート番号が 1 バイトでも書き込まれる前に、解析の時点でその旨を伝えられます。

落とし穴は、「ベースラインなし」を「コードにたまたま違反が存在しない」と読み取ってしまうことです。実際には逆です。ベースラインが存在しないことは、幸運な結果ではなく 原因 です。違反を置いておく場所がどこにもないため、違反を生むようなコードは、別の書き方をしなければなりません。ソースベースラインを持たない Level 10 は、事後にそれを記述する成績表ではなく、設計を形づくる制約です。

二つ目の誤解は、わずかな ignoreErrors エントリが、名前を変えただけのベースラインであるというものです。そうではありません。ベースラインは一括生成され、不透明です。これらは個別に記述され、識別子でスコープ指定され、説明が付され、reportUnmatchedIgnoredErrors によって保護されているため、気づかれないまま陳腐化することはありません。

このページは エンジンソース の解析について述べています。テストスイートは、意図的に区別された別個のスコープと設定の下で解析されます。ここでの「ベースラインなし」は src/ についての言明であり、リポジトリ内のすべての補助的な解析がベースラインなしであるという主張ではありません。PHPStan が証明するのは型の健全性であり、振る舞いの正しさではありません。これはテストピラミッドを置き換えるものではなく、そうでなければテストが追いかけなければならなかった失敗の一カテゴリを取り除くだけです。正確なレベル、フラグ、ignore セットは、このページのレビュー日時点で正確です。正式な出典は常に、core リポジトリ内の phpstan.neon.distphpstan-strict.neon.dist です。

エディションがこの規律を変えることはありません。あらゆるエディションは、同じ Level 10 のソースから構築されています:

Level 10 source analysis — edition availability
Edition Availability
Core Core のソースは、ソースベースラインを持たずに Level 10 で解析されます。
Pro Pro は、同じ Level 10 のソース規律の上に構築されています。
Enterprise Enterprise は、同じ Level 10 のソース規律の上に構築されています。
  • PHPStan Level 10 — 最も厳格な解析レベルであり、型のない値や緩く型付けされた値を、警告ではなくエラーとして扱う。
  • ベースライン — アナライザーに無視するよう指示する、既存の違反の生成された記録。NextPDF はエンジンのソースに対して一切使用しません。
  • treatPhpDocTypesAsCertain — PHPDoc の型アノテーションを、助言的なコメントではなくチェック済みの事実として扱う PHPStan の設定。
  • reportUnmatchedIgnoredErrors — ignore エントリがもはや何にも一致しなくなったときにビルドを失敗させ、陳腐化した抑制を防ぐ設定。
  • 設計上の圧力 — コードを単に測定するだけのチェックとは異なり、特定の書き方を強制する制約の効果。