コンテンツにスキップ

22ライブラリOSSサーベイ — 繰り返される偽陽性クラスタ + 着地したBigDecimal-coerce修正

日付。2026-05-18 → 2026-05-19。サーベイはe44cfee;修正はacc9882OverloadSelector: receiver-affinity pre-sort + Acceptance ancestor fallback)で着地。

スコープ。rigorリポジトリ外(~/repo/ruby/rigor-survey/)にクローンされrigor checkで解析された22の広く使われているOSS Ruby gem。目標: rigor自身のセルフチェックコーパスでは再現しない繰り返される偽陽性クラスタを特定し、ユーザー可視のインパクトでランク付けし、回帰カバレッジ付きで少なくとも1つの具体的な修正をエンドツーエンドで着地させる。

結果。3ラウンドのサーベイ(ラウンド1: 11の汎用ライブラリ;ラウンド2: 11のテンプレート / シリアライゼーションライブラリ;ラウンド3: 修正着地)。ファミリー3(BigDecimal誤推論)はコーパス全体で完全に消失 — 7ライブラリで25回 → 0 — 7ファイルの変更を通じて(2 lib + 1新モジュール + 2 spec + CHANGELOG + CURRENT_WORK)。他の5つの診断クラスタはキューのまま;このノートはサーベイ方法論とライブラリごとの結果を記録し、将来の実装者が同じデータを手元に持って次のスライスを選べるようにする。

コンパニオン成果物。ライブラリごとの生rigor check出力とクローン作業ツリーは、このリポジトリ外の~/repo/ruby/rigor-survey/_reports/<lib>.txtに保持される(チェックインされていない — クローンは大きすぎ、診断は下記§6のレシピから再現可能)。

ライブラリファイルメモリエラー警告備考
rgl281.0秒296 MB20ミックスインメソッドがObjectとして解決される
algorithms141.5秒314 MB5311ツリーコンテナ: nilナローイング + 数値推論
faraday331.3秒320 MB187クラスメソッドナローイング + nilレシーバークラスタ
rbnacl371.2秒300 MB01コーパス中最もクリーンな結果
protobuf (ruby)241.1秒365 MB160Numeric#to_i / Struct.newディスパッチバグ
parser561.4秒309 MB115<< for BigDecimal(Integer→BigDecimal誤推論)
rubocop-ast991.2秒326 MB43パターンDSLヘルパーがObjectとして見える
concurrent-ruby1781.4秒320 MB127Promises::Future#fulfillnilナローイングで失われる
kramdown551.5秒327 MB424nil上のel.type / el.optionsチェーン(10+10+7+6)
mail1112.5秒437 MB920literal predicate is always falsey ×11(ノイズ)
net-ssh971.3秒339 MB2822condition is always falsey ×10;未使用ローカル
合計73219580

すべての実行はライブラリあたり2.5秒以内に完了。最大ターゲット(mail、111ファイル)でもメモリは440 MB以下にとどまった。

algorithmsに対する最初の呼び出しで、すべてのファイルが次を生成した:

error: internal analyzer error: NoMethodError: undefined method 'try_static_refinement' for module Rigor::Inference::MethodDispatcher

その後の--clear-cache実行は、内部エラーなしで53の通常の診断を生成した。したがってバグは、このセッションでの先行するrigor check実行からの一時的なウォームキャッシュ状態に依存する。追跡する価値がある理由:

  1. ユーザーはリファクタ後の最初のアナライザー呼び出しでこれに当たる。
  2. メッセージ自体がプログラミングエラー(タイプミス / 定義漏れ) — MethodDispatcher.try_static_refinementルックアップが何らかのコードパスから到達可能;メソッドが未定義か、定義されるべきか。

アクション: コードベースをtry_static_refinementの呼び出し元でgrepする。トリガー条件は: キャッシュミス + プラグイン駆動のディスパッチャーエントリー。内部仕様の推論エンジンドキュメント契約もこれを列挙すべき。

3. 繰り返される偽陽性 / 改善クラスタ

Section titled “3. 繰り返される偽陽性 / 改善クラスタ”

