rigortype 0.2.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/docs/handbook/03-narrowing.md +2 -1
  4. data/docs/handbook/04-tuples-and-shapes.md +7 -3
  5. data/docs/handbook/06-classes.md +18 -9
  6. data/docs/handbook/08-understanding-errors.md +1 -0
  7. data/docs/handbook/11-sig-gen.md +11 -1
  8. data/docs/handbook/appendix-mypy.md +4 -3
  9. data/docs/install.md +14 -6
  10. data/docs/manual/01-installation.md +98 -4
  11. data/docs/manual/02-cli-reference.md +49 -12
  12. data/docs/manual/03-configuration.md +34 -2
  13. data/docs/manual/04-diagnostics.md +23 -0
  14. data/docs/manual/07-plugins.md +4 -4
  15. data/docs/manual/09-editor-integration.md +4 -4
  16. data/docs/manual/11-ci.md +51 -0
  17. data/docs/manual/12-caching.md +65 -0
  18. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
  19. data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
  20. data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
  21. data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
  22. data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
  23. data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
  24. data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
  25. data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
  26. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
  27. data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
  28. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
  29. data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +93 -0
  30. data/lib/rigor/analysis/check_rules.rb +557 -95
  31. data/lib/rigor/analysis/dependency_recorder.rb +93 -9
  32. data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
  33. data/lib/rigor/analysis/incremental_session.rb +456 -51
  34. data/lib/rigor/analysis/path_expansion.rb +42 -0
  35. data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
  36. data/lib/rigor/analysis/rule_catalog.rb +226 -1
  37. data/lib/rigor/analysis/run_cache_key.rb +68 -0
  38. data/lib/rigor/analysis/run_cache_probe.rb +72 -0
  39. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
  40. data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
  41. data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
  42. data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
  43. data/lib/rigor/analysis/runner.rb +343 -68
  44. data/lib/rigor/analysis/severity_stamp.rb +43 -0
  45. data/lib/rigor/analysis/worker_session.rb +30 -2
  46. data/lib/rigor/bleeding_edge.rb +37 -8
  47. data/lib/rigor/builtins/regex_refinement.rb +36 -1
  48. data/lib/rigor/cache/descriptor.rb +88 -28
  49. data/lib/rigor/cache/file_digest.rb +158 -0
  50. data/lib/rigor/cache/incremental_snapshot.rb +41 -5
  51. data/lib/rigor/cache/rbs_descriptor.rb +56 -10
  52. data/lib/rigor/cache/store.rb +33 -3
  53. data/lib/rigor/ci_detector.rb +90 -0
  54. data/lib/rigor/cli/annotate_command.rb +15 -9
  55. data/lib/rigor/cli/baseline_command.rb +4 -1
  56. data/lib/rigor/cli/check_command.rb +212 -27
  57. data/lib/rigor/cli/coverage_command.rb +6 -1
  58. data/lib/rigor/cli/docs_command.rb +0 -30
  59. data/lib/rigor/cli/doctor_command.rb +163 -23
  60. data/lib/rigor/cli/lsp_command.rb +5 -0
  61. data/lib/rigor/cli/mcp_command.rb +5 -0
  62. data/lib/rigor/cli/plugins_command.rb +36 -6
  63. data/lib/rigor/cli/plugins_renderer.rb +44 -12
  64. data/lib/rigor/cli/probe_environment.rb +85 -0
  65. data/lib/rigor/cli/sig_gen_command.rb +36 -9
  66. data/lib/rigor/cli/skill_command.rb +1 -32
  67. data/lib/rigor/cli/trace_command.rb +5 -6
  68. data/lib/rigor/cli/type_of_command.rb +9 -8
  69. data/lib/rigor/cli/type_scan_command.rb +8 -8
  70. data/lib/rigor/cli.rb +15 -4
  71. data/lib/rigor/config_audit.rb +48 -2
  72. data/lib/rigor/configuration/severity_profile.rb +31 -0
  73. data/lib/rigor/configuration.rb +156 -8
  74. data/lib/rigor/environment/default_libraries.rb +35 -0
  75. data/lib/rigor/environment/rbs_loader.rb +164 -5
  76. data/lib/rigor/environment.rb +19 -27
  77. data/lib/rigor/inference/budget_trace.rb +77 -2
  78. data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
  79. data/lib/rigor/inference/def_handle.rb +23 -0
  80. data/lib/rigor/inference/def_node_resolver.rb +90 -0
  81. data/lib/rigor/inference/def_return_typer.rb +2 -1
  82. data/lib/rigor/inference/expression_typer.rb +361 -113
  83. data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
  84. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
  85. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
  86. data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
  87. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +11 -0
  88. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
  89. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
  90. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
  91. data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
  92. data/lib/rigor/inference/method_dispatcher.rb +57 -1
  93. data/lib/rigor/inference/mutation_widening.rb +42 -8
  94. data/lib/rigor/inference/narrowing.rb +127 -17
  95. data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
  96. data/lib/rigor/inference/project_patched_scanner.rb +2 -1
  97. data/lib/rigor/inference/rbs_type_translator.rb +8 -1
  98. data/lib/rigor/inference/scope_indexer.rb +451 -36
  99. data/lib/rigor/inference/statement_evaluator.rb +64 -15
  100. data/lib/rigor/inference/struct_fold_safety.rb +48 -11
  101. data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
  102. data/lib/rigor/inference/void_origin.rb +25 -0
  103. data/lib/rigor/inference/void_tail_summary.rb +220 -0
  104. data/lib/rigor/language_server/completion_provider.rb +2 -1
  105. data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
  106. data/lib/rigor/language_server/folding_range_provider.rb +2 -1
  107. data/lib/rigor/language_server/selection_range_provider.rb +2 -1
  108. data/lib/rigor/language_server/signature_help_provider.rb +2 -1
  109. data/lib/rigor/plugin/base.rb +9 -22
  110. data/lib/rigor/plugin/inflector.rb +12 -3
  111. data/lib/rigor/plugin/io_boundary.rb +5 -1
  112. data/lib/rigor/plugin/isolation.rb +81 -11
  113. data/lib/rigor/plugin/load_error.rb +10 -1
  114. data/lib/rigor/plugin/loader.rb +96 -14
  115. data/lib/rigor/plugin/node_rule_walk.rb +5 -3
  116. data/lib/rigor/plugin/registry.rb +32 -23
  117. data/lib/rigor/plugin.rb +26 -0
  118. data/lib/rigor/protection/mutator.rb +3 -2
  119. data/lib/rigor/reflection.rb +64 -0
  120. data/lib/rigor/runtime/jit.rb +128 -0
  121. data/lib/rigor/scope/discovery_index.rb +12 -2
  122. data/lib/rigor/scope.rb +122 -28
  123. data/lib/rigor/sig_gen/classification.rb +5 -1
  124. data/lib/rigor/sig_gen/generator.rb +38 -6
  125. data/lib/rigor/sig_gen/observation_collector.rb +4 -3
  126. data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
  127. data/lib/rigor/sig_gen/renderer.rb +10 -0
  128. data/lib/rigor/sig_gen/write_result.rb +9 -4
  129. data/lib/rigor/sig_gen/writer.rb +24 -2
  130. data/lib/rigor/source/node_children.rb +116 -0
  131. data/lib/rigor/source/node_locator.rb +3 -1
  132. data/lib/rigor/source/node_walker.rb +4 -2
  133. data/lib/rigor/source.rb +1 -0
  134. data/lib/rigor/type/difference.rb +28 -24
  135. data/lib/rigor/type/hash_shape.rb +34 -10
  136. data/lib/rigor/version.rb +1 -1
  137. data/lib/rigortype.rb +24 -0
  138. data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
  139. data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
  140. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
  141. data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
  142. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
  143. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
  144. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
  145. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
  146. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
  147. data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
  148. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
  149. data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
  150. data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
  151. data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
  152. data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
  153. data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
  154. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
  155. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
  156. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
  157. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
  158. data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
  159. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
  160. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
  161. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
  162. data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
  163. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
  164. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
  166. data/sig/prism_node_children.rbs +9 -0
  167. data/sig/rigor/inference.rbs +7 -0
  168. data/sig/rigor/plugin/base.rbs +2 -3
  169. data/sig/rigor/reflection.rbs +1 -0
  170. data/sig/rigor/scope.rbs +12 -1
  171. data/skills/rigor-ci-setup/SKILL.md +10 -0
  172. data/skills/rigor-editor-setup/SKILL.md +11 -0
  173. data/skills/rigor-next-steps/SKILL.md +23 -2
  174. data/skills/rigor-plugin-author/SKILL.md +3 -3
  175. data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
  176. data/skills/rigor-plugin-review/SKILL.md +2 -2
  177. data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
  178. data/skills/rigor-project-init/SKILL.md +29 -3
  179. data/skills/rigor-project-init/references/01-detect.md +1 -1
  180. data/skills/rigor-project-init/references/02-configure.md +39 -5
  181. data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
  182. data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
  183. metadata +42 -24
  184. data/lib/rigor/cli/ci_detector.rb +0 -89
