rigortype 0.1.16 → 0.1.18

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -2
  3. data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
  4. data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
  5. data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +100 -0
  6. data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +226 -0
  7. data/lib/rigor/analysis/check_rules.rb +180 -73
  8. data/lib/rigor/analysis/dependency_recorder.rb +122 -0
  9. data/lib/rigor/analysis/diagnostic.rb +18 -0
  10. data/lib/rigor/analysis/incremental.rb +162 -0
  11. data/lib/rigor/analysis/incremental_session.rb +337 -0
  12. data/lib/rigor/analysis/rule_catalog.rb +48 -0
  13. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  14. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  15. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  16. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  17. data/lib/rigor/analysis/runner.rb +477 -1110
  18. data/lib/rigor/analysis/self_call_resolution_recorder.rb +121 -0
  19. data/lib/rigor/analysis/worker_session.rb +47 -8
  20. data/lib/rigor/builtins/static_return_refinements.rb +7 -1
  21. data/lib/rigor/cache/descriptor.rb +50 -49
  22. data/lib/rigor/cache/incremental_snapshot.rb +153 -0
  23. data/lib/rigor/cache/rbs_cache_producer.rb +34 -0
  24. data/lib/rigor/cache/rbs_class_ancestor_table.rb +2 -8
  25. data/lib/rigor/cache/rbs_class_type_param_names.rb +2 -8
  26. data/lib/rigor/cache/rbs_constant_table.rb +2 -8
  27. data/lib/rigor/cache/rbs_environment.rb +2 -8
  28. data/lib/rigor/cache/rbs_known_class_names.rb +2 -8
  29. data/lib/rigor/cache/store.rb +145 -14
  30. data/lib/rigor/cli/annotate_command.rb +2 -7
  31. data/lib/rigor/cli/baseline_command.rb +2 -7
  32. data/lib/rigor/cli/check_command.rb +705 -0
  33. data/lib/rigor/cli/ci_detector.rb +94 -0
  34. data/lib/rigor/cli/command.rb +47 -0
  35. data/lib/rigor/cli/coverage_command.rb +3 -23
  36. data/lib/rigor/cli/coverage_renderer.rb +3 -8
  37. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  38. data/lib/rigor/cli/diff_command.rb +3 -7
  39. data/lib/rigor/cli/explain_command.rb +2 -7
  40. data/lib/rigor/cli/lsp_command.rb +3 -7
  41. data/lib/rigor/cli/mcp_command.rb +3 -7
  42. data/lib/rigor/cli/options.rb +57 -0
  43. data/lib/rigor/cli/plugin_command.rb +3 -7
  44. data/lib/rigor/cli/plugins_command.rb +2 -7
  45. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  46. data/lib/rigor/cli/renderable.rb +26 -0
  47. data/lib/rigor/cli/sig_gen_command.rb +2 -7
  48. data/lib/rigor/cli/skill_command.rb +3 -7
  49. data/lib/rigor/cli/trace_command.rb +143 -0
  50. data/lib/rigor/cli/trace_renderer.rb +310 -0
  51. data/lib/rigor/cli/triage_command.rb +2 -7
  52. data/lib/rigor/cli/type_of_command.rb +5 -38
  53. data/lib/rigor/cli/type_of_renderer.rb +4 -9
  54. data/lib/rigor/cli/type_scan_command.rb +3 -23
  55. data/lib/rigor/cli/type_scan_renderer.rb +4 -9
  56. data/lib/rigor/cli.rb +15 -532
  57. data/lib/rigor/configuration/dependencies.rb +18 -1
  58. data/lib/rigor/configuration/severity_profile.rb +22 -3
  59. data/lib/rigor/configuration.rb +16 -3
  60. data/lib/rigor/environment/rbs_loader.rb +129 -71
  61. data/lib/rigor/environment.rb +1 -1
  62. data/lib/rigor/inference/acceptance.rb +10 -0
  63. data/lib/rigor/inference/block_parameter_binder.rb +1 -2
  64. data/lib/rigor/inference/builtins/array_catalog.rb +2 -5
  65. data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -5
  66. data/lib/rigor/inference/builtins/complex_catalog.rb +2 -5
  67. data/lib/rigor/inference/builtins/date_catalog.rb +2 -5
  68. data/lib/rigor/inference/builtins/encoding_catalog.rb +2 -5
  69. data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -5
  70. data/lib/rigor/inference/builtins/exception_catalog.rb +2 -5
  71. data/lib/rigor/inference/builtins/hash_catalog.rb +2 -5
  72. data/lib/rigor/inference/builtins/method_catalog.rb +15 -0
  73. data/lib/rigor/inference/builtins/numeric_catalog.rb +21 -93
  74. data/lib/rigor/inference/builtins/pathname_catalog.rb +2 -5
  75. data/lib/rigor/inference/builtins/proc_catalog.rb +2 -5
  76. data/lib/rigor/inference/builtins/random_catalog.rb +2 -5
  77. data/lib/rigor/inference/builtins/range_catalog.rb +2 -5
  78. data/lib/rigor/inference/builtins/rational_catalog.rb +2 -5
  79. data/lib/rigor/inference/builtins/re_catalog.rb +2 -5
  80. data/lib/rigor/inference/builtins/set_catalog.rb +2 -5
  81. data/lib/rigor/inference/builtins/string_catalog.rb +2 -5
  82. data/lib/rigor/inference/builtins/struct_catalog.rb +2 -5
  83. data/lib/rigor/inference/builtins/time_catalog.rb +2 -5
  84. data/lib/rigor/inference/expression_typer.rb +149 -63
  85. data/lib/rigor/inference/flow_tracer.rb +180 -0
  86. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  87. data/lib/rigor/inference/method_dispatcher/block_folding.rb +5 -1
  88. data/lib/rigor/inference/method_dispatcher/call_context.rb +65 -0
  89. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +11 -10
  90. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +12 -6
  91. data/lib/rigor/inference/method_dispatcher/data_folding.rb +246 -0
  92. data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -2
  93. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -2
  94. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -1
  95. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +4 -1
  96. data/lib/rigor/inference/method_dispatcher/math_folding.rb +6 -6
  97. data/lib/rigor/inference/method_dispatcher/method_folding.rb +12 -7
  98. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  99. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +23 -13
  100. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +9 -9
  101. data/lib/rigor/inference/method_dispatcher/set_folding.rb +6 -6
  102. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +120 -9
  103. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +12 -12
  104. data/lib/rigor/inference/method_dispatcher/singleton_folding.rb +49 -0
  105. data/lib/rigor/inference/method_dispatcher/time_folding.rb +6 -6
  106. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +9 -9
  107. data/lib/rigor/inference/method_dispatcher.rb +185 -84
  108. data/lib/rigor/inference/narrowing.rb +262 -5
  109. data/lib/rigor/inference/scope_indexer.rb +208 -21
  110. data/lib/rigor/inference/statement_evaluator.rb +110 -48
  111. data/lib/rigor/language_server/buffer_resolution.rb +33 -0
  112. data/lib/rigor/language_server/completion_provider.rb +4 -4
  113. data/lib/rigor/language_server/document_symbol_provider.rb +4 -4
  114. data/lib/rigor/language_server/folding_range_provider.rb +4 -4
  115. data/lib/rigor/language_server/hover_provider.rb +4 -4
  116. data/lib/rigor/language_server/selection_range_provider.rb +4 -4
  117. data/lib/rigor/language_server/signature_help_provider.rb +4 -4
  118. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  119. data/lib/rigor/plugin/base.rb +302 -45
  120. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  121. data/lib/rigor/plugin/registry.rb +281 -15
  122. data/lib/rigor/plugin.rb +1 -0
  123. data/lib/rigor/rbs_extended/conformance_checker.rb +293 -0
  124. data/lib/rigor/rbs_extended.rb +39 -0
  125. data/lib/rigor/scope/discovery_index.rb +58 -0
  126. data/lib/rigor/scope.rb +150 -167
  127. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  128. data/lib/rigor/source/literals.rb +14 -0
  129. data/lib/rigor/type/acceptance_router.rb +19 -0
  130. data/lib/rigor/type/accepts_result.rb +3 -10
  131. data/lib/rigor/type/app.rb +3 -7
  132. data/lib/rigor/type/bot.rb +2 -3
  133. data/lib/rigor/type/bound_method.rb +5 -12
  134. data/lib/rigor/type/combinator.rb +22 -0
  135. data/lib/rigor/type/constant.rb +2 -3
  136. data/lib/rigor/type/data_class.rb +80 -0
  137. data/lib/rigor/type/data_instance.rb +100 -0
  138. data/lib/rigor/type/difference.rb +5 -10
  139. data/lib/rigor/type/dynamic.rb +5 -10
  140. data/lib/rigor/type/hash_shape.rb +5 -15
  141. data/lib/rigor/type/integer_range.rb +5 -10
  142. data/lib/rigor/type/intersection.rb +5 -10
  143. data/lib/rigor/type/nominal.rb +5 -10
  144. data/lib/rigor/type/refined.rb +5 -10
  145. data/lib/rigor/type/singleton.rb +5 -10
  146. data/lib/rigor/type/top.rb +2 -3
  147. data/lib/rigor/type/tuple.rb +5 -10
  148. data/lib/rigor/type/union.rb +5 -10
  149. data/lib/rigor/type.rb +2 -0
  150. data/lib/rigor/value_semantics.rb +77 -0
  151. data/lib/rigor/version.rb +1 -1
  152. data/lib/rigor.rb +1 -1
  153. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  154. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  155. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  156. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  157. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  158. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  159. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  160. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  161. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +12 -2
  162. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  163. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  164. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  165. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  166. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  167. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  168. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  169. data/sig/rigor/cache.rbs +19 -0
  170. data/sig/rigor/environment.rbs +0 -2
  171. data/sig/rigor/inference.rbs +27 -0
  172. data/sig/rigor/plugin/base.rbs +1 -2
  173. data/sig/rigor/rbs_extended.rbs +2 -0
  174. data/sig/rigor/scope.rbs +42 -25
  175. data/sig/rigor/source.rbs +1 -0
  176. data/sig/rigor/type.rbs +58 -1
  177. data/sig/rigor.rbs +6 -1
  178. data/skills/rigor-ci-setup/SKILL.md +319 -0
  179. metadata +36 -2
  180. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -79
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
  require_relative "../../type"