これらは複数のライブラリにまたがって現れる — ランクは合計出現数で、おおよそ「ユーザーが自分のコードベースでどれだけのノイズを見るか」に比例する。

3a. BigDecimalとしての数値リテラル誤推論(最優先)

Section titled “3a. BigDecimalとしての数値リテラル誤推論(最優先)”
ライブラリメッセージ回数
algorithmsundefined method 'upto' for BigDecimal1
parserundefined method '<<' for BigDecimal3
kramdownundefined method 'times' for BigDecimal1
protobufundefined method 'to_i/to_f' for Numeric12

これらはユーザーが書いたBigDecimal算術ではない。呼び出しサイトを読むと(例: algorithms/lib/algorithms/sort.rb:70(arr.length - 1).upto(...))、レシーバーはInteger算術の結果。推論はIntegerNumericに広げ、それからBigDecimalupto/<<定義のない最も制限的なNumeric部分型)に間違ってナローイングしているように見える。

おそらくの根本原因: Integer - Integerが何らかのパスでNumericを返し、ユニオン射影が間違ったアームを選ぶ。ExpressionTyperで検証 — 最近のコミットe44cfeeはすでに__FILE__/__LINE__を絞り込んでいる;リテラル上の算術も同じ精度に値する。

3b. ミックスイン提供のメソッドがObject / Classとして解決される

Section titled “3b. ミックスイン提供のメソッドがObject / Classとして解決される”
ライブラリ
rglObject上のcycles_with_vertexremove_vertex
faradayClass上のoptions_formember_set
faradayObject/URI上のmerge!updatefind_proxy
rubocop-astObject/Binding上のcompile_termsunion_bind

パターン: モジュールがincludeされる(またはクラスレベルでextendされる)が、そのメソッドはディスパッチ中にレシーバーのメソッドテーブルに追加されない。rglでは、影響を受けるメソッドはMutable#each_vertexが呼び出し元にcycles_with_vertexremove_vertexの両方を提供することを期待するミックスインパターンに由来する。ScopeIndexerはinclude / extend / prepend解決に対して確認すべき。

これはまた、ユーザーが「Rigorはミックスインを理解しない」と最もよく誤読する症状でもある。焦点を絞った修正 + ハンドブックでの呼び出しの価値がある。

3c. パターンガードを通じて収束しないnilナローイング

Section titled “3c. パターンガードを通じて収束しないnilナローイング”
ライブラリクラスタ回数
kramdownnil上のel.type/el.options/el.children/el.value≥33
algorithmsnil上のnode.key/.left/.right/.value≥45
net-sshnil上のcall/close/shutdown≥12
concurrentnil上のfulfillexecutorresolved?≥5

これらは絶対エラー数を支配するが、多くは真陽性の可能性が高い — ツリーアルゴリズムは浅いチェックの後に真にnode.leftを参照解除する。問題は出力でそれらがすべて同じに見えること。2つの改善:

  1. nilレシーバー診断を単一のロールアップ下にグループ化し、ユーザーがalgorithms/lib/containers/splay_tree_map.rbを見たときに20の別々の行ではなく「node上の20のnilレシーバーエラー」を見るように。
  2. 一般的なイディオムを尊重する: return unless node / node or return / node && node.leftは帰結内でナローイングすべき。

splay_tree_map.rb:156(上記§3cで引用)をスポットチェックすると、ガードが使用から多くの行上にある深くネストされたメソッドが見える — これは明示的なアノテーションなしにフローナローイングが維持できるエッジ。

3d. condition is always falsey/truthyノイズ

Section titled “3d. condition is always falsey/truthyノイズ”
ライブラリ回数
net-ssh10
faraday6
parser5
concurrent5
kramdown2
rubocop-ast2

多くは§3a/§3cの下流 — レシーバー型が間違うと、囲むif/unlessは定数に畳まれる。3a/3cを修正することでこのカテゴリーは機械的に減る。残りの真陽性(デッドブランチ)は価値があるが、偽陽性の中で溺れやすい。

3e. Mail::Messageliteral predicate is always falsey ×11

Section titled “3e. Mail::Messageのliteral predicate is always falsey ×11”