@@ -8,6 +8,14 @@ module Rigor
8
8
  # docs/type-specification/inference-budgets.md is not yet wired); counting how often each fires on a real
9
9
  # project is the only way to see where inference actually stops.
10
10
  #
11
+ # A second family of counters (the `MEMO_*` categories) profiles the ADR-57 user-method return memo
12
+ # (`ExpressionTyper#infer_user_method_return`) rather than a cutoff — inference entries, memo hits/misses,
13
+ # the split of non-stored results by reason, and body-evaluation (compute) entries — plus two
14
+ # per-signature distributions (body-eval count and distinct-memo-key count). This is the evidence for the
15
+ # next return-memo design slice: a finalization-aware taint gate (recover the `consult-tainted`
16
+ # non-stores) vs memo-key normalization (collapse arg-granularity thrash — a signature whose distinct-key
17
+ # count far exceeds its body-eval savings).
18
+ #
11
19
  # Three categories, one per guard site:
12
20
  #
13
21
  # - {RECURSION_GUARD} — `ExpressionTyper#infer_user_method_return` detected a `(receiver, method)` cycle
@@ -44,9 +52,42 @@ module Rigor
44
52
  # `Dynamic[top]` (the escaping-block floor). Shared by slice B's loop-body fixpoint.
45
53
  BLOCK_WRITEBACK_CAP = :block_writeback_cap
