コンテンツにスキップ

付録 — PHPStanから来た場合

PHPStanは、Rigorが他の言語で持つ最も近い精神的なピアツールである。両ツールは同じ優先事項を共有する: アナライザーが特徴付けられないコードには沈黙し、必須アノテーションよりも推論に依拠し、高信頼の診断の小さなカタログを示す。Rigorの多くの設計選択 — 設定ファイルの形状、ベースライン(baseline)diff、深刻度プロファイル、assert系ディレクティブ — はPHPStanから直接影響を受けた。

PHPStanを使ったことがあれば、メンタルモデルはほぼそのまま持ち越せる。この付録は語彙をマッピングする。

この付録の内容 5秒ピッチ · 型語彙マッピング · @phpstan-assertファミリー · Type-Specifying Extensions ↔ プラグイン · 設定 · スタブ ↔ RBS · 深刻度プロファイルvsレベル · 「アノテーション不要」 · PHPStanにあってRigorにないもの · RigorにあってPHPStanにないもの · マイグレーションvignette

問いPHPStanRigor
アノテーションはどこに書くか?PHPDocの/** ... */ブロック.rbの隣の.rbsファイル
デフォルト動作推論、見通せなければ沈黙推論、見通せなければ沈黙
「レベル」0 – 10(数値)lenient/balanced/strict(名前付き)
ルールごとの制御ignoreErrors:正規表現、レベル降格disable:severity_overrides:
ベースラインphpstan-baseline.neonrigor.baseline.json
スタブ形式PHPスタブファイルRBSファイル
カスタムナローイング(narrowing)Type-Specifying Extensionsプラグイン(第9章)
カスタム戻り値シェイプ(shape)Dynamic Return Type Extensionsプラグインのflow_contribution_for

ふたつのツールは基礎的な決定のほとんどで一致している。最大の違いはサーフェス(surface) — Rubyの構文とランタイムシェイプ — であり、哲学ではない。

PHPStanとRigorには重複するリファインメント(refinement、篩型とも)語彙がある — これはどのピアツールとも最も近いマッチだ。

