rigortype 0.1.2 → 0.1.4

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +135 -31
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
  9. data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +11 -3
  12. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  13. data/lib/rigor/analysis/runner.rb +206 -6
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +59 -6
  16. data/lib/rigor/cache/store.rb +1 -1
  17. data/lib/rigor/cli/diff_command.rb +1 -1
  18. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  19. data/lib/rigor/cli/type_of_command.rb +1 -1
  20. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  21. data/lib/rigor/cli/type_scan_report.rb +2 -2
  22. data/lib/rigor/cli.rb +9 -1
  23. data/lib/rigor/configuration/dependencies.rb +235 -0
  24. data/lib/rigor/configuration.rb +45 -11
  25. data/lib/rigor/environment.rb +47 -4
  26. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  27. data/lib/rigor/flow_contribution/element.rb +1 -1
  28. data/lib/rigor/flow_contribution/fact.rb +1 -1
  29. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  30. data/lib/rigor/flow_contribution/merger.rb +7 -3
  31. data/lib/rigor/flow_contribution.rb +2 -2
  32. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  33. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  34. data/lib/rigor/inference/expression_typer.rb +67 -11
  35. data/lib/rigor/inference/fallback.rb +1 -1
  36. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  37. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
  38. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  39. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  40. data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
  41. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
  42. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  43. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
  44. data/lib/rigor/inference/method_dispatcher.rb +233 -2
  45. data/lib/rigor/inference/method_parameter_binder.rb +1 -3
  46. data/lib/rigor/inference/narrowing.rb +2 -4
  47. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  48. data/lib/rigor/inference/scope_indexer.rb +14 -9
  49. data/lib/rigor/inference/statement_evaluator.rb +70 -6
  50. data/lib/rigor/plugin/io_boundary.rb +0 -2
  51. data/lib/rigor/plugin/loader.rb +2 -2
  52. data/lib/rigor/plugin/manifest.rb +49 -7
  53. data/lib/rigor/plugin/registry.rb +11 -0
  54. data/lib/rigor/plugin/services.rb +1 -1
  55. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  56. data/lib/rigor/plugin.rb +1 -0
  57. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  58. data/lib/rigor/rbs_extended.rb +131 -32
  59. data/lib/rigor/scope.rb +25 -8
  60. data/lib/rigor/sig_gen/classification.rb +36 -0
  61. data/lib/rigor/sig_gen/generator.rb +1048 -0
  62. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  63. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  64. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  65. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  66. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  67. data/lib/rigor/sig_gen/renderer.rb +157 -0
  68. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  69. data/lib/rigor/sig_gen/write_result.rb +48 -0
  70. data/lib/rigor/sig_gen/writer.rb +530 -0
  71. data/lib/rigor/sig_gen.rb +25 -0
  72. data/lib/rigor/type/bound_method.rb +79 -0
  73. data/lib/rigor/type/combinator.rb +195 -2
  74. data/lib/rigor/type/constant.rb +13 -0
  75. data/lib/rigor/type/hash_shape.rb +0 -2
  76. data/lib/rigor/type/union.rb +20 -1
  77. data/lib/rigor/type.rb +1 -0
  78. data/lib/rigor/type_node/generic.rb +62 -0
  79. data/lib/rigor/type_node/identifier.rb +30 -0
  80. data/lib/rigor/type_node/indexed_access.rb +41 -0
  81. data/lib/rigor/type_node/integer_literal.rb +29 -0
  82. data/lib/rigor/type_node/name_scope.rb +52 -0
  83. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  84. data/lib/rigor/type_node/string_literal.rb +29 -0
  85. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  86. data/lib/rigor/type_node/union.rb +42 -0
  87. data/lib/rigor/type_node.rb +29 -0
  88. data/lib/rigor/version.rb +1 -1
  89. data/lib/rigor.rb +2 -0
  90. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  91. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  92. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  93. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  94. data/sig/rigor/cli/diff_command.rbs +4 -0
  95. data/sig/rigor/cli/explain_command.rbs +4 -0
  96. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  97. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  98. data/sig/rigor/environment.rbs +6 -2
  99. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  100. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  101. data/sig/rigor/inference/builtins.rbs +2 -0
  102. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  103. data/sig/rigor/plugin/base.rbs +6 -0
  104. data/sig/rigor/plugin/fact_store.rbs +11 -0
  105. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  106. data/sig/rigor/plugin/load_error.rbs +6 -0
  107. data/sig/rigor/plugin/loader.rbs +20 -0
  108. data/sig/rigor/plugin/manifest.rbs +9 -0
  109. data/sig/rigor/plugin/registry.rbs +3 -0
  110. data/sig/rigor/plugin/services.rbs +3 -0
  111. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  112. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  113. data/sig/rigor/plugin.rbs +8 -0
  114. data/sig/rigor/scope.rbs +4 -2
  115. data/sig/rigor/type.rbs +28 -6
  116. metadata +58 -1