46
54
 
55
+ # ADR-57 return-memo profile counters (not cutoffs — see the module doc). All bumped by
56
+ # `ExpressionTyper#infer_user_method_return` / `#compute_user_method_return`.
57
+ # - {MEMO_ENTRIES} — every `infer_user_method_return` entry (a user-method return inference).
58
+ # - {MEMO_HITS} / {MEMO_MISSES} — the memo was consulted (candidate frame) and the key was present /
59
+ # absent. Consults = hits + misses.
60
+ # - {MEMO_BODY_EVALS} — every `compute_user_method_return` entry (a body-evaluation compute; some are
61
+ # in-cycle re-entries that consult an ADR-55 summary rather than walk the body — those coincide with a
62
+ # `RECURSION_GUARD` hit). Body evals = misses + on-stack refusals (ADR-84 WD3 reduced candidacy to the
63
+ # on-stack check, so unroll-in-flight refusals are structurally gone).
64
+ # - {MEMO_REFUSE_ON_STACK} — the frame was not a memo candidate (bypassed the memo and computed): its
65
+ # plain `(receiver, method)` signature was already on the recursion guard stack.
66
+ # - {MEMO_REFUSE_UNROLL} — pre-ADR-84 blanket refusal (constant-arg unroll in flight). Kept in the table
67
+ # as the ADR-84 WD3 gate pin: structurally unreachable, must read ~0.
68
+ # - {MEMO_REFUSE_CONSULT_TAINTED} — a candidate frame computed a result but an ADR-55 fixpoint summary
69
+ # was *consulted* during the compute, so the result is a transient Kleene iterate and was NOT stored.
70
+ # - {MEMO_REFUSE_TRANSIENT} — ADR-84 WD3: a candidate frame's compute bracket saw a
71
+ # transient-machinery event (recursion-guard hit / unroll-fuel exhaustion / ADR-55 WD1 clamp /
72
+ # fixpoint-cap collapse — `ExpressionTyper#note_transient_fallback`) that referenced a stack frame
73
+ # BELOW the bracket's entry depth, so the ancestor context influenced the result and it was NOT
74
+ # stored. Own-machinery events (a recursive method's own fixpoint) do not refuse; top-of-stack
75
+ # computes are standalone by construction. Replaces the blanket unroll-in-flight exclusion.
76
+ MEMO_ENTRIES = :memo_entries
77
+ MEMO_HITS = :memo_hits
78
+ MEMO_MISSES = :memo_misses
79
+ MEMO_BODY_EVALS = :memo_body_evals
80
+ MEMO_REFUSE_ON_STACK = :memo_refuse_on_stack
81
+ MEMO_REFUSE_UNROLL = :memo_refuse_unroll
82
+ MEMO_REFUSE_CONSULT_TAINTED = :memo_refuse_consult_tainted
83
+ MEMO_REFUSE_TRANSIENT = :memo_refuse_transient
84
+
47
85
  CATEGORIES = [
48
86
  RECURSION_GUARD, ANCESTOR_WALK_LIMIT, HKT_FUEL_EXHAUSTED, RECURSION_UNROLL_FUEL,
49
- RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP
87
+ RECURSION_FIXPOINT_CAP, BLOCK_WRITEBACK_CAP,
88
+ MEMO_ENTRIES, MEMO_HITS, MEMO_MISSES, MEMO_BODY_EVALS,
89
+ MEMO_REFUSE_ON_STACK, MEMO_REFUSE_UNROLL, MEMO_REFUSE_CONSULT_TAINTED,
90
+ MEMO_REFUSE_TRANSIENT
50
91
  ].freeze
