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
@@ -6,10 +6,12 @@ require_relative "../reflection"
6
6
  require_relative "../type"
7
7
  require_relative "../analysis/fact_store"
8
8
  require_relative "../source/node_walker"
9
+ require_relative "../source/node_children"
9
10
  require_relative "../source/constant_path"
10
11
  require_relative "block_parameter_binder"
11
12
  require_relative "body_fixpoint"
12
13
  require_relative "dynamic_origin"
14
+ require_relative "../analysis/check_rules/inferred_param_guard"
13
15
  require_relative "struct_fold_safety"
14
16
  require_relative "closure_escape_analyzer"
15
17
  require_relative "indexed_narrowing"
@@ -187,6 +189,16 @@ module Rigor
187
189
  [@scope.type_of(node, tracer: @tracer), @scope]
188
190
  end
189
191
 
192
+ # ADR-89 WD2 — the sorted positions of the positional parameters whose CONTENT `def_node` mutates
193
+ # (`callee_content_mutated_parameters`, the ADR-56 arg-flooring surface a caller consumes). A per-def
194
+ # static property of the AST — this is a pure exposure of the existing private computation (it never
195
+ # reads the scope), so the incremental session can compute it on any `StatementEvaluator` (an empty
196
+ # scope suffices), persist it in a return summary, and re-check a callee's symbol dependents when it
197
+ # moves (a caller's arg flooring changes even if the callee's return does not).
198
+ def content_mutated_parameter_positions(def_node)
199
+ callee_content_mutated_parameters(def_node).values.uniq.sort
200
+ end
201
+
190
202
  private
191
203
 
192
204
  attr_reader :scope, :tracer
@@ -224,6 +236,16 @@ module Rigor
224
236
  end
225
237
 
226
238
  bound = post_rhs.with_local(node.name, rhs_type)
239
+ # ADR-67 WD6b — a local whose RHS is (transitively) rooted at an inferred parameter inherits the
240
+ # "inferred, not declared" mark, so a subsequent use of the local declines the negative rules for the
241
+ # same lower-bound reason (`vindex = codepoints[i] - x; vindex < y`). The mark is STICKY across
242
+ # narrowing/joins (see `Scope#without_inferred_param_mark`), so a genuine rewrite from a non-param RHS
243
+ # must clear it here. No-op unless the `parameter_inference:` gate seeded a parameter this RHS reaches.
244
+ if Analysis::CheckRules::InferredParamGuard.rooted?(node.value, scope)
245
+ return [rhs_type, bound.with_inferred_param_mark(node.name)]
246
+ end
247
+
248
+ bound = bound.without_inferred_param_mark(node.name)
227
249
  bound = bound.with_local_origin(node.name, rhs_origin(node.value, post_rhs, rhs_type))
228
250
  [rhs_type, bound]
229
251
  end
@@ -748,7 +770,14 @@ module Rigor
748
770
  node.is_a?(Prism::ModuleNode) ||
749
771
  node.is_a?(Prism::BlockNode)
750
772
 
751
- node.compact_child_nodes.any? { |c| arm_contains_retry?(c) }
773
+ found = false
774
+ node.rigor_each_child do |c|
775
+ next unless arm_contains_retry?(c)
776
+
777
+ found = true
778
+ break
779
+ end
780
+ found
752
781
  end
753
782
 
754
783
  def absorb_retry_rebinds(accumulator, entry_scope, arm_post_scope)
@@ -936,7 +965,7 @@ module Rigor
936
965
  return if node.nil?
937
966
 
938
967
  found[node] = true if node.is_a?(Prism::BreakNode)
939
- node.compact_child_nodes.each do |child|
968
+ node.rigor_each_child do |child|
940
969
  next if BREAK_BOUNDARY_NODES.any? { |klass| child.is_a?(klass) }
941
970
 
942
971
  collect_direct_breaks(child, found)
@@ -1322,6 +1351,13 @@ module Rigor
1322
1351
  # (e.g. `arms << x`, `@tags << hashtag`). Stops a literal-shape carrier (`Tuple` / `HashShape`) from outliving
1323
1352
  # its justification when the value is mutated. Always-safe (loses precision, never invents facts).
1324
1353
  post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope)
