rigortype 0.1.3 → 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 +125 -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 +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -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 +114 -3
  14. data/lib/rigor/builtins/imported_refinements.rb +360 -55
  15. data/lib/rigor/cache/descriptor.rb +1 -1
  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 +2 -2
  24. data/lib/rigor/configuration.rb +2 -2
  25. data/lib/rigor/environment.rb +35 -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 +3 -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 +146 -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 +7 -7
  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 +30 -9
  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 +5 -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 +52 -1
@@ -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,10 @@ 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
88
99
 
89
100
  # ADR-10 slice 2b-ii — dependency-source inference tier.
90
101
  # Sits BELOW RBS dispatch (RBS / RBS::Inline / generated
@@ -98,6 +109,21 @@ module Rigor
98
109
  dep_source_result = try_dependency_source(receiver_type, method_name, environment)
99
110
  return dep_source_result if dep_source_result
100
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
126
+
101
127
  # Slice 7 phase 10 — user-class ancestor fallback. When
102
128
  # the receiver is `Nominal[T]` or `Singleton[T]` for a
103
129
  # class not in the RBS environment (typically a
@@ -112,6 +138,58 @@ module Rigor
112
138
  try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
113
139
  end
114
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
+
115
193
  # ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
116
194
  # slice 7. Walks every loaded plugin's
117
195
  # `#flow_contribution_for(call_node:, scope:)` hook,
@@ -177,6 +255,71 @@ module Rigor
177
255
  budget_silence_result(class_name, index, environment)
178
256
  end
179
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
+
180
323
  def budget_silence_result(class_name, index, _environment)
181
324
  return nil unless index.budget_overrun_strategy == :dependency_silence
182
325
 
@@ -242,6 +385,7 @@ module Rigor
242
385
  ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
243
386
  FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