@@ -162,7 +162,8 @@ module Rigor
162
162
  kind: kind,
163
163
  args: args,
164
164
  type_vars: type_vars,
165
- block_type: block_type
165
+ block_type: block_type,
166
+ environment: environment
166
167
  )
167
168
  rescue StandardError
168
169
  # Defensive: if RBS' definition builder raises on a broken
@@ -194,6 +195,18 @@ module Rigor
194
195
  ["Array", :instance, tuple_type_args(receiver)]
195
196
  when Type::HashShape
196
197
  ["Hash", :instance, hash_shape_type_args(receiver)]
198
+ when Type::BoundMethod
199
+ # `BoundMethod` is a precision-bearing alias for
200
+ # `Nominal[Method]`: it carries the
201
+ # `(receiver, method_name)` binding that
202
+ # `MethodFolding.try_backward` consumes at
203
+ # `.call` / `.()` / `[]`, but every other call
204
+ # site (`.owner` / `.name` / `.arity` / …) must
205
+ # still resolve through Method's RBS contract.
206
+ # Routing here keeps reflective Method methods
207
+ # working without forcing the carrier to
208
+ # collapse to a plain Nominal at construction.
209
+ ["Method", :instance, []]
197
210
  when Type::Dynamic
198
211
  receiver_descriptor(receiver.static_facet)
199
212
  end
@@ -241,15 +254,15 @@ module Rigor
241
254
  param_names.zip(receiver_args).to_h
242
255
  end
243
256
 
244
- # rubocop:disable Metrics/ParameterLists
245
- def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:)
257
+ def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:,
258
+ environment: nil)
246
259
  # Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
247
260
  # return-type override through the merger so future
248
261
  # plugin / `:rbs_extended` bundles that also assert a
249
262
  # `return_type` slot at this call site compose with
250
263
  # the RBS::Extended directive instead of silently
251
264
  # racing it.
252
- override = merged_return_type(method_definition)
265
+ override = merged_return_type(method_definition, environment: environment)
253
266
  return override if override
254
267
 
255
268
  instance_type = Type::Combinator.nominal_of(class_name)
@@ -265,7 +278,8 @@ module Rigor
265
278
  self_type: self_type,
266
279
  instance_type: instance_type,
267
280
  type_vars: type_vars,
268
- block_required: !block_type.nil?
281
+ block_required: !block_type.nil?,
282
+ environment: environment
269
283
  )
270
284
  return nil unless method_type
271
285
 
@@ -278,7 +292,6 @@ module Rigor
278
292
  type_vars: full_type_vars
279
293
  )
280
294
  end
281
- # rubocop:enable Metrics/ParameterLists
282
295
 
283
296
  # ADR-7 § "Slice 4-A/4-B" — folds the
284
297
  # `RBS::Extended` `return:` directive (and any
@@ -287,8 +300,8 @@ module Rigor
287
300
  # before consuming. Returns the merged return type
288
301
  # or nil when no contribution overrides the
289
302
  # RBS-declared return.