1354
+ # ADR-48 slice 4 — Struct member-setter re-typing. After `s.x = v` on a fold-safe StructInstance local, rebind
1355
+ # `s` to a StructInstance with member `:x` replaced by the assigned type, so a later `s.x` folds to `v` and a
1356
+ # sibling `s.y` stays precise. `call_type` is the setter's own result (the assigned value type). Sound only for
1357
+ # a fold-safe local (never aliased / escaped, straight-line setters) — the fold-safe scan is the gate.
1358
+ post_scope = MethodDispatcher::StructFolding.apply_setter_writeback(
1359
+ call_node: node, assigned_type: call_type, scope: post_scope
1360
+ )
1325
1361
  # ADR-57 slice 3 work-item 1 (cross-method-boundary variant). When a self-call resolves to a user method that
1326
1362
  # CONTENT-mutates one of its parameters inside an escaping block (the `build_option_parser(opts)` idiom — the
1327
1363
  # callee returns an `OptionParser` whose `opts.on { o[:k] = v }` blocks close over the passed-in hash), floor
@@ -1624,9 +1660,9 @@ module Rigor
1624
1660
  def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
1625
1661
  result = nil
1626
1662
  relevant.each do |plugin|
1627
- next unless index.type_specifier_candidate_for?(plugin, name)
1663
+ next unless index.narrowing_facts_candidate_for?(plugin, name)
1628
1664
 
1629
- facts = plugin.type_specifier_facts(call_node: call_node, scope: current_scope)
1665
+ facts = plugin.narrowing_facts_for(call_node: call_node, scope: current_scope)
1630
1666
  (result ||= []) << Rigor::FlowContribution.new(post_return_facts: facts) if facts && !facts.empty?
1631
1667
  rescue StandardError
1632
1668
  next
@@ -1910,7 +1946,7 @@ module Rigor
1910
1946
  return if node.nil?
1911
1947
 
1912
1948
  yield node
1913
- node.compact_child_nodes.each do |child|
1949
+ node.rigor_each_child do |child|
1914
1950
  next if child.is_a?(Prism::BlockNode) || child.is_a?(Prism::DefNode) || child.is_a?(Prism::LambdaNode)
1915
1951
 
1916
1952
  each_node_outside_nested_scopes(child, &)
@@ -2480,7 +2516,7 @@ module Rigor
2480
2516
  sub_eval(node.body, fresh, class_context: new_context)
2481
2517
  end
2482
2518
 
2483
- def build_method_entry_scope(def_node)
2519
+ def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
2484
2520
  singleton = singleton_def?(def_node)
2485
2521
  binder = MethodParameterBinder.new(
2486
2522
  environment: scope.environment,
@@ -2492,8 +2528,11 @@ module Rigor
2492
2528
  # ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
2493
2529
  # RBS-declared parameter wins, the table is empty on a normal `check` run). The inferred type lives only as a
2494
2530
  # body local, never as an RBS contract, so it cannot fire a parameter-boundary diagnostic (WD1, satisfied by
2495
- # construction).
2496
- bindings = seed_inferred_param_types(bindings, def_node, singleton)
2531
+ # construction). WD6b — the second element is the set of parameters this seed overrode, stamped with
2532
+ # the "inferred, not declared" provenance mark at bind time so the in-body rules can decline on them.
2533
+ seeded = seed_inferred_param_types(bindings, def_node, singleton)
2534
+ bindings = seeded.fetch(0)
2535
+ inferred_names = seeded.fetch(1)
2497
2536
 
2498
2537
  # Method bodies do NOT see the outer scope's locals. They start from a fresh scope with the same environment,
2499
2538
  # then receive the parameter bindings. Slice 7 phase 2: instance defs ALSO seed their `ivars` map from the
@@ -2511,7 +2550,7 @@ module Rigor
2511
2550
  def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
2512
2551
  )
2513
2552
  )
2514
- bindings.reduce(fresh) { |acc, (name, type)| bind_param(acc, name, type) }
2553
+ bindings.reduce(fresh) { |acc, (name, type)| bind_param(acc, name, type, inferred_names) }
2515
2554
  end
2516
2555
 
2517
2556
  # ADR-82 root-enrichment — bind a method parameter, and for an *undeclared* (untyped) parameter seed its
@@ -2520,8 +2559,14 @@ module Rigor
2520
2559
  # route to parameter inference rather than reporting no cause at all. Reuses the WD1 `local_origins`
2521
2560
  # channel, so WD6 then carries the cause through any chain rooted at the parameter (`x.foo.bar`). An
2522
2561
  # RBS-declared / call-site-inferred parameter (a non-untyped binding) keeps no origin — it is not a hole.