244
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) ||
245
389
  BlockFolding.try_fold(
246
390
  receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
247
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?
@@ -889,7 +889,7 @@ module Rigor
889
889
  # or nil otherwise. Centralised so each per-matcher
890
890
  # decoder can short-circuit on a non-matching outer
891
891
  # call.
892
- def rspec_expectation_target(call_node) # rubocop:disable Metrics/CyclomaticComplexity
892
+ def rspec_expectation_target(call_node)
893
893
  receiver = call_node.receiver
894
894
  return nil unless receiver.is_a?(Prism::CallNode) && receiver.name == :expect
895
895
  return nil unless receiver.receiver.nil?
@@ -967,7 +967,7 @@ module Rigor
967
967
  method_def = resolve_call_method(call_node, current_scope)
968
968
  return current_scope if method_def.nil?
969
969
 
970
- contribution = RbsExtended.read_flow_contribution(method_def)
970
+ contribution = RbsExtended.read_flow_contribution(method_def, environment: current_scope.environment)
971
971
  return current_scope if contribution.nil?
972
972
 
973
973
  result = Rigor::FlowContribution::Merger.merge([contribution])
@@ -1034,7 +1034,7 @@ module Rigor
1034
1034
  end
1035
1035
  end
1036
1036
 
1037
- def resolve_call_method(call_node, current_scope) # rubocop:disable Metrics/PerceivedComplexity
1037
+ def resolve_call_method(call_node, current_scope)
1038
1038
  receiver_node = call_node.receiver
1039
1039
  receiver_type =
1040
1040
  if receiver_node
@@ -1120,7 +1120,7 @@ module Rigor
1120
1120
  end
1121
1121
  end
1122
1122
 
1123
- def lookup_post_return_arg(call_node, method_def, target_name) # rubocop:disable Metrics/CyclomaticComplexity
1123
+ def lookup_post_return_arg(call_node, method_def, target_name)
1124
1124
  # Plugin-source contributions arrive without an
1125
1125
  # authoritative method_def (the plugin recognised the
1126
1126
  # call shape directly). Parameter-targeting falls back
@@ -1394,6 +1394,8 @@ module Rigor
1394
1394
  .with_class_ivars(scope.class_ivars)
1395
1395
  .with_class_cvars(scope.class_cvars)
1396
1396
  .with_program_globals(scope.program_globals)
1397
+ .with_discovered_methods(scope.discovered_methods)
1398
+ .with_discovered_method_visibilities(scope.discovered_method_visibilities)
1397
1399
  end
1398
1400
 
1399
1401
  def singleton_def?(def_node)
@@ -1501,7 +1503,7 @@ module Rigor
1501
1503
  EXIT_CALL_NAMES = %i[raise throw exit abort fail].freeze
1502
1504
  private_constant :EXIT_CALL_NAMES
1503
1505
 
1504
- def branch_unconditionally_exits?(node) # rubocop:disable Metrics/CyclomaticComplexity
1506
+ def branch_unconditionally_exits?(node)
1505
1507
  return false if node.nil?
1506
1508
 
1507
1509
  case node
@@ -1607,7 +1609,6 @@ module Rigor
1607
1609
  # Returns an array of `[Symbol, Rigor::Type]` pairs for every
1608
1610
  # variable captured by `pattern`. Unrecognised pattern nodes
1609
1611
  # contribute no bindings (fail-soft).
1610
- # rubocop:disable Metrics/CyclomaticComplexity
1611
1612
  def collect_in_pattern_bindings(subject, pattern, scope)
1612
1613
  case pattern
1613
1614
  when Prism::CapturePatternNode
@@ -1629,7 +1630,6 @@ module Rigor
1629
1630
  []
1630
1631
  end
1631
1632
  end
1632
- # rubocop:enable Metrics/CyclomaticComplexity
1633
1633
 
1634
1634
  def collect_array_pattern_bindings(pattern, scope)
1635
1635
  bindings = [*pattern.requireds, *pattern.posts].flat_map do |elem|
@@ -137,7 +137,6 @@ module Rigor
137
137
  # to the same `#get(url, timeout:, max_bytes:)` shape so the
138
138
  # tests don't require network access.
139
139
  class DefaultHttpClient
140
- # rubocop:disable Metrics/MethodLength
141
140
  def get(url, timeout:, max_bytes:)
142
141
  require "net/http"
143
142
  require "uri"
@@ -169,7 +168,6 @@ module Rigor
169
168
  end
170
169
  body
171
170
  end
172
- # rubocop:enable Metrics/MethodLength
173
171
  end
174
172
  end
175
173
  end
@@ -81,7 +81,7 @@ module Rigor
81
81
  # "rigor-rails"
82
82
  # { "gem" => "rigor-rails", "id" => "rails", "config" => {...} }
83
83
  # { gem: "rigor-rails", id: "rails", config: {...} }
84
- def normalise_entry(raw, index) # rubocop:disable Metrics/CyclomaticComplexity
84
+ def normalise_entry(raw, index)
85
85
  case raw
86
86
  when String
87
87
  { gem: raw, id: nil, config: {} }
@@ -136,7 +136,7 @@ module Rigor
136
136
  )
137
137
  end
138
138
 
139
- def lookup_plugin_class!(entry, newly_registered) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
139
+ def lookup_plugin_class!(entry, newly_registered)
140
140
  if entry[:id]
141
141
  plugin_class = Plugin.registered_for(entry[:id])
142
142
  unless plugin_class
@@ -11,7 +11,7 @@ module Rigor
11
11
  # The fields are pinned by ADR-2 § "Registration, Configuration,
12
12
  # and Caching"; the v0.1.0 plugin contract surface treats this
13
13
  # struct as the public manifest shape.
14
- class Manifest # rubocop:disable Metrics/ClassLength
14
+ class Manifest
15
15
  # Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
16
16
  # so plugin ids round-trip through cache producer ids and
17
17
  # `plugin.<id>.<rule>` diagnostic identifiers without escape.