5
+ require_relative "singleton_folding"
5
6
 
6
7
  module Rigor
7
8
  module Inference
@@ -40,24 +41,23 @@ module Rigor
40
41
  module_function
41
42
 
42
43
  # @return [Rigor::Type, nil] folded result, or nil to defer.
43
- def try_dispatch(receiver:, method_name:, args:)
44
- return nil unless dispatch_target?(receiver)
44
+ def try_dispatch(context)
45
+ receiver = context.receiver
46
+ method_name = context.method_name
47
+ args = context.args
48
+ return nil unless SingletonFolding.receiver?(receiver, "URI")
45
49
  return nil unless URI_COMPONENT_METHODS.include?(method_name)
46
50
 
47
51
  fold_uri_call(method_name, args)
48
52
  end
49
53
 
50
- def dispatch_target?(receiver)
51
- receiver.is_a?(Type::Singleton) && receiver.class_name == "URI"
52
- end
53
-
54
54
  def fold_uri_call(method_name, args)
55
55
  return nil unless args.size == 1
56
56
 
57
- arg = args.first
58
- return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
57
+ str = SingletonFolding.constant_string(args.first)
58
+ return nil if str.nil?
59
59
 
60
- Type::Combinator.constant_of(URI.public_send(method_name, arg.value))
60
+ Type::Combinator.constant_of(URI.public_send(method_name, str))
61
61
  rescue StandardError