290
- def merged_return_type(method_definition)
291
- contribution = RbsExtended.read_flow_contribution(method_definition)
303
+ def merged_return_type(method_definition, environment: nil)
304
+ contribution = RbsExtended.read_flow_contribution(method_definition, environment: environment)
292
305
  return nil if contribution.nil?
293
306
 
294
307
  Rigor::FlowContribution::Merger.merge([contribution]).return_type
@@ -377,13 +390,15 @@ module Rigor
377
390
  class_name: class_name,
378
391
  kind: kind,
379
392
  args: args,
380
- type_vars: type_vars
393
+ type_vars: type_vars,
394
+ environment: environment
381
395
  )
382
396
  rescue StandardError
383
397
  []
384
398
  end
385
399
 
386
- def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:)
400
+ def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:,
401
+ environment: nil)
387
402
  instance_type = Type::Combinator.nominal_of(class_name)
388
403
  self_type =
389
404
  case kind
@@ -397,7 +412,8 @@ module Rigor
397
412
  self_type: self_type,
398
413
  instance_type: instance_type,
399
414
  type_vars: type_vars,
400
- block_required: true
415
+ block_required: true,
416
+ environment: environment
401
417
  )
402
418
  return [] unless method_type
403
419
 
@@ -626,7 +626,6 @@ module Rigor
626
626
  # (so it can serve as a Hash key). Produces a closed
627
627
  # `HashShape` whose entries mirror the per-position
628
628
  # pairs. Empty Tuples fold to the empty HashShape.
629
- # rubocop:disable Metrics/CyclomaticComplexity
630
629
  def tuple_to_h(tuple, _method_name, args)
631
630
  return nil unless args.empty?
632
631
  return Type::Combinator.hash_shape_of({}) if tuple.elements.empty?
@@ -637,7 +636,6 @@ module Rigor
637
636
 
638
637
  Type::Combinator.hash_shape_of(pairs.to_h)
639
638
  end
640
- # rubocop:enable Metrics/CyclomaticComplexity
641
639
 
642
640
  def tuple_to_h_pair(element)
643
641
  return nil unless element.is_a?(Type::Tuple)
@@ -865,7 +863,6 @@ module Rigor
865
863
  # `HashShape` accepts as keys). Duplicate values would
866
864
  # alias under inversion, so Rigor declines on
867
865
  # collisions rather than silently dropping entries.
868
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
869
866
  def hash_invert(shape, _method_name, args)
870
867
  return nil unless args.empty?
871
868
  return nil unless shape.closed?
@@ -880,7 +877,6 @@ module Rigor
880
877
  end
881
878
  Type::Combinator.hash_shape_of(inverted)
882
879
  end
883
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
884
880
 
885
881
  # `shape.first` — returns the first `[k, v]` pair as a
886
882
  # 2-Tuple, or `Constant[nil]` when the shape is empty.
@@ -12,6 +12,7 @@ require_relative "method_dispatcher/iterator_dispatch"
12
12
  require_relative "method_dispatcher/block_folding"
13
13
  require_relative "method_dispatcher/file_folding"
14
14
  require_relative "method_dispatcher/kernel_dispatch"
15
+ require_relative "method_dispatcher/method_folding"
15
16
 
16
17
  module Rigor
17
18
  module Inference
@@ -61,11 +62,18 @@ module Rigor
61
62
  # @param environment [Rigor::Environment, nil] required for
62
63
  # RBS-backed dispatch; when nil only constant folding can fire.
63
64
  # @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
64
- def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/ParameterLists
65
+ def dispatch(receiver_type:, method_name:, arg_types:,
65
66
  block_type: nil, environment: nil,
66
67
  call_node: nil, scope: nil)
67
68
  return nil if receiver_type.nil?
68
69
 
70
+ bound_method_result = MethodFolding.try_backward(
71
+ receiver: receiver_type, method_name: method_name, args: arg_types,
72
+ block_type: block_type, environment: environment,
73
+ call_node: call_node, scope: scope
74
+ )
75
+ return bound_method_result if bound_method_result
76
+
69
77
  precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