すべてmail/lib/mail/message.rb内。スポットチェックするとif @raw_source.blank?のような述語であることが分かり、Rigorが@raw_sourceに対して非blank形を推論した。パターンは11回同一 — おそらくすべてのgetterに波及する単一のコンストラクタ側の過剰ナローイング。

3f. Struct.new / Class.newディスパッチ

Section titled “3f. Struct.new / Class.newディスパッチ”
  • protobuf: wrong number of arguments to 'new' on Struct (given 0, expected 1..Infinity)
  • concurrent-ruby: wrong number of arguments to 'new' on Class (given 1/2, expected 0)

これらはおそらくStruct.new(:a, :b)Class.new(SuperClass) { ... }形。両方ともRBSで定義済みのシグネチャを持つが、RigorはObject#newにフォールバックする。単一のディスパッチャーパッチ(Struct + Classメタメソッド)の価値がある。

3g. インスタンス変数型乖離ノイズ

Section titled “3g. インスタンス変数型乖離ノイズ”

algorithmsmailnet-sshparserrbnaclconcurrent-rubyrubocop-astkramdownにまたがる — パターン:

instance variable '@X' on Klass was previously assigned NilClass; this write assigns ConcreteType

これは正準なRuby: def initialize; @x = nil; endしてから後で@x = build!。診断は真の型シフトを捕えるが、パターンがほぼ普遍的なので、大量の低シグナルノイズを生成する。3つのオプション:

  1. 唯一の事前代入がinitialize内のnilで、型ユニオンが正確にNilClass | ConcreteTypeのときに抑制。
  2. デフォルトの:hint深刻度を持つ別個の診断ファミリーに昇格。
  3. そのままにし、抑制マーカーを目立つように文書化。
  • すべての11実行が印刷、同じ.rigor.yml:1:1: info: 24 gem(s) in Gemfile.lock have no RBS available — これはRigorリポジトリのGemfileで、ターゲットのものではない。チェックはRigorのcwdから実行された。次のいずれかの価値がある:
    • 「ターゲットがcwd外」を自動検出し、cwd相対のGemfileアドバイスを抑制、または
    • 代わりにターゲット相対のアドバイスを発行。
  • キャッシュヒット可観測性 — すべての実行が(source attribution unavailable on cache-hit runs; --no-cache surfaces it)を報告する。これは良いガイダンスだが、ターゲットがそれまでチェックされたことがなくても現れる。「この実行は≥1キャッシュヒットを持った」のみに厳しくすることを検討。
  • Git-dirty警告warning: Git tree '/Users/megurine/repo/ruby/rigor' is dirtyが、ターゲットパスがdirtyツリーの外でも発行される。アウトオブツリーのターゲットに対してそれを沈黙させるか、チェックをターゲット自身のgitルートに対してリベースする。

5. 上位3つの実行可能な改善(推奨順)

Section titled “5. 上位3つの実行可能な改善(推奨順)”
  1. Integer算術 → BigDecimal誤推論を修正(§3a) — 最小の修正、最大のノイズ削減。11ライブラリのうち≥4に影響。
  2. ScopeIndexerを通じたmixin/includeルックアップを解決(§3b) — 中程度の労力、最も「バグとして誤解されるカテゴリー」を修正。「Rigorは私のコードを理解しない」という認識を減らす。
  3. try_static_refinementコールドキャッシュクラッシュを追跡し、修正するか文書化する(§2) — 小さな修正、新ユーザーが当たれば高い当惑コスト。

§3cのnilナローイング改善はより価値が高いが、より大きなスコープ — 次のリリースに急ぐのではなく、独自の設計パス(おそらくcontrol-flow-analysis仕様に結びつく)の価値がある。

Terminal window
cd /Users/megurine/repo/ruby/rigor-survey
# クローンはすでに配置済み;やり直すには:
for d in rgl algorithms faraday rbnacl parser rubocop-ast \
concurrent-ruby kramdown mail net-ssh; do
(cd "$d" && git pull --depth=1 -q)
done
# ライブラリごとのチェック
cd /Users/megurine/repo/ruby/rigor
nix --extra-experimental-features 'nix-command flakes' develop --command \
bundle exec exe/rigor check --clear-cache \
/Users/megurine/repo/ruby/rigor-survey/<name>/lib