@@ -31,19 +31,19 @@ module Rigor
31
31
  # topological sort + missing-producer detection (slice 5);
32
32
  # slice 4 carries the declarations on the manifest but the
33
33
  # loader does not yet enforce them.
34
- Consumption = Data.define(:plugin_id, :name, :optional) do
34
+ class Consumption < Data.define(:plugin_id, :name, :optional)
35
35
  def initialize(plugin_id:, name:, optional: false)
36
36
  super(plugin_id: plugin_id.to_s, name: name.to_sym, optional: optional ? true : false)
37
37
  end
38
38
  end
39
39
 
40
40
  attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
41
- :owns_receivers
41
+ :owns_receivers, :type_node_resolvers
42
42
 
43
43
  def initialize( # rubocop:disable Metrics/ParameterLists
44
44
  id:, version:,
45
45
  description: nil, protocols: [], config_schema: {},
46
- produces: [], consumes: [], owns_receivers: []
46
+ produces: [], consumes: [], owns_receivers: [], type_node_resolvers: []
47
47
  )
48
48
  validate_id!(id)
49
49
  validate_version!(version)
@@ -51,15 +51,18 @@ module Rigor
51
51
  validate_config_schema!(config_schema)
52
52
  validate_produces!(produces)
53
53
  validate_owns_receivers!(owns_receivers)
54
+ validate_type_node_resolvers!(type_node_resolvers)
54
55
 
55
- assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers)
56
+ assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
57
+ type_node_resolvers)
56
58
  freeze
57
59
  end
58
60
 
59
61
  private
60
62
 
61
- # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize
62
- def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers)
63
+ # rubocop:disable Metrics/ParameterLists
64
+ def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
65
+ type_node_resolvers)
63
66
  @id = id.dup.freeze
64
67
  @version = version.dup.freeze
65
68
  @description = description.nil? ? nil : description.to_s.dup.freeze
@@ -68,8 +71,9 @@ module Rigor
68
71
  @produces = produces.map(&:to_sym).freeze
69
72
  @consumes = coerce_consumes(consumes)
70
73
  @owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
74
+ @type_node_resolvers = type_node_resolvers.dup.freeze
71
75
  end
72
- # rubocop:enable Metrics/ParameterLists,Metrics/AbcSize
76
+ # rubocop:enable Metrics/ParameterLists
73
77
 
74
78
  public
75
79
 
@@ -105,7 +109,8 @@ module Rigor
105
109
  "config_schema" => config_schema.to_h { |k, v| [k, v.to_s] },
106
110
  "produces" => produces.map(&:to_s),
107
111
  "consumes" => consumes.map { |c| consumption_hash(c) },
108
- "owns_receivers" => owns_receivers
112
+ "owns_receivers" => owns_receivers,
113
+ "type_node_resolvers" => type_node_resolvers.map { |r| r.class.name }
109
114
  }
110
115
  end
111
116
 
@@ -187,6 +192,22 @@ module Rigor
187
192
  "got #{owns_receivers.inspect}"
188
193
  end
189
194
 
195
+ # ADR-13 slice 2 — `type_node_resolvers:` declares the
196
+ # plugin-supplied `TypeNodeResolver` instances the parser
197
+ # consults (in slice 3) when an RBS::Extended payload's
198
+ # named- or generic-type head misses the built-in registry.
199
+ # Slice 2 carries the declarations on the manifest and the
200
+ # registry exposes them in registration order; the parser
201
+ # integration that actually drives the chain lands in
202
+ # slice 3.
203
+ def validate_type_node_resolvers!(resolvers)
204
+ return if resolvers.is_a?(Array) && resolvers.all?(TypeNodeResolver)
205
+
206
+ raise ArgumentError,
207
+ "plugin manifest type_node_resolvers must be an Array of " \
208
+ "Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
209
+ end
210
+
190
211
  def coerce_consumes(consumes)