62
62
  nil
63
63
  end
@@ -6,9 +6,12 @@ require_relative "../flow_contribution"
6
6
  require_relative "../flow_contribution/merger"
7
7
  require_relative "../builtins/hkt_builtins"
8
8
  require_relative "../builtins/static_return_refinements"
9
+ require_relative "flow_tracer"
10
+ require_relative "method_dispatcher/call_context"
9
11
  require_relative "method_dispatcher/constant_folding"
10
12
  require_relative "method_dispatcher/literal_string_folding"
11
13
  require_relative "method_dispatcher/shape_dispatch"
14
+ require_relative "method_dispatcher/data_folding"
12
15
  require_relative "method_dispatcher/rbs_dispatch"
13
16
  require_relative "method_dispatcher/iterator_dispatch"
14
17
  require_relative "method_dispatcher/block_folding"
@@ -71,19 +74,45 @@ module Rigor
71
74
  # @param environment [Rigor::Environment, nil] required for
72
75
  # RBS-backed dispatch; when nil only constant folding can fire.
73
76
  # @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
74
- def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
+ def dispatch(receiver_type:, method_name:, arg_types:,
75
78
  block_type: nil, environment: nil,
76
79
  call_node: nil, scope: nil)
80
+ result = resolve(
81
+ receiver_type: receiver_type, method_name: method_name, arg_types: arg_types,
82
+ block_type: block_type, environment: environment,
83
+ call_node: call_node, scope: scope
84
+ )
85
+ # `rigor trace` — record the dispatch outcome (resolved type, or
86
+ # the fail-soft `nil` the caller widens to `Dynamic[Top]`).
87
+ if FlowTracer.active?
88
+ FlowTracer.dispatch(
89
+ receiver: receiver_type, method_name: method_name, args: arg_types,
90
+ result: result, location: call_node&.location
91
+ )
92
+ end
93
+ result
94
+ end
95
+
96
+ def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
+ block_type: nil, environment: nil,
98
+ call_node: nil, scope: nil)
77
99
  return nil if receiver_type.nil?