2523
- def bind_param(acc, name, type)
2562
+ #
2563
+ # ADR-67 WD6b — a parameter the call-site inference seed overrode (`inferred_names`) is stamped with the
2564
+ # "inferred, not declared" provenance mark instead: its type is a concrete (non-untyped) lower bound, so it
2565
+ # is not an `inferred_return_untyped` hole, but the negative in-body rules must still decline on it (the
2566
+ # union is open, so firing is an FP by construction). The mark drops on any flow-live rewrite of the local.
2567
+ def bind_param(acc, name, type, inferred_names = nil)
2524
2568
  bound = acc.with_local(name, type)
2569
+ return bound.with_inferred_param_mark(name) if inferred_names&.include?(name)
2525
2570
  return bound unless untyped_binding?(type)
2526
2571
 
2527
2572
  bound.with_local_origin(name, DynamicOrigin::INFERRED_RETURN_UNTYPED)
@@ -2534,19 +2579,23 @@ module Rigor
2534
2579
  # seed is byte-identical there.
2535
2580
  def seed_inferred_param_types(bindings, def_node, singleton)
2536
2581
  inferred = scope.param_inferred_types
2537
- return bindings if inferred.empty?
2582
+ return [bindings, nil] if inferred.empty?
2538
2583
 
2539
2584
  path = current_class_path
2540
- return bindings if path.nil?
2585
+ return [bindings, nil] if path.nil?
2541
2586
 
2542
2587
  table = inferred[[path, def_node.name, singleton ? :singleton : :instance]]
2543
- return bindings if table.nil? || table.empty?
2588
+ return [bindings, nil] if table.nil? || table.empty?
2544
2589
 
2545
2590
  merged = bindings.dup
2591
+ overridden = nil
2546
2592
  table.each do |name, type|
2547
- merged[name] = type if merged.key?(name) && untyped_binding?(merged[name])
2593
+ next unless merged.key?(name) && untyped_binding?(merged[name])
2594
+
2595
+ merged[name] = type
2596
+ (overridden ||= Set.new) << name
2548
2597
  end
2549
- merged
2598
+ [merged, overridden]
2550
2599
  end
2551
2600
 
2552
2601
  # True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
@@ -3,6 +3,7 @@
3
3
  require "prism"
4
4
 
5
5
  require_relative "../source/constant_path"
6
+ require_relative "../source/node_children"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -59,11 +60,18 @@ module Rigor
59
60
  return EMPTY if members.empty?
60
61
 
61
62
  total = Hash.new(0)
62
- pure = Hash.new(0)
63
- count_uses(root, members, total, pure)
64
-
63
+ safe_uses = Hash.new(0)
64
+ deferred_setter = {}
65
+ count_uses(root, members, total, safe_uses, deferred_setter, false)
66
+
67
+ # A local is fold-safe iff every read of it is a safe use — a pure read OR (ADR-48 slice 4) a
68
+ # straight-line member setter (`n.x = v`) that the setter write-back re-types the binding for.
69
+ # `deferred_setter` disqualifies a local whose setter sits inside a loop / block / lambda, where a
70
+ # single static pass cannot model the setter's per-iteration effect (the write-back would leave a
71
+ # stale binding for the fold to read).
65
72
  safe = members.each_key.select do |name|
66
- writes[name] == 1 && total[name].positive? && total[name] == pure[name]
73
+ writes[name] == 1 && total[name].positive? &&
74
+ total[name] == safe_uses[name] && !deferred_setter[name]
67
75
  end
68
76
  safe.empty? ? EMPTY : safe.to_set
69
77
  end
@@ -84,22 +92,30 @@ module Rigor
84
92
  end
85
93
  end
86
94
 
87
- # Pass 2 — count, per recorded struct local, total reads vs. reads that are the receiver of a pure-read call.
88
- def count_uses(node, members, total, pure)
95
+ # Pass 2 — count, per recorded struct local, total reads vs. SAFE-use reads (the receiver of a pure-read call,
96
+ # or of a straight-line member setter). `deferred` is true inside a loop / block / lambda; a member setter seen
97
+ # there marks the local's `deferred_setter` so it is excluded (its write-back cannot be modelled statically).
98
+ def count_uses(node, members, total, safe_uses, deferred_setter, deferred)
89
99
  return if node.nil?
90
100
 
91
101
  total[node.name] += 1 if node.is_a?(Prism::LocalVariableReadNode) && members.key?(node.name)
92
102
 
93
103
  if node.is_a?(Prism::CallNode)
94
104
  receiver = node.receiver