191
212
  unless consumes.is_a?(Array)
192
213
  raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
@@ -44,6 +44,17 @@ module Rigor
44
44
  !load_errors.empty?
45
45
  end
46
46
 
47
+ # ADR-13 slice 2 — flat ordered list of every loaded
48
+ # plugin's manifest-declared {TypeNodeResolver} instances,
49
+ # in plugin registration order. Slice 3 wires this into
50
+ # the parser's resolver chain; until then the method is a
51
+ # read-side aggregator only. The first non-nil
52
+ # `#resolve(node, scope)` return wins per ADR-13 WD3 / WD5
53
+ # — registration order is the user's lever.
54
+ def type_node_resolvers
55
+ plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
56
+ end
57
+
47
58
  EMPTY = new.freeze
48
59
  end
49
60
  end
@@ -42,7 +42,7 @@ module Rigor
42
42
  class Services
43
43
  attr_reader :reflection, :type, :configuration, :cache_store, :trust_policy, :fact_store
44
44
 
45
- def initialize( # rubocop:disable Metrics/ParameterLists
45
+ def initialize(
46
46
  reflection:, type:, configuration:,
47
47
  cache_store: nil, trust_policy: nil, fact_store: nil
48
48
  )
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Plugin
5
+ # Plugin-supplied resolver for custom named / generic type
6
+ # vocabulary in RBS::Extended payloads. ADR-13 § "Decision".
7
+ #
8
+ # Subclasses override {#resolve} to return a
9
+ # {Rigor::Type::Base} when the node matches the vocabulary
10
+ # the resolver covers, or `nil` to fall through to the next
11
+ # resolver in the chain (and finally to the built-in / RBS
12
+ # fallback). The base implementation returns `nil` so an
13
+ # unimplemented subclass is a safe no-op.
14
+ #
15
+ # Resolvers are registered through their plugin's manifest
16
+ # under the `type_node_resolvers:` slot:
17
+ #
18
+ # class RigorTypescriptUtilityTypes < Rigor::Plugin::Base
19
+ # manifest(
20
+ # id: "typescript-utility-types",
21
+ # version: "0.1.0",
22
+ # type_node_resolvers: [Resolvers::Pick.new,
23
+ # Resolvers::Omit.new]
24
+ # )
25
+ # end
26
+ #
27
+ # Slice 2 of the ADR-13 envelope (this file) ships the base
28
+ # class + manifest hook + registry aggregation. The parser-
29
+ # side wiring that actually consults the resolver chain
30
+ # arrives in slice 3, when {Rigor::TypeNode::NameScope} and
31
+ # the dispatcher between {Rigor::Builtins::ImportedRefinements::Parser}
32
+ # and the chain land. Until then resolvers can be unit-tested
33
+ # in isolation but never run for a real `%a{rigor:v1:...}`
34
+ # payload.
35
+ #
36
+ # Resolvers SHOULD be stateless and re-entrant; the registry
37
+ # builds the chain once per `Analysis::Runner.run` and may
38
+ # consult any resolver multiple times for the same node.
39
+ class TypeNodeResolver
40
+ # @param node [Rigor::TypeNode::Identifier, Rigor::TypeNode::Generic]
41
+ # the parser-emitted node the chain is asking about.
42
+ # @param scope [Rigor::TypeNode::NameScope] companion
43
+ # value object (slice 3); slice 2 invocations MAY pass
44
+ # `nil` because the chain doesn't exist yet.
45
+ # @return [Rigor::Type::Base, nil] resolved type, or `nil`
46
+ # to fall through.
47
+ def resolve(node, scope) # rubocop:disable Lint/UnusedMethodArgument
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/rigor/plugin.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "plugin/type_node_resolver"
3
4
  require_relative "plugin/manifest"
4
5
  require_relative "plugin/access_denied_error"
5
6
  require_relative "plugin/trust_policy"