51
92
 
52
93
  # Distribution (histogram) categories — read-only observations of a value's size at a site, used to
@@ -55,12 +96,28 @@ module Rigor
55
96
  # `Combinator.union` produces — the distribution the `union_size` budget default should be set from.
56
97
  UNION_ARITY = :union_arity
57
98
 
58
- DISTRIBUTION_CATEGORIES = [UNION_ARITY].freeze
99
+ # ADR-57 return-memo per-signature distributions — `{signature => count}` maps, NOT integer-size
100
+ # histograms, so `summarize` (percentiles over integer values) does not apply; read them with
101
+ # {distribution}. Both keyed by the `"Receiver#method"` plain signature.
102
+ # - {MEMO_BODY_EVAL_BY_SIGNATURE} — body-eval (compute) count per signature (via {observe}).
103
+ # - {MEMO_DISTINCT_KEY_BY_SIGNATURE} — distinct memo keys per signature (via {observe_distinct}), i.e.
104
+ # how many `(receiver, arg-type)` variants a signature was memoised under. A count far above the
105
+ # signature's body-eval savings is arg-granularity thrash — the key-normalization candidate.
106
+ MEMO_BODY_EVAL_BY_SIGNATURE = :memo_body_eval_by_signature
107
+ MEMO_DISTINCT_KEY_BY_SIGNATURE = :memo_distinct_key_by_signature
108
+
109
+ DISTRIBUTION_CATEGORIES = [
110
+ UNION_ARITY, MEMO_BODY_EVAL_BY_SIGNATURE, MEMO_DISTINCT_KEY_BY_SIGNATURE
111
+ ].freeze
59
112
 
60
113
  @enabled = !ENV["RIGOR_BUDGET_TRACE"].to_s.empty?
61
114
  @mutex = Mutex.new
62
115
  @counts = Hash.new(0)
63
116
  @distributions = Hash.new { |h, k| h[k] = Hash.new(0) }
117
+ # Auxiliary de-duplication state for {observe_distinct}: category → Set of the `[bucket, member]`
118
+ # pairs already counted, so a repeated `(signature, memo-key)` observation is counted once. Only ever
119
+ # written when tracing is enabled; not part of the reported surface.
120
+ @distinct_seen = Hash.new { |h, k| h[k] = Set.new }
64
121
 
65
122
  module_function
66
123
 
@@ -101,6 +158,23 @@ module Rigor
101
158
  @mutex.synchronize { @distributions[category][value] += 1 }
102
159
  end
103
160
 
161
+ # Records one observation of `member` into `category`'s `{bucket => count}` map, but only the FIRST time
162
+ # a given `(bucket, member)` pair is seen — so the resulting count per bucket is the number of DISTINCT
163
+ # members, not the number of observations. Used for the distinct-memo-key-per-signature distribution.
164
+ # No-op (one boolean check) when disabled.
165
+ def observe_distinct(category, bucket, member)
166
+ return unless @enabled
167
+
168
+ @mutex.synchronize do
169
+ seen = @distinct_seen[category]
170
+ pair = [bucket, member]
171
+ unless seen.include?(pair)
172
+ seen << pair
173
+ @distributions[category][bucket] += 1
174
+ end
175
+ end
176
+ end
177
+
104
178
  # Frozen `{value => count}` histogram for a distribution category.