70
78
  return precise if precise
71
79
 
@@ -84,7 +92,37 @@ module Rigor
84
92
  receiver: receiver_type, method_name: method_name, args: arg_types,
85
93
  environment: environment, block_type: block_type
86
94
  )
87
- return rbs_result if rbs_result
95
+ if rbs_result
96
+ record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
97
+ return rbs_result
98
+ end
99
+
100
+ # ADR-10 slice 2b-ii — dependency-source inference tier.
101
+ # Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
102
+ # stubs / plugin contracts always win) and ABOVE the
103
+ # user-class fallback so a method defined in an opt-in
104
+ # gem stops emitting `call.undefined-method` even when
105
+ # no signature contract resolves. Returns
106
+ # `Dynamic[top]` — slice 2b-ii deliberately stops at the
107
+ # dynamic-origin envelope; per-method return-type
108
+ # precision is queued for a later slice.
109
+ dep_source_result = try_dependency_source(receiver_type, method_name, environment)
110
+ return dep_source_result if dep_source_result
111
+
112
+ # v0.1.3 — discovered-method dispatch tier. When the
113
+ # receiver class has no RBS BUT scope_indexer recorded
114
+ # `def method_name` for that class (or singleton), the
115
+ # call dispatches to `Dynamic[top]` rather than falling
116
+ # through to the user-class fallback. Sits below RBS /
117
+ # dependency-source so authoritative signatures still win.
118
+ # The scope-indexer-built table records every project-side
119
+ # `def`, `define_method`, and `alias_method`; the
120
+ # `discovered_method?` consult here closes the
121
+ # fail-soft-event hot spot on implicit-self calls
122
+ # (`sibling_private(...)`) inside `lib/rigor/`'s own
123
+ # internals (analyser private helpers don't have RBS).
124
+ discovered_result = try_discovered_method(receiver_type, method_name, scope)
125
+ return discovered_result if discovered_result
88
126
 
89
127
  # Slice 7 phase 10 — user-class ancestor fallback. When
90
128
  # the receiver is `Nominal[T]` or `Singleton[T]` for a
@@ -100,6 +138,58 @@ module Rigor
100
138
  try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
101
139
  end
102
140
 
141
+ # v0.1.3 — discovered-method dispatch tier. `scope` carries
142
+ # the `discovered_methods` table built once per program by
143
+ # `ScopeIndexer` (a `Hash[String, Hash[Symbol, :instance |
144
+ # :singleton]]`). When the receiver names a discovered
145
+ # class AND the requested method is recorded for that
146
+ # class's appropriate kind, return `Type::Combinator.untyped`
147
+ # — the dispatcher cannot infer a more precise return type
148
+ # from the bare `def` shape, but the call site stops being a
149
+ # fail-soft hot spot.
150
+ #
151
+ # Returns `nil` when scope / receiver class is unavailable,
152
+ # when the method is not in the discovered table, OR when
153
+ # `discovered_def_nodes` carries a re-typable body for the
154
+ # method (so the downstream
155
+ # `ExpressionTyper#try_user_method_inference` tier can
156
+ # re-type the body for a precise return type rather than
157
+ # collapsing to `Dynamic[top]` here).
158
+ #
159
+ # The tier does NOT gate on `rbs_class_known?`. RBS dispatch
160
+ # already had its turn upstream and returned `nil` (otherwise
161
+ # we wouldn't be here). When RBS knows the class but the
162
+ # particular method is missing from the sig — common for
163
+ # internal helpers and for auto-generated stubs that emit
164
+ # `class X` without enumerating every method — falling
165
+ # through to the user-class fallback would mistakenly fire
166
+ # `call.undefined-method`. Honoring the discovered table
167
+ # here keeps the sibling-private call resolution working
168
+ # under partial RBS coverage.
169
+ def try_discovered_method(receiver_type, method_name, scope)
170
+ return nil if scope.nil?
171
+
172
+ class_name, kind = discovered_method_lookup(receiver_type)
173
+ return nil if class_name.nil?
174
+ return nil unless scope.discovered_method?(class_name, method_name, kind)
175
+ return nil if kind == :instance && scope.user_def_for(class_name, method_name)
176
+
177
+ Type::Combinator.untyped
178
+ end
179
+
180
+ # Resolves the `(class_name, kind)` pair scope_indexer keys
181
+ # its `discovered_methods` table on. `Nominal[X]` looks up
182
+ # instance methods on X; `Singleton[X]` looks up singleton
183
+ # methods on X. Other carriers return `[nil, nil]` so the
184
+ # tier declines.
185
+ def discovered_method_lookup(receiver_type)
186
+ case receiver_type
187
+ when Type::Nominal then [receiver_type.class_name, :instance]
188
+ when Type::Singleton then [receiver_type.class_name, :singleton]
189
+ else [nil, nil]
190
+ end
191
+ end
192
+
103
193
  # ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