ラウンド2: テンプレート&シリアライゼーションライブラリ(2026-05-18)

Section titled “ラウンド2: テンプレート&シリアライゼーションライブラリ(2026-05-18)”

11の追加ライブラリをサーベイ(テンプレートエンジン + シリアライゼーション)。ラウンド1と同じ方法論。

7. ライブラリごとのサマリー(ラウンド2)

Section titled “7. ライブラリごとのサマリー(ラウンド2)”
ライブラリファイルメモリエラー警告備考
herb421.2秒388 MB119Gem::Specification#full_gem_pathがRBSに欠落
liquid641.0秒304 MB177Class上のadd_filter → Class上のmixinルックアップギャップ
pycall220.9秒342 MB20非常にクリーン;Array[Dynamic[top]]上のwith_index
numo-narray20.9秒287 MB82C-ext gem;1つの.rbファイル。BigDecimal誤推論が再発
ox150.8秒311 MB120nil上の比較演算子;`Dynamic[top]
oj110.8秒285 MB50JSON::Ext::Generator::State.from_stateがRBSに欠落
jbuilder140.9秒290 MB1262ジェネレータ.rb ERBテンプレートがRubyとしてパースされる(118/126)
slim271.0秒345 MB982つのivar型乖離;read for nil
hamlit611.0秒321 MB188html_safe for String(ActiveSupport extn);BigDecimal
haml511.0秒307 MB156hamlitと同じ;merge_attributes! Object上のmixin
erubi30.8秒285 MB30Erubi#begin/#endのivar nilアクセス
ラウンド2合計31222642
総合1044421122

8a. .rb拡張子を持つジェネレータERBテンプレート(新規、高インパクト)

Section titled “8a. .rb拡張子を持つジェネレータERBテンプレート(新規、高インパクト)”

jbuilder/lib/generators/rails/templates/{api_,}controller.rbはERBテンプレート(<%= namespaced_path %>)で、Railsジェネレータがそれを期待するため.rb拡張子で保存されている。Rigorはそれらをrubyとしてパースし、126のjbuilderエラーのうち118を生成する(unexpected '<', '>''@' without identifiers is not allowed)。残りの8エラーはjbuilder.rb内の実際の発見。

このパターンはジェネレータを出荷するRailsスタイルのgemに普遍的。2つの緩和策:

  1. デフォルト除外、ターゲットに.rigor.ymlがないときのlib/generators/**/templates/**/*.rb
  2. ソースバイト内でERBマーカー(<%/%>)を検出し、118のパースエラーの代わりに単一の「スキップ: テンプレートファイル」:info診断を表面化。

オプション2はより原則的;オプション1は出荷が速い。

8b. String#html_safeが認識されない(新規、Railsエコシステム)

Section titled “8b. String#html_safeが認識されない(新規、Railsエコシステム)”

hamlit + haml: 6回のundefined method 'html_safe' for Stringの出現。これはActiveSupportのcore_extメソッド。ユーザーはrigor-activesupport-core-extプラグインを利用可能だが、デフォルトでは適用されない。3つのオプション:

  1. 診断でプラグインをより大きく文書化(「ヒント: プラグインXを有効化」)
  2. gem activesupportGemfile.lockにあるときのビルド時ヒント
  3. 現状(ユーザー駆動のオプトイン)

オプション2は最も侵入的でない — Gemfile.lockに見落とされていないが認識されたgemが利用可能なプラグインをシグナルするときに実行ごとに単一の:infoを発行。

8c. from_state / full_gem_path / markup_context= — RBSカバレッジギャップ

Section titled “8c. from_state / full_gem_path / markup_context= — RBSカバレッジギャップ”

これらは個別には小さいが、合わせてojherbliquidにまたがる約10の偽陽性を占める。それぞれがvendored_gem_sigs/またはコアRBSに欠けている既知のメソッド。影響を受けるリポジトリでのrigor sig-genフォローアップ、またはvendored_gem_sigs/の補足RBSに適する。

8d. nil上の比較演算子(§3cの精緻化)

Section titled “8d. nil上の比較演算子(§3cの精緻化)”

ox/lib/ox/element.rbはパターンを鋭く示す:

argument type mismatch at `<' on Integer: expected Numeric, got Dynamic[top] | nil