PHPStanのPHPDocRigorの表現備考
stringString
intInteger
floatFloat
boolbool(`ConstantConstant`)
nullConstant<nil>Rubyにはnilのみ。
mixedTop「何でもあり」キャリア(carrier)。
neverBot空の型。
voidvoid同様。
array<T> / T[]Array[T]
array<K, V>Hash[K, V]Rubyはコンテナの種類で分ける。
array{name: string, age: int}HashShape{name: String, age: Integer}同じキーごとのモデル。
array{0: int, 1: string}(リストシェイプ)Tuple[Integer, String]同じ位置ごとのモデル。
non-empty-stringnon-empty-string名前と意味が同一
non-falsy-stringnon-empty-stringRigorはfalsy-but-nonemptyのケースを分けない。
numeric-stringnumeric-string同一。
lowercase-stringlowercase-string同一。
class-stringSingleton[T]等価なシェイプ。
int<1, 9>int<1, 9>構文が同一
positive-intpositive-int同一。
negative-intnegative-int同一。
non-zero-intnon-zero-int同一。
non-empty-array<T>non-empty-array[T]同一。
non-empty-list<T>(独立したキャリアなし — non-empty-array[T]でカバー)Rubyにはlist/dict分割がない。
`TU``T
T & UIntersection[T, U]
literal-stringliteral-string概念が同一。ソースコードのリテラルから構築されたことが証明可能。
'hello'(リテラル型)Constant<"hello">
42(リテラル型)Constant<42>

このテーブルは付録の中で最も密度が高い — 重複がとても近いから。PHPStanの「PHPDoc Types」ページを別のタブで開いているなら、ほぼすべての高度なリファインメントが転用できる。

PHPStanのアサーションナローイングPHPDocタグは、RigorのRBS::Extendedディレクティブ文法に直接対応する。第7章でテーブルを詳しく説明しているが、参照のためここに再掲する:

PHPStanのPHPDocRigorのRBS::Extended効果
@phpstan-assert T $x%a{rigor:v1:assert: x is T}returnの後、呼び出し元のxT
@phpstan-assert-if-true T $x%a{rigor:v1:predicate-if-true: x is T}メソッドがtruthyを返せば、呼び出し元のxT
@phpstan-assert-if-false T $x%a{rigor:v1:predicate-if-false: x is T}メソッドがfalseyを返せば、呼び出し元のxT
@phpstan-assert !T $x%a{rigor:v1:assert: x is ~T}returnの後、呼び出し元のxTではない
@phpstan-assert =T $x(assert-and-narrow)assert:でカバー)同じ効果。
@phpstan-self-out T%a{rigor:v1:assert: self is T}selfが呼び出し元スコープでナローイングされる。
@phpstan-impure(対応なし)Rigorはfold-through-method-callの純粋性をまだモデル化していない。

Rigorの文法が提供するすべてのディレクティブにはPHPStanのPHPDoc対応物がある。「このメソッドがreturnした後に何が何をナローイングするか」についてPHPStanのメンタルモデルがあれば、そのまま転用できる。

Type-Specifying Extensions ↔ プラグイン

Section titled “Type-Specifying Extensions ↔ プラグイン”

アサーションが呼び出しシェイプで認識される場合 — PHPStanのTypeSpecifyingExtensionインターフェースで、フレームワークがインスタンス化して「この呼び出しが与えられたとき、どのナローイングが生じるか?」と問うクラスを書く — Rigorの対応物はプラグインの#flow_contribution_for#diagnostics_for_fileフックとエンジンのpost_return_facts基盤。

PHPStanの拡張型Rigorの対応物
MethodTypeSpecifyingExtensionflow_contribution_forから返されるプラグインのFact(target_kind: :parameter)
StaticMethodTypeSpecifyingExtensionFact(target_kind: :receiver-class)付きで同様
FunctionTypeSpecifyingExtensionFact(target_kind: :argument)付きで同様
DynamicMethodReturnTypeExtensionプラグインのflow_contribution_for(call_node:, scope:)
DynamicStaticMethodReturnTypeExtensionプラグインコード内でreceiver-classブランチによって変化する、同様
DynamicFunctionReturnTypeExtensionモジュールレベルメソッドに対して同様

docs/internal-spec/plugin.mdに固定されたプラグイン契約(contract)は、PHPStanの拡張APIがカバーするすべてのシェイプを与える。同様のライフサイクル(マニフェスト宣言、呼び出しごとのディスパッチ、ファクト(fact)発行)を持つ。第9章には高レベルの方向性がある。内部仕様が拘束力のある契約。

第10章のrigor-sorbetアダプタは「スケールでのType-Specifying Extension」の作業例そのもの — T.mustT.castT.bindT.assert_type!のすべての呼び出しはsigではなく呼び出しシェイプで認識される。

PHPStanのphpstan.neonとRigorの.rigor.yml/.rigor.dist.ymlは同じ形状を使う: プロジェクトルートの単一の設定ファイル、存在する場合は自動ロード、paths:、深刻度コントロール、includesを持つ。

PHPStanRigor
phpstan.neon.rigor.yml
phpstan.neon.dist.rigor.dist.yml
paths:paths:
level:severity_profile:
excludePaths:(現時点で対応なし — パスは明示的にリストする)
ignoreErrors:(正規表現/パターン)disable:(ルール識別子またはワイルドカード)
parameters: ignoreErrors:パスごとファイルヘッドの# rigor:disable-file <rule>
includes:includes:
phpstan-baseline.neonrigor.baseline.json
phpstan analyse --generate-baselinerigor check --format=json > rigor.baseline.json
phpstan analyserigor check
phpstan analyse --baselinerigor diff rigor.baseline.json
パス解決: 宣言ファイルからの相対パス解決: 宣言ファイルからの相対(同じルール)。

ベースラインのワークフローは同一。第8章にウォークスルーがある。

includes:のセマンティクスもPHPStanのものと一致する: 宣言順、後のものが前のものをオーバーライドし、現在のファイルのキーはincludeされたファイルより優先される。RigorのHe.rigor.yml.rigor.dist.ymlと自動マージしない — オーバーライドはincludes:の下にdistファイルを明示的にリストしなければならない。PHPStanもphpstan.neonphpstan.neon.distの両方がある場合に同じ動作をする。

PHPStanはPHPDocを提供しないライブラリにはPHPスタブファイル(.stub)を読む。Rigorは同じ目的のために.rbsファイルを読む。ディスパッチは類似している — 両ツールとも「スタブ宣言された契約が推論ボディより優先」をレイヤーリングする — そして両方ともPHPDoc/RBS::Extendedアノテーション経由でリファインメントを付与するための正統な場所としてスタブファイルを使う。

PHPStanRigor
*.stubファイルsig/(プロジェクト)とrbs_collection.lock.yaml(サードパーティ)の.rbsファイル
スタブのPHPDocRBS::Extended%a{rigor:v1:...}アノテーション
#[Override] / #[\Deprecated]属性RBSのattr_*def宣言
phpstan/extension-installerプラグインgemのBundler + Gemfile

両方の世界で機能する実践的なパターン: スタブ/RBSファイルをパブリック契約の権威として保持し、スタブの隣に@phpstan-*/RBS::Extendedディレクティブを使ってプロジェクト固有の締め付けをレイヤーリングする。

深刻度プロファイルvs PHPStanレベル

Section titled “深刻度プロファイルvs PHPStanレベル”

PHPStanのレベルは数値のはしご(0 = 「形状のみ」、10 = 「最も厳格」)。Rigorのプロファイルは名前付き(lenientbalancedstrict)。

PHPStanのレベルRigorのプロファイル(大まかな対応)備考
0 – 2lenientほとんどのルール → :warning。不確かなルールは:infoに下げる。
3 – 6balanced(デフォルト)ほとんどのルール → :error
7 – 10strictすべて → :errorbalancedでの:warningルールも含む。

マッピングは近似 — ルールセットが1:1ではない — が実践的なアドバイスは同じ: デフォルトから始めて時間をかけて締め付けていく。第8章の「有用なワークフロー」はPHPStanのオンボーディングパターンと合致する。

「アノテーション不要」— そうだが、スタブは必要

Section titled “「アノテーション不要」— そうだが、スタブは必要”

PHPStanとRigorは推論が重い仕事をするという哲学を共有する。すべての変数をアノテートするのではなく、境界(関数シグネチャ、ライブラリスタブ)をアノテートし、推論が内側に伝播する。

PHPStanの問題はPHPDocがPHPソースと同じファイルに存在すること。Rigorの問題はRBSがsig/という並行ツリーに存在すること。トレードオフは知られている:

  • 同一ファイルのPHPDocは文書を記述するコードの隣に置く — 更新しやすく、忘れにくい。
  • 並行する.rbsは型を気にしない開発者のためにランタイムソースをきれいに保つ — 本番メソッドへのPHPDocの混入なし。

Rigorは文化的理由(Rubyのコンパクトなソースの伝統)から並行ファイルモデルに傾いているが、RBS::InlineはPHPDocスタイルの隣接性を好むプロジェクトのためのファイル内代替を提供する。根拠はADR-1参照。

  • スタブライブラリ全体にわたるバウンド付き制約のジェネリクス。PHPStanのジェネリクスエコシステムはより成熟している。RBSのジェネリクスは存在するが標準ライブラリのカバレッジはより断片的。
  • @phpstan-impureとpure-by-defaultモデリング。Rigorは組み込みのdata/builtins/ruby_core/ YAMLでメソッドごとの純粋性をカタログ化しているが、fold-throughのためにメソッドを純粋と宣言するユーザー向けの手段をまだ公開していない。
  • カスタムルール。PHPStanのRuleインターフェースはASTパターンで発火するルールをPHPで書かせる。Rigorのプラグインサーフェスは#diagnostics_for_file経由で診断の発行をカバーするが、ルールの形状はPHPStanのフレームワークほど洗練されていない。
  • treatPhpDocTypesAsCertain。PHPStanの「PHPDocを信頼する」ノブにRigorの対応物はない — RigorはRBS宣言を常に権威として信頼する。
  • メソッド呼び出しを通じた定数folding。PHPStanは定数伝播をいくらか行う。Rigorはカタログ化されたビルトイン(NumericStringSymbolArrayHash)を通じて積極的にfoldする。
  • Rubyの述語メソッドに対するファーストクラスのフローセンシティブ(flow-sensitive)なナローイングs.empty?/n.zero?/n.positive?等は名前で認識され、それに応じてナローイングされる。PHPStanはType-Specifying Extensionsで同じアイデアを持つが、Rigorはカタログを標準で提供する。
  • literal-stringキャリア。両ツールともこの概念を持つが、Rigorのキャリアは補間を通じて合成される — "#{a}#{b}"abの両方がliteral-stringならliteral-stringになる。PHPStanには「この位置でのリテラル」としてのliteral-stringがあるが伝播ルールが異なる。
  • Sorbet入力アダプタ。プロジェクトが部分的にSorbet(いくつかのファイルはRBSに移行したが残りはそのまま)の場合、Rigorは両ソースを並行して読む。PHPStanには対応するものがない — 「PHPのSorbet」に相当するものはない。

PHPStanで締め付けられたライブラリをRubyに移植している。元のPHP:

class Slug {
/**
* @phpstan-param non-empty-string $name
* @phpstan-return non-empty-lowercase-string
*/
public function normalise(string $name): string {
return strtolower(preg_replace('/\s+/', '-', $name));
}
/**
* @phpstan-assert non-empty-string $value
*/
public function assertNotEmpty(string $value): void {
if ($value === '') throw new InvalidArgumentException();
}
}

Rigorの移植 — Rubyソースは慣用的なまま変更なし、境界のRBS:

lib/slug.rb
class Slug
def normalise(name)
name.downcase.gsub(/\s+/, "-")
end
def assert_not_empty(value)
raise ArgumentError if value.empty?
end
end
sig/slug.rbs
class Slug
%a{rigor:v1:param: name is non-empty-string}
%a{rigor:v1:return: non-empty-lowercase-string}
def normalise: (String name) -> String
%a{rigor:v1:assert: value is non-empty-string}
def assert_not_empty: (String value) -> void
end

ディレクティブ文法は構造的に変換そのもの: すべてのPHPStanの@phpstan-*.rbsファイルの対応するdef行の%a{rigor:v1:...}アノテーションになる。

この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:

他のツールと比較したい場合は、兄弟付録ページがTypeScriptmypySteepTypeProfをカバーしている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.