104
194
  # slice 7. Walks every loaded plugin's
105
195
  # `#flow_contribution_for(call_node:, scope:)` hook,
@@ -125,6 +215,146 @@ module Rigor
125
215
  FlowContribution::Merger.merge(contributions).return_type
126
216
  end
127
217
 
218
+ # ADR-10 slice 2b-ii. Consults the per-run
219
+ # `Analysis::DependencySourceInference::Index` carried by
220
+ # the environment for `(class_name, method_name)`
221
+ # observations harvested from opt-in gems' `roots:`. On a
222
+ # hit, returns `Combinator.untyped` so the call site
223
+ # carries the `Dynamic[top]` provenance (per ADR-10's
224
+ # "Inference contract": gem-source-inferred shapes never
225
+ # publish as ground-truth `T`). Returns `nil` when the
226
+ # environment carries no index, the index has no entry, or
227
+ # the receiver has no nominal class to look up.
228
+ def try_dependency_source(receiver_type, method_name, environment)
229
+ index = environment&.dependency_source_index
230
+ return nil if index.nil? || index.empty?
231
+
232
+ class_name = dep_source_class_name(receiver_type)
233
+ return nil if class_name.nil?
234
+
235
+ # ADR-10 5a — per-receiver plugin veto. When a
236
+ # registered plugin declares `manifest(owns_receivers:
237
+ # [<class>])` AND the call's receiver IS that class
238
+ # (or a subclass), decline and let plugins handle the
239
+ # call. Plugins that own a receiver are the
240
+ # authoritative source for that type; gem-source
241
+ # inference must not contribute behind their backs.
242
+ return nil if plugin_owns_receiver?(class_name, environment)
243
+
244
+ contribution_kind = index.contribution_for(class_name: class_name, method_name: method_name)
245
+ return Type::Combinator.untyped if contribution_kind
246
+
247
+ # ADR-10 5b — β budget semantics. On a catalog miss,
248
+ # if the receiver class belongs to a budget-exceeded
249
+ # gem AND the user opted into `:dependency_silence`,
250
+ # return `Dynamic[top]` rather than falling through to
251
+ # the user-class fallback. The user-class fallback
252
+ # would otherwise emit `call.undefined-method` for
253
+ # methods Rigor's catalog couldn't reach because the
254
+ # walker hit its cap.
255
+ budget_silence_result(class_name, index, environment)
256
+ end
257
+
258
+ # ADR-10 slice 5c — record a
259
+ # `dynamic.dependency-source.boundary-cross` event when
260
+ # RBS dispatch resolves a call AND the receiver class
261
+ # belongs to a `mode: :full` opt-in gem whose Walker
262
+ # also catalogued the same `(class_name, method_name)`.
263
+ # The dispatcher still returns the RBS answer (per
264
+ # ADR-10's tier order: authoritative-source wins), but
265
+ # the reporter accumulates the crossing for end-of-run
266
+ # audit diagnostics.
267
+ #
268
+ # Five honest fall-throughs keep the gate narrow:
269
+ #
270
+ # - environment / index / reporter missing — slice 5c
271
+ # needs all three.
272
+ # - receiver has no nominal class name (Dynamic-only
273
+ # carriers) — nothing to look up.
274
+ # - receiver class doesn't belong to a `mode: :full` gem
275
+ # — the user didn't opt this gem into the distinct
276
+ # dispatch path.
277
+ # - the gem-source catalog has no entry for the method —
278
+ # only RBS knows about it; nothing to cross.
279
+ # - the RBS-side result is itself `Dynamic[Top]` — the
280
+ # "agreement" is trivially `untyped ≈ untyped`, no
281
+ # meaningful divergence to flag.
282
+ def record_boundary_cross_if_applicable(receiver_type, method_name, rbs_result, environment)
283
+ class_name = boundary_cross_class_name(receiver_type, environment, rbs_result)
284
+ return if class_name.nil?
285
+
286
+ index = environment.dependency_source_index
287
+ return unless index.full_mode?(class_name)
288
+ return unless index.contribution_for(class_name: class_name, method_name: method_name)
289
+
290
+ environment.boundary_cross_reporter.record(
291
+ class_name: class_name, method_name: method_name,
292
+ gem_name: index.gem_for(class_name),
293
+ rbs_display: rbs_display_for(rbs_result)
294
+ )
295
+ end
296
+
297
+ # Composite preflight for {#record_boundary_cross_if_applicable}.
298
+ # Returns the receiver class name only when every prerequisite
299
+ # for emitting the diagnostic is satisfied (environment carries
300
+ # an index + reporter, receiver is a nominal carrier, RBS-side
301
+ # result is not the trivial `Dynamic[Top]` envelope). Returns
302
+ # `nil` to short-circuit otherwise.
303
+ def boundary_cross_class_name(receiver_type, environment, rbs_result)
304
+ return nil if environment.nil?
305
+ return nil if environment.dependency_source_index.nil?
306
+ return nil if environment.dependency_source_index.empty?
307
+ return nil if environment.boundary_cross_reporter.nil?
308
+ return nil if rbs_result_untyped?(rbs_result)
309
+
310
+ dep_source_class_name(receiver_type)
311
+ end
312
+
313
+ def rbs_result_untyped?(rbs_result)
314
+ rbs_result.is_a?(Type::Dynamic) && rbs_result.static_facet.is_a?(Type::Top)
315
+ end
316
+
317
+ def rbs_display_for(rbs_result)
318
+ return "untyped" if rbs_result.nil?
319
+
320
+ rbs_result.respond_to?(:describe) ? rbs_result.describe : rbs_result.inspect
321
+ end
322
+
323
+ def budget_silence_result(class_name, index, _environment)
324
+ return nil unless index.budget_overrun_strategy == :dependency_silence
325
+
326
+ owning_gem = index.gem_for(class_name)
327
+ return nil if owning_gem.nil?
328
+ return nil unless index.budget_exceeded.include?(owning_gem)
329
+
330
+ Type::Combinator.untyped
331
+ end
332
+
333
+ def plugin_owns_receiver?(class_name, environment)
334
+ registry = environment&.plugin_registry
335
+ return false if registry.nil? || registry.empty?
336
+
337
+ registry.plugins.any? do |plugin|
338
+ owns = plugin.manifest.owns_receivers # rigor:disable undefined-method
339
+ owns.any? { |owner| receiver_matches_owner?(class_name, owner, environment) }
340
+ end
341
+ end
342
+
343
+ def receiver_matches_owner?(class_name, owner, environment)
344
+ return true if class_name == owner
345
+
346
+ ordering = environment.class_ordering(class_name, owner)
347
+ %i[equal subclass].include?(ordering)
348
+ rescue StandardError
349
+ false
350
+ end
351
+
352
+ def dep_source_class_name(receiver_type)
353
+ case receiver_type
354
+ when Type::Nominal, Type::Singleton then receiver_type.class_name
355
+ end
356
+ end
357
+
128
358
  def collect_plugin_contributions(registry, call_node, scope)