78
100
 
79
- bound_method_result = MethodFolding.try_backward(
101
+ # Build the call context once and thread it — unchanged —
102
+ # through every tier (`_DispatchTier#try_dispatch`). The
103
+ # dispatcher's own private fallback tiers still read the
104
+ # positional locals below; only the tier modules consume the
105
+ # context object.
106
+ context = CallContext.build(
80
107
  receiver: receiver_type, method_name: method_name, args: arg_types,
81
108
  block_type: block_type, environment: environment,
82
109
  call_node: call_node, scope: scope
83
110
  )
111
+
112
+ bound_method_result = MethodFolding.try_backward(context)
84
113
  return bound_method_result if bound_method_result
85
114
 
86
- precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
115
+ precise = dispatch_precise_tiers(context)
87
116
  return precise if precise
88
117
 
89
118
  # v0.1.1 Track 2 slice 7 — plugin return-type contribution
@@ -123,10 +152,7 @@ module Rigor
123
152
  static_refinement = try_static_refinement(receiver_type, method_name, arg_types)
124
153
  return static_refinement if static_refinement
125
154
 
126
- rbs_result = RbsDispatch.try_dispatch(
127
- receiver: receiver_type, method_name: method_name, args: arg_types,
128
- environment: environment, block_type: block_type, scope: scope
129
- )
155
+ rbs_result = RbsDispatch.try_dispatch(context)
130
156
  if rbs_result
131
157
  record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
132
158
  return rbs_result
@@ -211,7 +237,7 @@ module Rigor
211
237
  # introspection (`attr_reader`, `private`, ...) on
212
238
  # user classes without requiring the user to author
213
239
  # their own RBS.
214
- try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node)
240
+ try_user_class_fallback(receiver_type, environment, call_node, context)
215
241
  end
216
242
 
217
243
  # v0.1.3 — discovered-method dispatch tier. `scope` carries
@@ -292,12 +318,12 @@ module Rigor
292
318
  end
293
319
 
294
320
  # ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
295
- # slice 7. Walks every loaded plugin's
296
- # `#flow_contribution_for(call_node:, scope:)` hook,
297
- # collects the non-nil `FlowContribution` bundles, merges
298
- # them through `FlowContribution::Merger`, and returns
299
- # the merged `return_type` slot (or nil when no plugin
300
- # contributed a return type).
321
+ # slice 7; ADR-52 WD3 — consults each loaded plugin's gated
322
+ # `dynamic_return` rules, wraps the contributed types as
323
+ # `FlowContribution` bundles, merges them through
324
+ # `FlowContribution::Merger`, and returns the merged
325
+ # `return_type` slot (or nil when no plugin contributed a
326
+ # return type).
301
327
  #
302
328
  # Plugins whose hook raises have their contribution
303
329
  # silently dropped for this call so the dispatch chain
@@ -467,8 +493,10 @@ module Rigor
467
493
 