105
179
  def distribution(category)
106
180
  @mutex.synchronize { @distributions[category].dup.freeze }
@@ -137,6 +211,7 @@ module Rigor
137
211
  @mutex.synchronize do
138
212
  @counts.clear
139
213
  @distributions.clear
214
+ @distinct_seen.clear
140
215
  end
141
216
  end
142
217
  end
@@ -144,6 +144,16 @@ module Rigor
144
144
  RANGE_EXTRA = %i[step].freeze
145
145
  INTEGER_EXTRA = %i[times upto downto].freeze
146
146
 
147
+ # IO / File / StringIO line- and byte-iteration methods invoke the block immediately, once per line /
148
+ # byte / char, and never retain it — the same immediate-invocation contract as `Array#each`. Both the
149
+ # singleton `File.foreach` / `IO.foreach` (receiver `singleton(File)` → class name "File") and the
150
+ # instance `io.each_line` / `each_char` … forms resolve through the same class-name key. Without these,
151
+ # `File.foreach(path) { case … when … then flag = true when … then return true if flag end }` classifies
152
+ # `:unknown` and misses the loop-body re-narrowing, so a local written in one `when` arm reads its
153
+ # pre-loop value in a sibling arm and a guarding condition folds to a spurious constant.
154
+ IO_ITERATION = %i[each_line each each_byte each_char each_codepoint].freeze
155
+ IO_SINGLETON_ITERATION = %i[foreach].freeze
156
+
147
157
  NON_ESCAPING = {
148
158
  "Array" => (ENUMERABLE_NON_ESCAPING + ARRAY_EXTRA).freeze,
149
159
  "Hash" => (ENUMERABLE_NON_ESCAPING + HASH_EXTRA).freeze,
@@ -151,7 +161,10 @@ module Rigor
151
161
  "Set" => ENUMERABLE_NON_ESCAPING,
152
162
  "Integer" => INTEGER_EXTRA,
153
163
  "Enumerator" => ENUMERABLE_NON_ESCAPING,
154
- "Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING
164
+ "Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING,
165
+ "IO" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
166
+ "File" => (IO_ITERATION + IO_SINGLETON_ITERATION).freeze,
167
+ "StringIO" => IO_ITERATION
155
168
  }.freeze
156
169
 