129
359
  registry.plugins.filter_map do |plugin|
130
360
  contribution = plugin.flow_contribution_for(call_node: call_node, scope: scope)
@@ -155,6 +385,7 @@ module Rigor
155
385
  ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
156
386
  FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
157
387
  KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
388
+ MethodFolding.try_forward(receiver: receiver_type, method_name: method_name, args: arg_types) ||
158
389
  BlockFolding.try_fold(
159
390
  receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
160
391
  )
@@ -32,7 +32,6 @@ module Rigor
32
32
  # `#singleton_method`.
33
33
  #
34
34
  # See docs/internal-spec/inference-engine.md for the binding contract.
35
- # rubocop:disable Metrics/ClassLength
36
35
  class MethodParameterBinder
37
36
  # @param environment [Rigor::Environment]
38
37
  # @param class_path [String, nil] the qualified name of the class
@@ -175,7 +174,7 @@ module Rigor
175
174
  # `non-empty-array[Integer]` describe the parameter binding
176
175
  # they actually want, not its element type.
177
176
  def apply_param_overrides(types, slots, rbs_method)
178
- override_map = RbsExtended.param_type_override_map(rbs_method)
177
+ override_map = RbsExtended.param_type_override_map(rbs_method, environment: @environment)
179
178
  return if override_map.empty?