468
494
  module_type = Type::Combinator.nominal_of(module_name)
469
495
  RbsDispatch.try_dispatch(
470
- receiver: module_type, method_name: method_name, args: arg_types,
471
- environment: environment, block_type: block_type
496
+ CallContext.build(
497
+ receiver: module_type, method_name: method_name, args: arg_types,
498
+ environment: environment, block_type: block_type
499
+ )
472
500
  )
473
501
  end
474
502
 
@@ -635,23 +663,15 @@ module Rigor
635
663
  Type::Combinator.untyped
636
664
  end
637
665
 
666
+ # ADR-52 WD1 — the per-dispatch plugins × owns_receivers ×
667
+ # `class_ordering` walk moved into the compiled contribution
668
+ # table: the union is built once per registry (almost always
669
+ # empty → O(1) false) and per-class verdicts memoise per run.
638
670
  def plugin_owns_receiver?(class_name, environment)
639
671
  registry = environment&.plugin_registry
640
672
  return false if registry.nil? || registry.empty?
641
673
 
642
- registry.plugins.any? do |plugin|
643
- owns = plugin.manifest.owns_receivers # rigor:disable undefined-method
644
- owns.any? { |owner| receiver_matches_owner?(class_name, owner, environment) }
645
- end
646
- end
647
-
648
- def receiver_matches_owner?(class_name, owner, environment)
649
- return true if class_name == owner
650
-
651
- ordering = environment.class_ordering(class_name, owner)
652
- %i[equal subclass].include?(ordering)
653
- rescue StandardError
654
- false
674
+ registry.contribution_index.owns_receiver?(class_name, environment)
655
675
  end
656
676
 
657
677
  def dep_source_class_name(receiver_type)
@@ -660,22 +680,66 @@ module Rigor
660
680
  end
661
681
  end
662
682
 
663
- # ADR-37 slice 2 — gathers each plugin's return-type contribution
664
- # from BOTH the narrow `dynamic_return` DSL (receiver-gated, wrapped
665
- # as a return-only `FlowContribution`) and the legacy
666
- # `flow_contribution_for` escape valve, so migrated and unmigrated
667
- # plugins compose through the same merger.
683
+ # ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's return-type
684
+ # contribution from the gated `dynamic_return` DSL, wrapped as a
685
+ # return-only `FlowContribution` for the shared merger. (The legacy
686
+ # ungated `flow_contribution_for` escape valve was deleted once its
687
+ # five users migrated.)
688
+ EMPTY_CONTRIBUTIONS = [].freeze
689
+ private_constant :EMPTY_CONTRIBUTIONS
690
+
691
+ # Collects every plugin's flow / dynamic-return contribution for one
692
+ # call site. Two prunings keep this off the hot path on plugin-heavy
693
+ # projects (it was the #1 allocation site and a top CPU cost):
694
+ #
695
+ # 1. Only the plugins that *structurally* implement a per-call path
696
+ # are visited — `registry.contribution_index.for_method_dispatch`
697
+ # is the registry-ordered subset declaring a `dynamic_return`.
698
+ # Iterating the subset in registry order, and gating each path by
699
+ # membership, yields the exact same contributions in the same
700
+ # order as visiting every plugin would (a skipped plugin's call
701
+ # returns nil/[] anyway). The receiver-class ancestry match still
702
+ # happens per dispatch inside `dynamic_return_type`.
703
+ # 2. Contributions accumulate lazily — allocate only when one
704
+ # actually appears, and share a frozen empty array otherwise. The
705
+ # caller treats the result as read-only (`.empty?` / `Merger.merge`).
706
+ # 3. ADR-52 WD1 — method-name gates compiled at registry build. The
707
+ # global gate makes the common "no plugin cares about this call"
708
+ # case a single Set probe; the per-plugin gate skips a plugin
709
+ # whose `dynamic_return` rules are all `methods:`-gated on other
710
+ # names. A pruned consultation could only have returned nil, so
711
+ # contribution order and content are unchanged.
668
712
  def collect_plugin_contributions(registry, call_node, scope, receiver_type)