95
- if receiver.is_a?(Prism::LocalVariableReadNode) && members.key?(receiver.name) &&
96
- pure_read_call?(node, members[receiver.name])
97
- pure[receiver.name] += 1
105
+ if receiver.is_a?(Prism::LocalVariableReadNode) && members.key?(receiver.name)
106
+ member_set = members[receiver.name]
107
+ if pure_read_call?(node, member_set)
108
+ safe_uses[receiver.name] += 1
109
+ elsif member_setter_call?(node, member_set)
110
+ safe_uses[receiver.name] += 1
111
+ deferred_setter[receiver.name] = true if deferred
112
+ end
98
113
  end
99
114
  end
100
115
 
116
+ child_deferred = deferred || deferred_boundary?(node)
101
117
  each_local_scope_child(node) do |child|
102
- count_uses(child, members, total, pure)
118
+ count_uses(child, members, total, safe_uses, deferred_setter, child_deferred)
103
119
  end
104
120
  end
105
121
 
@@ -110,6 +126,27 @@ module Rigor
110
126
  FIXED_READS.include?(name) || member_set.include?(name)
111
127
  end
112
128
 
129
+ # A `n.<member> = v` attribute setter on the receiver (ADR-48 slice 4): the selector strips its trailing `=` to
130
+ # a member reader and the call carries exactly one argument. Comparison operators (`==`, `>=`, `!=`) also end
131
+ # with `=` but never strip to a member, so they stay unknown (unsafe) calls. `:[]=` is likewise not a member.
132
+ def member_setter_call?(call_node, member_set)
133
+ name = call_node.name.to_s
134
+ return false unless name.length > 1 && name.end_with?("=")
135
+ return false unless member_set.include?(name[0..-2].to_sym)
136
+
137
+ (call_node.arguments&.arguments&.size || 0) == 1
138
+ end
139
+
140
+ # Constructs whose body runs zero-or-many times or is deferred (loops, blocks, lambdas): a single static pass
141
+ # over the body cannot model a member setter's effect across iterations, so a setter inside one disqualifies the
142
+ # local. Straight-line conditionals (`if` / `unless` / `case`) are NOT boundaries — their branch scopes join
143
+ # soundly, so a setter in one branch is fine.
144
+ def deferred_boundary?(node)
145
+ node.is_a?(Prism::WhileNode) || node.is_a?(Prism::UntilNode) ||
146
+ node.is_a?(Prism::ForNode) || node.is_a?(Prism::BlockNode) ||
147
+ node.is_a?(Prism::LambdaNode)
148
+ end
149
+
113
150
  # The member set of a `<Struct chain>.new(...)` / `.[]` materialisation, or nil. Handles the inline
114
151
  # `Struct.new(:a, :b).new(...)` form and the `Const.new(...)` form (resolved through the layout side-table).
115
152
  def struct_materialization_members(value_node, layout_lookup)
@@ -156,7 +193,7 @@ module Rigor
156
193
  # Yields each child to recurse into, skipping the subtree of a nested local-variable-scope boundary (a `def` /
157
194
  # `class` / `module`).
158
195
  def each_local_scope_child(node)
159
- node.compact_child_nodes.each do |child|
196
+ node.rigor_each_child do |child|
160
197
  next if scope_boundary?(child)
161
198
 
162
199
  yield child
@@ -5,6 +5,7 @@ require "prism"
5
5
  require_relative "../plugin/macro/heredoc_template"
6
6
  require_relative "../plugin/macro/trait_registry"
7
7
  require_relative "../source/literals"
8
+ require_relative "../source/node_children"
8
9
  require_relative "synthetic_method"
9
10
  require_relative "synthetic_method_index"
10
11
 
@@ -166,12 +167,12 @@ module Rigor
166
167
  name = class_name_from(node, scope_stack)
167
168
  yield name, node if name
168
169
  new_stack = scope_stack + [node]
169
- node.body&.compact_child_nodes&.each { |child| walk_classes(child, new_stack, &) }
170
+ node.body&.rigor_each_child { |child| walk_classes(child, new_stack, &) }
170
171
  when Prism::ModuleNode
171
172
  new_stack = scope_stack + [node]
172
- node.body&.compact_child_nodes&.each { |child| walk_classes(child, new_stack, &) }
173
+ node.body&.rigor_each_child { |child| walk_classes(child, new_stack, &) }
173
174
  else
174
- node.compact_child_nodes.each { |child| walk_classes(child, scope_stack, &) }
175
+ node.rigor_each_child { |child| walk_classes(child, scope_stack, &) }
175
176
  end