これは§3cの逆 — 「nil上のメソッド」ではなく、「Numericが期待されるところにDynamic[top] | nilを渡す」。同じ根本原因(使用前にnilがナローイングされていない);異なる診断ファミリー。仕様の堅牢性原則がディスパッチの両側で引数位置Dynamic[top]を一貫させるべきという指摘の価値がある。

9. 精緻化された横断ビュー(合成コーパス)

Section titled “9. 精緻化された横断ビュー(合成コーパス)”

22ライブラリ後、コーパス全体での総出現数でランク付けされた上位3つの診断ファミリーは:

ランクファミリー合計影響するライブラリ数
1undefined method X for nil / X is undefined on NilClass~14022のうち16
2condition is always falsey/truthy~5522のうち13
3Integer → Numeric → BigDecimal誤推論~2522のうち7
(数値上のupto<<timesto_ito_f

ファミリー3は最も明確な単一のバグ — それを修正することで機械的にファミリー2を減らす(不正な数値ナローイングがデッドブランチ診断につながるため)、そしてInteger#+ / Integer#-オーバーロード解決に集中している。

ファミリー3(Integer算術のNumeric誤推論)を最初に着地させる改善として選ぶ。影響を受けるライブラリ: algorithmsparserkramdownprotobufnumo-narrayhamlithaml。修正を駆動する具体的な最初のケース:

algorithms/lib/algorithms/sort.rb:70:13 (i+1).upto(container.size-1) do |j| error: undefined method 'upto' for BigDecimal

ここでiは外側の0.upto(...)のIntegerブロックパラメータ。根本原因仮説: Integer#+(Integer)オーバーロード選択が(Integer) → IntegerアームではなくNumeric → Numericフォールバックを選び、実体化されたNumericキャリアがBigDecimaluptoのない最も具体的な部分型)に畳まれる。


ラウンド3: ファミリー3(BigDecimal誤推論)の修正着地(2026-05-19)

Section titled “ラウンド3: ファミリー3(BigDecimal誤推論)の修正着地(2026-05-19)”

§3aが仮説したNumeric → Numericの広がりではない。実際のチェーン:

  1. Rigorのプロセスはbigdecimalrequireしないbigdecimal gemはRuby 3.4でデフォルトから格下げされ、Gemfileにはない)。
  2. Acceptance#accepts_nominal_from_constantObject.const_get("BigDecimal")を呼ぶ → NameError → 「判断できない」ため:maybeを返す。class_subtype_resultでも同じ。
  3. bigdecimal stdlib RBSはInteger#+ / - / *などをオーバーロードリストの先頭def +: (BigDecimal) -> BigDecimal | ...で再オープンする(| ...は元のIntegerオーバーロードを後にマージ)。
  4. OverloadSelectoryesまたはmaybeをマッチとして受け入れる。パス1はall-acceptする最初のオーバーロードを選ぶ。BigDecimalが最初でその受容がIntegerの値を持つ任意の引数に対してmaybeを返すので、BigDecimalアームが勝つ → 戻り型BigDecimal
  5. 下流のBigDecimal.upto / .<< / .timesは存在しない → 偽陽性。

再現は次に縮小: nDynamic[top]5 + n。直接のEnvironment.default env(bigdecimal未ロード)はIntegerを返す。Environment.for_projectDEFAULT_LIBRARIES = […, bigdecimal, …]をロード)はBigDecimalを返す。その非対称性がバグを固定した。

12. 修正(2部、master HEADに着地)

Section titled “12. 修正(2部、master HEADに着地)”

(a) lib/rigor/inference/acceptance.rbresolve_class(target)が失敗するがresolve_class(actual)が成功するとき、actual.ancestors.map(&:name).include?(target_name)にフォールバックし、権威的な:yes / :no回答を与える。定数の値は実行時に常にロード可能(値が存在する)なので、Constant<1>.value.classIntegerInteger.ancestors"BigDecimal"を含まない → 関係は:maybeではなく:noclass_subtype_resultNominal.accepts(Nominal)軸にも同じフォールバックを追加。完全に解決不能(両方ともユーザークラス)なケースは:maybeのまま。