669
- registry.plugins.flat_map do |plugin|
670
- contributions = []
671
- legacy = plugin.flow_contribution_for(call_node: call_node, scope: scope)
672
- contributions << legacy if legacy.is_a?(FlowContribution)
713
+ index = registry.contribution_index
714
+ relevant = index.for_method_dispatch
715
+ return EMPTY_CONTRIBUTIONS if relevant.empty?
716
+
717
+ # `call_node` is not always a CallNode — the `&:symbol` block
718
+ # path dispatches with the `Prism::BlockArgumentNode` itself
719
+ # (`ExpressionTyper#symbol_block_return_type`). A bare `.name`
720
+ # here raised, and the raise was silently absorbed by
721
+ # `block_return_type_for`'s rescue, nil-ing the block type and
722
+ # flipping `select(&:p)`-style calls onto their no-block
723
+ # Enumerator overloads (caught by the GitLab corpus gate).
724
+ name = call_node.respond_to?(:name) ? call_node.name : nil
725
+ return EMPTY_CONTRIBUTIONS unless index.dispatch_candidate?(name)
726
+
727
+ collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
728
+ end
729
+
730
+ # The post-gate walk, in registry order — the same order the
731
+ # ungated walk used.
732
+ def collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
733
+ result = nil
734
+ relevant.each do |plugin|
735
+ next unless index.dynamic_candidate_for?(plugin, name)
736
+
673
737
  dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
674
- contributions << FlowContribution.new(return_type: dynamic) if dynamic
675
- contributions
738
+ (result ||= []) << FlowContribution.new(return_type: dynamic) if dynamic
676
739
  rescue StandardError
677
- []
740
+ next
678
741
  end
742
+ result || EMPTY_CONTRIBUTIONS
679
743
  end
680
744
 
681
745
  # Runs the precision tiers (constant fold, shape dispatch,
@@ -690,37 +754,80 @@ module Rigor
690
754
  # its rules apply only to block-taking calls, so the cheaper
691
755
  # arity-based fold tiers above it filter out the common
692
756
  # cases first. When `block_type` is nil the tier is a no-op.
693
- def dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil)
694
- meta_result = try_meta_introspection(receiver_type, method_name, arg_types)
757
+ # The precise-tier folders, consulted in order via the uniform
758
+ # `_DispatchTier` interface (`try_dispatch(CallContext) -> Type?`).
759
+ # Order is significant: ConstantFolding's exact-value folds win
760
+ # first, the eight stdlib singleton folders sit in the middle (each
761
+ # gates on a distinct `Singleton` receiver, so their relative order
762
+ # is immaterial), and BlockFolding runs last because its rules only
763
+ # apply to block-taking calls — the cheaper arity folds above it
764
+ # filter the common cases first. Adding a precise tier is a
765
+ # one-line append here rather than another link in a hand-written
766
+ # `||` ladder.
767
+ PRECISE_TIERS_HEAD = Ractor.make_shareable([
768
+ ConstantFolding, LiteralStringFolding, ShapeDispatch
769
+ ].freeze)
770
+ private_constant :PRECISE_TIERS_HEAD
771
+
772
+ # ADR-53 re-review follow-up (gate-by-held-key applied to the
773
+ # built-in tiers): the eight stdlib singleton folders are mutually
774
+ # exclusive — each fires only on `Singleton[<its class>]`, the
775
+ # first check in every `try_dispatch` — so at most one can match a
776
+ # given receiver and their relative trial order was never
777
+ # observable. Compiling them into a class-name table turns eight
778
+ # no-op trials per call into one Hash read, skipped entirely when
779
+ # the receiver is not a `Singleton` (the overwhelmingly common
780
+ # case). The table sits where the eight sat in the old flat list:
781
+ # after ShapeDispatch, before KernelDispatch.
782
+ STDLIB_SINGLETON_FOLDERS = Ractor.make_shareable({
783
+ "File" => FileFolding,
784
+ "Shellwords" => ShellwordsFolding,
785
+ "Math" => MathFolding,
786
+ "Time" => TimeFolding,
787
+ "Regexp" => RegexpFolding,
788
+ "CGI" => CGIFolding,
789
+ "URI" => URIFolding,
790
+ "Set" => SetFolding
791
+ }.freeze)
792
+ private_constant :STDLIB_SINGLETON_FOLDERS
793
+
794
+ PRECISE_TIERS_TAIL = Ractor.make_shareable([
795
+ KernelDispatch, MethodFolding, BlockFolding
796
+ ].freeze)
797
+ private_constant :PRECISE_TIERS_TAIL
798
+
799
+ def dispatch_precise_tiers(context)
800
+ # ADR-48 — Data value folding runs ahead of meta-introspection:
801
+ # `meta_new` intercepts every `Singleton[*].new` (returning
802
+ # `Nominal`), which would mask a `Data` class's precise instance.
803
+ # The tier only fires on Data receivers (a `Data.define`, a
804
+ # `DataClass`/`DataInstance`, or a `Singleton` with a recorded
805
+ # member layout), so it never shadows meta's Array/Set/Range lifts.
806
+ data_result = DataFolding.try_dispatch(context)
807
+ return data_result if data_result
808
+
809
+ meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
695
810
  return meta_result if meta_result