176
177
  end
177
178
 
@@ -303,12 +304,12 @@ module Rigor
303
304
  name = class_name_from(node, scope_stack)
304
305
  yield name, node.body
305
306
  new_stack = scope_stack + [node]
306
- node.body&.compact_child_nodes&.each { |child| walk_module_decls(child, new_stack, &) }
307
+ node.body&.rigor_each_child { |child| walk_module_decls(child, new_stack, &) }
307
308
  when Prism::ClassNode
308
309
  new_stack = scope_stack + [node]
309
- node.body&.compact_child_nodes&.each { |child| walk_module_decls(child, new_stack, &) }
310
+ node.body&.rigor_each_child { |child| walk_module_decls(child, new_stack, &) }
310
311
  else
311
- node.compact_child_nodes.each { |child| walk_module_decls(child, scope_stack, &) }
312
+ node.rigor_each_child { |child| walk_module_decls(child, scope_stack, &) }
312
313
  end
313
314
  end
314
315
 
@@ -372,7 +373,7 @@ module Rigor
372
373
  hierarchy.freeze
373
374
  end
374
375
 
375
- def walk_class_decls(node, scope_stack, &) # rubocop:disable Metrics/PerceivedComplexity
376
+ def walk_class_decls(node, scope_stack, &)
376
377
  return unless node.respond_to?(:compact_child_nodes)
377
378
 
378
379
  if node.is_a?(Prism::ClassNode)
@@ -380,19 +381,19 @@ module Rigor
380
381
  parent = parent_name_from(node, scope_stack)
381
382
  yield name, parent if name
382
383
  new_stack = scope_stack + [node]
383
- node.body&.compact_child_nodes&.each { |child| walk_class_decls(child, new_stack, &) }
384
+ node.body&.rigor_each_child { |child| walk_class_decls(child, new_stack, &) }
384
385
  elsif node.is_a?(Prism::ModuleNode)
385
386
  new_stack = scope_stack + [node]
386
- node.body&.compact_child_nodes&.each { |child| walk_class_decls(child, new_stack, &) }
387
+ node.body&.rigor_each_child { |child| walk_class_decls(child, new_stack, &) }
387
388
  else
388
- node.compact_child_nodes.each { |child| walk_class_decls(child, scope_stack, &) }
389
+ node.rigor_each_child { |child| walk_class_decls(child, scope_stack, &) }
389
390
  end
390
391
  end
391
392
 
392
393
  # Yields `(class_name, call_node)` for every Prism::CallNode at class-body top level (singleton-context calls).
393
394
  # Nested method bodies, blocks, and conditionals are skipped — the Tier C call shapes the substrate targets all
394
395
  # live at the class body's top level.
395
- def walk_class_bodies(node, scope_stack = [], &) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
396
+ def walk_class_bodies(node, scope_stack = [], &)
396
397
  return unless node.respond_to?(:compact_child_nodes)
397
398
 
398
399
  if node.is_a?(Prism::ClassNode)
@@ -403,12 +404,12 @@ module Rigor
403
404
  yield name, stmt if stmt.is_a?(Prism::CallNode) && stmt.receiver.nil?
404
405
  end
405
406
  end
406
- node.body&.compact_child_nodes&.each { |child| walk_class_bodies(child, new_stack, &) }
407
+ node.body&.rigor_each_child { |child| walk_class_bodies(child, new_stack, &) }
407
408
  elsif node.is_a?(Prism::ModuleNode)
408
409
  new_stack = scope_stack + [node]
409
- node.body&.compact_child_nodes&.each { |child| walk_class_bodies(child, new_stack, &) }
410
+ node.body&.rigor_each_child { |child| walk_class_bodies(child, new_stack, &) }
410
411
  else
411
- node.compact_child_nodes.each { |child| walk_class_bodies(child, scope_stack, &) }
412
+ node.rigor_each_child { |child| walk_class_bodies(child, scope_stack, &) }
412
413
  end
413
414
  end