180
179
 
181
180
  slots.each do |slot|
@@ -275,6 +274,5 @@ module Rigor
275
274
  end
276
275
  end
277
276
  end
278
- # rubocop:enable Metrics/ClassLength
279
277
  end
280
278
  end
@@ -376,7 +376,7 @@ module Rigor
376
376
  # the predicate shape is recognised, or `nil` to signal "no
377
377
  # narrowing" so the public surface can fall back to the entry
378
378
  # scope.
379
- def analyse(node, scope) # rubocop:disable Metrics/CyclomaticComplexity
379
+ def analyse(node, scope)
380
380
  case node
381
381
  when Prism::ParenthesesNode
382
382
  analyse_parentheses(node, scope)
@@ -445,7 +445,6 @@ module Rigor
445
445
  # intersects each half with the integer-domain parts of
446
446
  # `current_type`. Non-integer parts of a Union receiver
447
447
  # (nil, String, …) survive unchanged.
448
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
449
448
  def complement_integer_range(current_type, range)
450
449
  halves = integer_range_complement_halves(range)
451
450
  parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
@@ -466,7 +465,6 @@ module Rigor
466
465
 
467
466
  Type::Combinator.union(*survivors)
468
467
  end
469
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
470
468
 
471
469
  # Returns the two open halves of an IntegerRange's
472
470
  # complement: the left half `int<-∞, a-1>` (when `a` is
@@ -1337,7 +1335,7 @@ module Rigor
1337
1335
  method_def = resolve_rbs_extended_method(node, scope)
1338
1336
  return nil if method_def.nil?
1339
1337
 
1340
- contribution = RbsExtended.read_flow_contribution(method_def)
1338
+ contribution = RbsExtended.read_flow_contribution(method_def, environment: scope.environment)
1341
1339
  return nil if contribution.nil?
1342
1340
 
1343
1341
  result = Rigor::FlowContribution::Merger.merge([contribution])
@@ -41,7 +41,6 @@ module Rigor
41
41
  # `Nominal[C]` regardless of which method body we are in.
42
42
  # When either argument is omitted, the corresponding token degrades
43
43
  # to Dynamic[Top].
44
- # rubocop:disable Metrics/ModuleLength
45
44
  module RbsTypeTranslator
46
45
  # Hash-based dispatch keeps `translate` linear and dodges the
47
46
  # bookkeeping costs of a 20-arm `case` (RuboCop AbcSize/CCN/Length
@@ -214,6 +213,5 @@ module Rigor
214
213
  end
215
214
  end
216
215
  end
217
- # rubocop:enable Metrics/ModuleLength
218
216
  end
219
217
  end