696
811
 
697
- ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
698
- LiteralStringFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
699
- ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
700
- dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types) ||
701
- KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
702
- MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
703
- BlockFolding.try_fold(
704
- receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
705
- )
706
- end
812
+ PRECISE_TIERS_HEAD.each do |tier|
813
+ result = tier.try_dispatch(context)
814
+ return result if result
815
+ end
816
+
817
+ receiver = context.receiver
818
+ if receiver.is_a?(Type::Singleton) && (folder = STDLIB_SINGLETON_FOLDERS[receiver.class_name])
819
+ result = folder.try_dispatch(context)
820
+ return result if result
821
+ end
707
822
 
708
- # Stdlib module singleton-folding tiers: File, Shellwords, Math,
709
- # Time, Regexp, CGI, URI, Set. Extracted from
710
- # `dispatch_precise_tiers` to keep the parent method within the
711
- # cyclomatic-complexity limit.
712
- def dispatch_stdlib_module_tiers(receiver_type, method_name, arg_types)
713
- FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
714
- ShellwordsFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
715
- MathFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
716
- TimeFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
717
- RegexpFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
718
- CGIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
719
- URIFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
720
- SetFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
823
+ PRECISE_TIERS_TAIL.each do |tier|
824
+ result = tier.try_dispatch(context)
825
+ return result if result
826
+ end
827
+ nil
721
828
  end
722
829
 
723
- def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type, call_node = nil)
830
+ def try_user_class_fallback(receiver_type, environment, call_node, context)
724
831
  return nil if environment.nil?
725
832
 
726
833
  fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
@@ -745,13 +852,12 @@ module Rigor
745
852
  # self / `self.`-receiver calls (`puts`, `raise`, `require`)
746
853
  # keep resolving — those are the fallback's intended targets.
747
854
  RbsDispatch.try_dispatch(
748
- receiver: fallback_receiver,
749
- method_name: method_name,
750
- args: arg_types,
751
- environment: environment,
752
- block_type: block_type,
753
- self_type_override: receiver_type,
754
- public_only: explicit_non_self_receiver?(call_node)
855
+ context.with(
856
+ receiver: fallback_receiver,
857
+ self_type_override: receiver_type,
858
+ public_only: explicit_non_self_receiver?(call_node),
859
+ call_node: nil, scope: nil
860
+ )
755
861
  )
756
862
  end
757
863
 
@@ -1093,19 +1199,14 @@ module Rigor
1093
1199
  def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil)
1094
1200
  return [] if receiver_type.nil?
1095
1201
 
1096
- iterator_result = IteratorDispatch.block_param_types(
1097
- receiver: receiver_type,
1098
- method_name: method_name,
1099
- args: arg_types
1202
+ context = CallContext.build(
1203
+ receiver: receiver_type, method_name: method_name,
1204
+ args: arg_types, environment: environment
1100
1205
  )
1206
+ iterator_result = IteratorDispatch.block_param_types(context)
1101
1207
  return iterator_result if iterator_result
1102
1208
 
1103
- RbsDispatch.block_param_types(
1104
- receiver: receiver_type,
1105
- method_name: method_name,
1106
- args: arg_types,
1107
- environment: environment
1108
- )
1209
+ RbsDispatch.block_param_types(context)
1109
1210
  end
1110
1211
  end
1111
1212
  end