(b) lib/rigor/inference/method_dispatcher/receiver_affinity.rb — 新しいモジュール + OverloadSelector.selectの先頭での新しい事前ソート、すべての位置パラム型がself_type.class_name自体またはその真のRBS祖先の1つであるアームが先頭に来るよう、オーバーロードを安定的にパーティション分けする。envがclass_orderingに答えられレシーバーがクラス名を運ぶときに事前ソートが発火する;「引数がuntypedを含む」にはゲートされていない、何もマッチしないときの誤順序なoverloads.firstフォールバックも同様に間違っているため。

13. サーベイ差分(22ライブラリコーパス)

Section titled “13. サーベイ差分(22ライブラリコーパス)”
ライブラリ修正前のエラー修正後のエラーΔ
protobuf161−15
parser118−3
hamlit1816−2
haml1513−2
algorithms5352−1
kramdown4241−1
concurrent-ruby1211−1
numo-narray880*
mail/net-ssh/その他(変更なし)(変更なし)0
合計421397−24

* numo-narrayの残りの8エラーは今、異なるカテゴリー(ex-BigDecimal-timesエラーの1つがInteger#timesオーバーロード選択問題を表面化: ブロックなしでは、RBSの() -> Enumeratorが勝つべきだが、アナライザーは依然ブロック付きアームを選んでいる)。別個のバグ;キュー。

コーパス全体でBigDecimal/Numericの偽陽性は7ライブラリで25回 → 0に削減。

  • make verify: 3789 spec(3783 + 6新)、0失敗、2 pending(既存のRactor-readiness項目)。
  • bundle exec rubocop: 601ファイル、0違反。
  • bundle exec exe/rigor check lib(セルフチェック): hkt_body_parser.rb / hkt_registry.rbの3つの既存のcondition is always truthy警告。git stash && rigor check経由でベースラインから変化なしを確認 — この修正によって導入されたものではない。
  • git diff --check: クリーン。

15. サーベイから残るカテゴリー(まだ対処されていない)

Section titled “15. サーベイから残るカテゴリー(まだ対処されていない)”

優先順、最大の残余バケットが最初:

  1. §3cパターンガードを通じたnilナローイング — 22ライブラリのうち16にまたがる約140回出現。絶対量で支配的なカテゴリー;多くの真陽性(ツリーコードが浅いチェック後にnode.leftを参照解除する)、しかし深くネストされたケースはreturn unless node / node && node.xイディオムに対するフロー絞り込みの地平線を露呈する。control-flow-analysis仕様に結びつく;迅速な修正ではなく専用の設計スライスに値する。
  2. §3b Object / Classとして解決されるミックスイン提供メソッドrglfaradayliquidrubocop-asthaml。ユーザーが「Rigorはミックスインを理解しない」と最もよく誤読する症状。ScopeIndexerのinclude/extend/prepend解決の監査。
  3. §8a Rubyとしてパースされるrailsジェネレータ.rb ERBテンプレート — jbuilderが126エラーのうち118(94%)を占める。lib/generators/**/templates/**/*.rbをデフォルト除外、またはソースバイト内のERBマーカーを検出。単一修正で高インパクト。
  4. §8b String#html_safeが認識されない — hamlit + haml。activesupportがターゲットのGemfile.lockにあるときにrigor-activesupport-core-extプラグインを:info診断で昇格させる。
  5. §3d condition is always falsey/truthyノイズ — §11〜12の修正経由で機械的に約5ケース削減(正しい数値ナローイングの下流)。残りのケースはほとんど真のデッドブランチ + §3c関連の残余。
  6. §3gインスタンス変数型乖離ノイズ — 設計判断(initializeからのnil | Tパターンを抑制、または:hintファミリーに分割)。

§2のコールドキャッシュtry_static_refinement内部エラーバグは、最初の呼び出し後に再現せず、このスライス中に再発しなかった。キューされた調査項目として残す — try_static_refinementの呼び出し元のgrepパスと推論エンジン仕様での呼び出しの価値がある。

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