@@ -57,9 +57,19 @@ module Rigor
57
57
  # `Scope#with_local` / `#with_fact` / `#with_self_type`
58
58
  # propagates it across every derived scope.
59
59
  declared_types, discovered_classes = build_declaration_artifacts(root)
60
+ # Merge the indexer's findings on top of whatever the
61
+ # base scope already carries so callers that seed
62
+ # cross-file class knowledge (e.g. the ADR-14
63
+ # `SigGen::ObservationCollector` pre-walking project
64
+ # `lib/` before scanning `spec/`) keep their seeds
65
+ # alongside the per-file declarations the indexer
66
+ # itself discovers. Indexer-found entries win on
67
+ # collision — same-file declarations are the most
68
+ # specific authority.
69
+ merged_classes = default_scope.discovered_classes.merge(discovered_classes)
60
70
  seeded_scope = default_scope
61
71
  .with_declared_types(declared_types)
62
- .with_discovered_classes(discovered_classes)
72
+ .with_discovered_classes(merged_classes)
63
73
 
64
74
  # Slice 7 phase 2. Pre-pass over every class/module body
65
75
  # to collect the per-class ivar accumulator. Seeded after
@@ -300,7 +310,7 @@ module Rigor
300
310
  accumulator.freeze
301
311
  end
302
312
 
303
- def walk_constant_writes(node, qualified_prefix, default_scope, accumulator) # rubocop:disable Metrics/CyclomaticComplexity
313
+ def walk_constant_writes(node, qualified_prefix, default_scope, accumulator)
304
314
  return unless node.is_a?(Prism::Node)
305
315
 
306
316
  case node
@@ -348,7 +358,7 @@ module Rigor
348
358
  accumulator.transform_values(&:freeze).freeze
349
359
  end
350
360
 
351
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
361
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
352
362
  def walk_methods(node, qualified_prefix, in_singleton_class, accumulator)
353
363
  return unless node.is_a?(Prism::Node)
354
364
 
@@ -385,7 +395,7 @@ module Rigor
385
395
  walk_methods(child, qualified_prefix, in_singleton_class, accumulator)
386
396
  end
387
397
  end
388
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/AbcSize
398
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
389
399
 
390
400
  # v0.1.2 — when a `Const = Data.define(*sym) do ... end`
391
401
  # / `Const = Struct.new(*sym) do ... end` constant write
@@ -430,7 +440,6 @@ module Rigor
430
440
  accumulator.transform_values(&:freeze).freeze
431
441
  end
432
442
 
433
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
434
443
  def walk_def_nodes(node, qualified_prefix, in_singleton_class, accumulator)
435
444
  return unless node.is_a?(Prism::Node)
436
445
 
@@ -462,8 +471,6 @@ module Rigor
462
471
  walk_def_nodes(child, qualified_prefix, in_singleton_class, accumulator)
463
472
  end
464
473
  end
465
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
466
-
467
474
  # v0.0.3 A — sentinel key under which `record_def_node`
468
475
  # files DefNodes that live outside any class / module
469
476
  # body (top-level helpers, `def`s nested inside DSL
@@ -646,7 +653,6 @@ module Rigor
646
653
 
647
654
  # Builds a map `{class_name => {new_name_sym => old_name_sym}}` by
648
655
  # walking the tree for `AliasMethodNode` nodes inside class bodies.
649
- # rubocop:disable Metrics/CyclomaticComplexity
650
656
  def collect_class_alias_map(node, qualified_prefix, accumulator)
651
657
  return accumulator unless node.is_a?(Prism::Node)
652
658
 
@@ -667,7 +673,6 @@ module Rigor
667
673
  node.compact_child_nodes.each { |child| collect_class_alias_map(child, qualified_prefix, accumulator) }
668
674
  accumulator
669
675
  end
670
- # rubocop:enable Metrics/CyclomaticComplexity
671
676
 
672
677
  def record_alias_map_entry(alias_node, qualified_prefix, accumulator)
673
678
  return if qualified_prefix.empty?