414
415
 
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-100 WD3 — the resolved `-> void` origin site recorded in {Rigor::Scope#void_origins}. A small cause
6
+ # record, never a carrier field: it says *which* author-declared `-> void` return was recovered to `top`
7
+ # at a use site, so the `static.value-use.void` diagnostic can name it. Mirrors the provenance-as-
8
+ # side-channel discipline {DynamicOrigin} established (ADR-75) — it never participates in subtyping,
9
+ # consistency, normalization, or erasure.
10
+ #
11
+ # @!attribute class_name
12
+ # @return [String] the receiver class whose method declared `-> void`.
13
+ # @!attribute method_name
14
+ # @return [Symbol] the method whose return was recovered.
15
+ # @!attribute kind
16
+ # @return [Symbol] `:instance` or `:singleton`.
17
+ VoidOrigin = Data.define(:class_name, :method_name, :kind) do
18
+ # A human-facing `Class#method` / `Class.method` label for the diagnostic message.
19
+ def label
20
+ separator = kind == :singleton ? "." : "#"
21
+ "#{class_name}#{separator}#{method_name}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "void_origin"
6
+ require_relative "../reflection"
7
+ require_relative "../source/node_children"
8
+
9
+ module Rigor
10
+ module Inference
11
+ # ADR-100 WD4 — the lazy per-`def` "void tail" summary that carries an author-declared `-> void`
12
+ # provenance ACROSS an intermediate method whose own signature declares nothing. The direct case
13
+ # (`x = logger.log(...)`, an RBS `-> void` resolved on the receiver's own class) is recorded at the
14
+ # call site by {MethodDispatcher::RbsDispatch}; this summary answers the transitive case
15
+ # (`def bar; foo; end; b = bar`) where the `void` reaches the use through `bar`'s body, so no rule
16
+ # reading `bar`'s signature can see it.
17
+ #
18
+ # `VoidTail(def_node) -> VoidOrigin | nil`: computed on demand at the first consult, memoised per
19
+ # `def_node` (identity-keyed), cycle-guarded (a def already on the current chain rejects), and
20
+ # **pure** — AST shape + RBS reflection ({Reflection.instance_method_definition} /
21
+ # {Reflection.singleton_method_definition}) + discovery-index lookups ({Scope#user_def_for} /
22
+ # {Scope#singleton_def_for}) only, never expression evaluation. Because it never evaluates, it can
23
+ # neither re-enter the dispatcher nor depend on evaluation order or the fork-pool's file
24
+ # partitioning — the eager record-at-body-evaluation table WD4 rejected failed exactly there.
25
+ #
26
+ # A `def` (resolved on `owner` / `kind`) is admitted iff all four hold:
27
+ #
28
+ # 1. its OWN resolved RBS signature for `(owner, name, kind)` is absent, or every overload
29
+ # returns `untyped` (`RBS::Types::Bases::Any`). An author-declared concrete return — including
30
+ # `-> void` itself, which the direct rule already serves at the call site — keeps it out, so
31
+ # the direct rule and the summary are disjoint by construction (no double record).
32
+ # 2. its body is a plain statements body (no `rescue` / `else` / `ensure` on the `def` — those
33
+ # shapes make `def_node.body` a `Prism::BeginNode`) containing NO `Prism::ReturnNode` anywhere:
34
+ # a bare `return` yields `nil`, a non-void path, so the void tail must be the SOLE return path.
35
+ # The walk is deliberately over-conservative — a `return` inside a nested block also
36
+ # disqualifies.
37
+ # 3. its tail (last) expression is a `Prism::CallNode` with an implicit-`self` receiver (`nil`
38
+ # receiver or a literal `self`).
39
+ # 4. that tail resolves on the SAME owner (exact class, no ancestor walk on the discovery side) to
40
+ # either (a) an RBS definition whose EVERY overload returns `void` — the leaf, whose origin is
41
+ # `VoidOrigin(owner, tail_name, kind)` — or (b) another discovered `def` of the same kind, which
42
+ # recurses. Composition (`def baz; bar; end`) is the recursion, and the origin stays the leaf,
43
+ # so the message still names the author's `-> void` method. The RBS-void-leaf branch is checked
44
+ # FIRST because under ADR-93 auto-wire the leaf `def` is BOTH RBS-`void` and a discovered `def`;
45
+ # recursing into its body would miss the leaf.
46
+ #
47
+ # Everything else — explicit-receiver tails, conditional / boolean / `begin` tails, unresolvable
48
+ # names, non-void concrete RBS — rejects.
49
+ #
50
+ # Each tail chain is a functional graph (every `def` has exactly one tail, hence at most one
51
+ # successor), so a `def`'s result is fully determined by its own forward chain and is independent of
52
+ # the entry point: a chain either reaches a void leaf (`VoidOrigin`), a non-admitted node (nil), or a
53
+ # cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
54
+ # is still mid-computation on the current chain (never yet memoised).
55
+ class VoidTailSummary
56
+ # Run-scoped memo, the ADR-84 `return_memo_bucket` house pattern: a single retained slot on
57
+ # `Thread.current` keyed by the run-generation token's identity, so a re-run in one process (LSP,
58
+ # ADR-62 warm loop) lands in a fresh bucket and can neither serve stale entries nor accumulate
59
+ # them. Thread-local ⇒ each fork-pool worker (a separate process) and each Ractor-pool worker (its
60
+ # own Ractor-local `Thread.current`) builds its own bucket; the bucket hash is created and mutated
61
+ # inside the worker and never shared, so no frozen / shared object is mutated and no
62
+ # graceful-degradation branch is needed. `def_node` identity is stable per run (the ADR-85
63
+ # {DefNodeResolver} yields one node object per `(path, node_id)`), the same identity the ADR-84
64
+ # return memo relies on.
65
+ MEMO_KEY = :__rigor_void_tail_summary_memo__
66
+ private_constant :MEMO_KEY
67
+
68
+ def initialize(scope)
69
+ @scope = scope
70
+ @environment = scope.environment
71
+ end
72
+
73
+ # The recovered `-> void` origin for `def_node` (whose owner / kind the caller resolved through
74
+ # the discovery index), or nil when the `def` is not admitted. A fresh visited set per top-level
75
+ # consult; the memo persists across consults for the run. The memo is probed before the visited
76
+ # set is allocated so the common post-warmup hit path allocates nothing.
77
+ #
78
+ # @param def_node [Prism::DefNode]
79
+ # @param owner [String] the qualified receiver class the `def` belongs to.
80
+ # @param kind [Symbol] `:instance` or `:singleton`.
81
+ # @return [Inference::VoidOrigin, nil]
82
+ def origin_for(def_node, owner, kind)
83
+ return nil if def_node.nil?
84
+
85
+ memo = memo_bucket
86
+ return memo[def_node] if memo.key?(def_node)
87
+
88
+ walk(def_node, owner, kind, {}.compare_by_identity)
89
+ end
90
+
91
+ private
92
+
93
+ attr_reader :scope, :environment
94
+
95
+ # Memo + cycle-guarded computation. A def already on the current chain rejects WITHOUT memoising
96
+ # (it is mid-computation; its true result is memoised when its own frame completes).
97
+ def walk(def_node, owner, kind, visited)
98
+ return nil if def_node.nil?
99
+ return nil if visited.key?(def_node)
100
+
101
+ memo = memo_bucket
102
+ return memo[def_node] if memo.key?(def_node)
103
+
104
+ visited[def_node] = true
105
+ memo[def_node] = compute(def_node, owner, kind, visited)
106
+ end
107
+
108
+ def compute(def_node, owner, kind, visited)
109
+ return nil unless own_signature_admits?(owner, def_node.name, kind) # admission 1
110
+
111
+ tail = admissible_tail(def_node) # admissions 2 + 3
112
+ return nil if tail.nil?
113
+
114
+ resolve_tail(tail.name, owner, kind, visited) # admission 4
115
+ end
116
+
117
+ # Admission 1 — absent own signature, or every overload `untyped`. `untyped` is read as "no claim"
118
+ # per RBS's own semantics, so a hand-written `-> untyped` intermediate still admits: the provenance
119
+ # fact (this value was produced by an author-declared `-> void` return) remains true of it.
120
+ def own_signature_admits?(owner, method_name, kind)
121
+ definition = method_definition(owner, method_name, kind)
122
+ return true if definition.nil?
123
+
124
+ every_overload_return?(definition, RBS::Types::Bases::Any)
125
+ end
126
+
127
+ # Admissions 2 + 3 — the tail expression when the body is a plain statements body with no
128
+ # `ReturnNode` anywhere and the tail is an implicit-self call; nil (reject) otherwise.
129
+ def admissible_tail(def_node)
130
+ body = def_node.body
131
+ return nil if body.nil?
132
+ return nil if body.is_a?(Prism::BeginNode) # def-level rescue / else / ensure
133
+ return nil if contains_return?(body)
134
+
135
+ tail = tail_expression(body)
136
+ return nil unless tail.is_a?(Prism::CallNode)
137
+ return nil unless implicit_self_receiver?(tail.receiver)
138
+
139
+ tail
140
+ end
141
+
142
+ # A `Prism::StatementsNode` (the plain and endless-def shapes both parse to one) tails on its last
143
+ # statement; any other surviving shape is treated as the tail expression itself.
144
+ def tail_expression(body)
145
+ case body
146
+ when Prism::StatementsNode then body.body.last
147
+ else body
148
+ end
149
+ end
150
+
151
+ def implicit_self_receiver?(receiver)
152
+ receiver.nil? || receiver.is_a?(Prism::SelfNode)
153
+ end
154
+
155
+ # True when a `Prism::ReturnNode` appears anywhere in the subtree — nested blocks and defs
156
+ # included (over-conservative is correct: a method that can return anything other than the void
157
+ # tail must never enter the table).
158
+ def contains_return?(node)
159
+ return false unless node.is_a?(Prism::Node)
160
+ return true if node.is_a?(Prism::ReturnNode)
161
+
162
+ node.rigor_each_child { |child| return true if contains_return?(child) }
163
+ false
164
+ end
165
+
166
+ # Admission 4 — the void leaf (RBS every-overload `void`) wins over the discovered-def recursion,
167
+ # because under ADR-93 auto-wire the annotated leaf is both.
168
+ def resolve_tail(tail_name, owner, kind, visited)
169
+ definition = method_definition(owner, tail_name, kind)
170
+ if definition && every_overload_return?(definition, RBS::Types::Bases::Void)
171
+ return VoidOrigin.new(class_name: owner, method_name: tail_name, kind: kind)
172
+ end
173
+
174
+ next_def = discovered_def(owner, tail_name, kind)
175
+ return nil if next_def.nil?
176
+
177
+ walk(next_def, owner, kind, visited)
178
+ end
179
+
180
+ def method_definition(owner, method_name, kind)
181
+ if kind == :singleton
182
+ Reflection.singleton_method_definition(owner, method_name, environment: environment)
183
+ else
184
+ Reflection.instance_method_definition(owner, method_name, environment: environment)
185
+ end
186
+ end
187
+
188
+ def discovered_def(owner, method_name, kind)
189
+ if kind == :singleton
190
+ scope.singleton_def_for(owner, method_name)
191
+ else
192
+ scope.user_def_for(owner, method_name)
193
+ end
194
+ end
195
+
196
+ # True when the definition declares at least one overload and EVERY overload's return type is an
197
+ # instance of `base_klass` (`RBS::Types::Bases::Any` for admission 1, `...::Void` for the leaf).
198
+ # An `UntypedFunction` (`(?) -> T`) still responds to `return_type`, so it is handled the same way.
199
+ def every_overload_return?(definition, base_klass)
200
+ method_types = definition.method_types
201
+ return false if method_types.empty?
202
+
203
+ method_types.all? do |method_type|
204
+ fun = method_type.type
205
+ fun.respond_to?(:return_type) && fun.return_type.is_a?(base_klass)
206
+ end
207
+ end
208
+
209
+ def memo_bucket
210
+ generation = scope.run_generation || scope.discovered_def_nodes
211
+ slot = Thread.current[MEMO_KEY]
212
+ unless slot && slot[0].equal?(generation)
213
+ slot = [generation, {}.compare_by_identity]
214
+ Thread.current[MEMO_KEY] = slot
215
+ end
216
+ slot[1]
217
+ end
218
+ end
219
+ end
220
+ end
@@ -8,6 +8,7 @@ require_relative "../environment"
8
8
  require_relative "../reflection"
9
9
  require_relative "../scope"
10
10
  require_relative "../source/node_locator"
11
+ require_relative "../source/node_children"
11
12
  require_relative "../inference/scope_indexer"
12
13
  require_relative "../type/nominal"
13
14
  require_relative "../type/singleton"
@@ -187,7 +188,7 @@ module Rigor
187
188
  n.location.start_offset <= symbol_offset && symbol_offset <= n.location.end_offset
188
189
  result = n
189
190
  end
190
- n.compact_child_nodes.each(&walk)
191
+ n.rigor_each_child(&walk)
191
192
  end
192
193
  walk.call(root)
193
194
  result
@@ -4,6 +4,7 @@ require "prism"
4
4
 
5
5
  require_relative "uri"
6
6
  require_relative "buffer_resolution"
7
+ require_relative "../source/node_children"
7
8
 
8
9
  module Rigor
9
10
  module LanguageServer
@@ -67,7 +68,7 @@ module Rigor
67
68
  when Prism::DefNode
68
69
  block.call(def_symbol(node, in_namespace: in_namespace))
69
70
  else
70
- node.compact_child_nodes.each do |child|
71
+ node.rigor_each_child do |child|
71
72
  each_decl(child, in_namespace: in_namespace, &block)
72
73
  end
73
74
  end