157
170
  # Methods that are documented to **retain** the block past the call. The block is stored or scheduled,
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-85 WD3 — a lazy reference to a `Prism::DefNode` stored in a cross-file discovery table, in place of
6
+ # the live node itself. When an incremental recheck rebuilds the discovery index from cached per-file seed
7
+ # bundles (ADR-85 WD2), the `def_nodes` / `singleton_def_nodes` tables carry handles for every unchanged
8
+ # file's methods: the file was never parsed this run, so there is no live node to store. A handle records
9
+ # the three things needed to resolve or fingerprint it without the node:
10
+ #
11
+ # - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
12
+ # file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
13
+ # repeated parses of identical bytes). `name` is the cross-check: a resolved node whose `name` differs
14
+ # from the handle's forces a fresh walk (bundle/source skew that the digest gate should make impossible —
15
+ # conservative, never silent).
16
+ # - `fingerprint` — the SHA-256 of the def's source slice, captured when the bundle was built. This lets
17
+ # `Runner#symbol_fingerprints` read the change-detection fingerprint (ADR-46 slice 4) off the handle
18
+ # without re-parsing the file — the one value-deref consumer besides the three accessor choke points.
19
+ #
20
+ # Marshal-clean by construction (Integer + Strings), so it rides the `IncrementalSnapshot` blob directly.
21
+ DefHandle = Data.define(:path, :node_id, :name, :fingerprint)
22
+ end
23
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "def_handle"
5
+ require_relative "../source/node_children"
6
+
7
+ module Rigor
8
+ module Inference
9
+ # ADR-85 WD3 — resolves a {DefHandle} (stored in a bundle-rebuilt discovery table) to a live
10
+ # `Prism::DefNode`, through a per-run parse memo that yields ONE stable node object per `(path, node_id)`
11
+ # per run. Identity stability is a correctness constraint, not an optimisation: the ADR-84 return memo keys
12
+ # on the resolved def-node's object identity, so a fresh parse per resolution would mint a new identity
13
+ # each time and fragment that memo. The memo parses each demanded file at most once (recon Q5: an
14
+ # incremental recheck demands 0–6 files' bodies), builds a `node_id → DefNode` index over its DefNodes, and
15
+ # returns the same object on every later lookup.
16
+ #
17
+ # `node_id` is the primary key; `name` is a cross-check. A miss (the node_id is absent, or its node's name
18
+ # differs — a bundle/source skew the digest gate should make impossible, since identical bytes under the
19
+ # same Prism yield identical node_ids) falls back to the first same-named DefNode in a fresh walk of the
20
+ # file, and finally nil — conservative (the accessor then reports no user def, losing precision but never
21
+ # firing a wrong diagnostic), never silent.
22
+ #
23
+ # The memo is a thread-local installed for the duration of a run (the {Cache::FileDigest} pattern:
24
+ # process-local, Ractor-safe, never crosses a fork boundary, reset per run). Outside a run scope every
25
+ # resolution parses directly — a correct if un-memoised fallback for runner-less probes.
26
+ module DefNodeResolver
27
+ MEMO_KEY = :rigor_def_node_resolver_memo
28
+ private_constant :MEMO_KEY
29
+
30
+ # Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
31
+ # `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index.
32
+ def self.with_run
33
+ previous = Thread.current[MEMO_KEY]
34
+ Thread.current[MEMO_KEY] = { nodes: {}, indexes: {} }
35
+ yield
36
+ ensure
37
+ Thread.current[MEMO_KEY] = previous
38
+ end
39
+
40
+ # Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
41
+ # nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
42
+ # node (cold / re-walked file) or a handle (unchanged file) without branching.
43
+ def self.resolve(handle)
44
+ return handle unless handle.is_a?(DefHandle)
45
+
46
+ memo = Thread.current[MEMO_KEY]
47
+ return locate(handle, {}) if memo.nil?
48
+
49
+ key = [handle.path, handle.node_id].freeze
50
+ nodes = memo[:nodes]
51
+ return nodes[key] if nodes.key?(key)
52
+
53
+ nodes[key] = locate(handle, memo[:indexes])
54
+ end
55
+
56
+ # Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
57
+ def self.locate(handle, index_cache)
58
+ index = index_cache[handle.path] ||= build_file_index(handle.path)
59
+ node = index[:by_id][handle.node_id]
60
+ return node if node && node.name.to_s == handle.name
61
+
62
+ index[:by_name][handle.name]
63
+ rescue StandardError
64
+ nil
65
+ end
66
+ private_class_method :locate
67
+
68
+ def self.build_file_index(path)
69
+ root = Prism.parse(File.read(path)).value
70
+ by_id = {}
71
+ by_name = {}
72
+ collect_def_nodes(root, by_id, by_name)
73
+ { by_id: by_id, by_name: by_name }
74
+ end
75
+ private_class_method :build_file_index
76
+
77
+ # Walks the tree once, indexing every DefNode by node_id (primary) and by name (first-wins fallback).
78
+ def self.collect_def_nodes(node, by_id, by_name)
79
+ return unless node.is_a?(Prism::Node)
80
+
81
+ if node.is_a?(Prism::DefNode)
82
+ by_id[node.node_id] = node
83
+ by_name[node.name.to_s] ||= node
84
+ end
85
+ node.rigor_each_child { |child| collect_def_nodes(child, by_id, by_name) }
86
+ end
87
+ private_class_method :collect_def_nodes
88
+ end
89
+ end
90
+ end
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../type"
6
+ require_relative "../source/node_children"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -62,7 +63,7 @@ module Rigor
62
63
  return if RETURN_BARRIER_NODES.any? { |klass| node.is_a?(klass) }
63
64
 
64
65
  type_return_node(node, scope_index, out) if node.is_a?(Prism::ReturnNode)
65
- node.compact_child_nodes.each { |c| collect_return_types(c, scope_index, out) }
66
+ node.rigor_each_child { |c| collect_return_types(c, scope_index, out) }
66
67
  end
67
68
 
68
69
  def type_return_node(return_node, scope_index, out)