rigortype 0.1.17 → 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 (70) 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/unreachable_clause_collector.rb +18 -1
  6. data/lib/rigor/analysis/check_rules.rb +34 -6
  7. data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
  8. data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
  9. data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
  10. data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
  11. data/lib/rigor/analysis/runner.rb +160 -1190
  12. data/lib/rigor/analysis/worker_session.rb +47 -8
  13. data/lib/rigor/cache/incremental_snapshot.rb +10 -4
  14. data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
  15. data/lib/rigor/cache/store.rb +46 -13
  16. data/lib/rigor/cli/check_command.rb +705 -0
  17. data/lib/rigor/cli/ci_detector.rb +94 -0
  18. data/lib/rigor/cli/diagnostic_formats.rb +345 -0
  19. data/lib/rigor/cli/prism_colorizer.rb +10 -3
  20. data/lib/rigor/cli/trace_command.rb +143 -0
  21. data/lib/rigor/cli/trace_renderer.rb +310 -0
  22. data/lib/rigor/cli.rb +15 -614
  23. data/lib/rigor/configuration.rb +9 -6
  24. data/lib/rigor/environment/rbs_loader.rb +53 -68
  25. data/lib/rigor/environment.rb +1 -1
  26. data/lib/rigor/inference/acceptance.rb +10 -0
  27. data/lib/rigor/inference/expression_typer.rb +28 -62
  28. data/lib/rigor/inference/flow_tracer.rb +180 -0
  29. data/lib/rigor/inference/macro_block_self_type.rb +10 -11
  30. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
  31. data/lib/rigor/inference/method_dispatcher.rb +115 -54
  32. data/lib/rigor/inference/narrowing.rb +60 -0
  33. data/lib/rigor/inference/scope_indexer.rb +75 -15
  34. data/lib/rigor/inference/statement_evaluator.rb +35 -52
  35. data/lib/rigor/plugin/additional_initializer.rb +61 -38
  36. data/lib/rigor/plugin/base.rb +282 -41
  37. data/lib/rigor/plugin/node_rule_walk.rb +147 -0
  38. data/lib/rigor/plugin/registry.rb +263 -35
  39. data/lib/rigor/plugin.rb +1 -0
  40. data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
  41. data/lib/rigor/scope/discovery_index.rb +58 -0
  42. data/lib/rigor/scope.rb +67 -198
  43. data/lib/rigor/sig_gen/observation_collector.rb +6 -6
  44. data/lib/rigor/source/literals.rb +14 -0
  45. data/lib/rigor/type/combinator.rb +5 -0
  46. data/lib/rigor/version.rb +1 -1
  47. data/lib/rigor.rb +0 -1
  48. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
  49. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
  50. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
  51. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
  52. data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
  53. data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
  54. data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
  55. data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
  56. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
  57. data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
  58. data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
  59. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
  60. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
  61. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
  62. data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
  63. data/sig/rigor/environment.rbs +0 -2
  64. data/sig/rigor/inference.rbs +5 -0
  65. data/sig/rigor/plugin/base.rbs +1 -2
  66. data/sig/rigor/scope.rbs +41 -29
  67. data/sig/rigor/source.rbs +1 -0
  68. data/skills/rigor-ci-setup/SKILL.md +319 -0
  69. metadata +15 -2
  70. data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
@@ -197,45 +197,83 @@ module Rigor
197
197
  MIGRATION_PATH_PATTERNS.any? { |pattern| path_s.match?(pattern) }
198
198
  end
199
199
 
200
- # v0.1.2 return-type contribution. `Model.find(id)`
201
- # narrows the call site's return type to `Nominal[Model]`,
202
- # so chained calls (`User.find(1).name`) resolve through
203
- # the analyzer's normal dispatch instead of the RBS-level
204
- # untyped fall-back. `Model.find_by(...)` narrows to
205
- # `Nominal[Model] | nil` because Rails returns nil when no
206
- # row matches. `where` / `find_or_*` are intentionally
207
- # deferredthey return relations, and Rigor does not yet
208
- # carry an Enumerable-backed relation shape that would be
209
- # more precise than the RBS envelope.
210
- def flow_contribution_for(call_node:, scope:)
200
+ # The class-side finder / relation entry-point names
201
+ # `finder_return_type` recognises. Static half of the
202
+ # `dynamic_return` name gate; the run-time half comes from
203
+ # the model index (scopes, associations, columns).
204
+ FINDER_METHOD_NAMES = %i[find find_by! find_by where all order limit none select].freeze
205
+ private_constant :FINDER_METHOD_NAMES
206
+
207
+ # v0.1.2 — return-type contribution; ADR-52 slice 5b
208
+ # migrated off `flow_contribution_for` onto the run-time
209
+ # `methods:` name gate. `Model.find(id)` narrows the call
210
+ # site's return type to `Nominal[Model]`, so chained calls
211
+ # (`User.find(1).name`) resolve through the analyzer's
212
+ # normal dispatch instead of the RBS-level untyped
213
+ # fall-back; scopes, association accessors, and column
214
+ # readers narrow per the paths below.
215
+ #
216
+ # WHY a method-name gate and not `receivers:` — the ADR-52
217
+ # "rigor-activerecord blocker": a project model not in RBS
218
+ # types its constant as `Dynamic[top]`, so a receiver-type
219
+ # gate declines exactly the calls this plugin exists for. A
220
+ # *name* gate never reads the receiver type; the block keeps
221
+ # the plugin's own AST-constant / `self_type` / `type_of`
222
+ # resolution, so the Dynamic-constant case still reaches it
223
+ # (the same shape as rigor-sorbet's catalog path). The set —
224
+ # the static finder names ∪ every scope, association, and
225
+ # column name (plus `column?` predicate forms) the model
226
+ # index discovered — is exactly the union of names the four
227
+ # resolution paths below can return a type for, so gating on
228
+ # it is byte-identical to the old ungated hook. It is broad
229
+ # (`name`, `id`, …), but membership is one Set probe and the
230
+ # expensive block runs only on candidate hits.
231
+ dynamic_return methods: -> { recognised_method_names } do |call_node, scope|
232
+ contribution_return_type(call_node, scope)
233
+ end
234
+
235
+ private
236
+
237
+ # The run-time name gate: finders ∪ scopes ∪ associations ∪
238
+ # column readers (+ `?` predicates). Resolved lazily on first
239
+ # dispatch (after `#prepare` built the index), memoised by the
240
+ # engine. Returns [] when discovery found nothing — the gate
241
+ # then declines every call, matching the old hook's
242
+ # `index.nil? || index.empty?` early return.
243
+ def recognised_method_names
244
+ index = model_index
245
+ return [] if index.nil? || index.empty?
246
+
247
+ names = FINDER_METHOD_NAMES.dup
248
+ index.entries.each_value do |entry|
249
+ names.concat(entry.scopes.map(&:to_sym))
250
+ names.concat(entry.association_names.map(&:to_sym))
251
+ entry.column_names.each do |column|
252
+ names << column.to_sym
253
+ names << :"#{column}?"
254
+ end
255
+ end
256
+ names
257
+ end
258
+
259
+ # The migrated body of the legacy `flow_contribution_for` —
260
+ # same resolution order, returning the bare type the
261
+ # `dynamic_return` contract expects.
262
+ def contribution_return_type(call_node, scope)
211
263
  return nil unless call_node.is_a?(Prism::CallNode)
212
264
 
213
265
  index = model_index
214
266
  return nil if index.nil? || index.empty?
215
267
 
216
- return_type =
217
- if call_node.receiver
218
- class_call_return_type(call_node, index) ||
219
- relation_call_return_type(call_node, scope, index) ||
220
- instance_call_return_type(call_node, scope, index)
221
- else
222
- implicit_self_class_call_return_type(call_node, scope, index)
223
- end
224
- return nil if return_type.nil?
225
-
226
- Rigor::FlowContribution.new(
227
- return_type: return_type,
228
- provenance: Rigor::FlowContribution::Provenance.new(
229
- source_family: "plugin.#{manifest.id}",
230
- plugin_id: manifest.id,
231
- node: call_node,
232
- descriptor: nil
233
- )
234
- )
268
+ if call_node.receiver
269
+ class_call_return_type(call_node, index) ||
270
+ relation_call_return_type(call_node, scope, index) ||
271
+ instance_call_return_type(call_node, scope, index)
272
+ else
273
+ implicit_self_class_call_return_type(call_node, scope, index)
274
+ end
235
275
  end
236
276
 
237
- private
238
-
239
277
  def class_call_return_type(call_node, index)
240
278
  model_name = constant_receiver_name(call_node.receiver)
241
279
  return nil if model_name.nil?
@@ -12,7 +12,7 @@ module Rigor
12
12
  # attachment mapping the plugin sees.
13
13
  #
14
14
  # No `:error` diagnostics in this slice — the
15
- # `flow_contribution_for` return-type narrowing carries
15
+ # `dynamic_return` return-type narrowing carries
16
16
  # the type-checking value; surfacing unknown attachment
17
17
  # names as errors requires a coupled receiver-class
18
18
  # narrowing pass that the integration spec doesn't yet
@@ -50,8 +50,8 @@ module Rigor
50
50
  return if attachments.nil?
51
51
 
52
52
  # Only flag when the method matches a known
53
- # attachment name (the `flow_contribution_for`
54
- # tier provides the narrowing; the diagnostic just
53
+ # attachment name (the `dynamic_return` rule
54
+ # provides the narrowing; the diagnostic just
55
55
  # confirms the recognition).
56
56
  attachment = attachments.find { |a| a[:name] == node.name.to_s }
57
57
  return if attachment.nil?
@@ -79,47 +79,41 @@ module Rigor
79
79
 
80
80
  # Return-type contribution: when the receiver is
81
81
  # `Nominal[Model]` and the method matches a discovered
82
- # attachment, narrow to
82
+ # attachment, narrows to
83
83
  # `Nominal[ActiveStorage::Attached::One]` (singular) or
84
- # `Nominal[ActiveStorage::Attached::Many]` (collection).
84
+ # `Nominal[ActiveStorage::Attached::Many]` (collection)
85
+ # via a `dynamic_return` rule keyed on the live set of
86
+ # model class names from the attachment index.
85
87
  # The chained call (`.attached?`, `.purge`, `.url`)
86
88
  # then resolves through ActiveStorage's RBS surface.
87
89
  # Attachment setters (`user.avatar=`) decline — they
88
90
  # take side-effecting argument types that the RBS
89
91
  # surface already covers.
90
- def flow_contribution_for(call_node:, scope:)
91
- return nil unless call_node.is_a?(Prism::CallNode)
92
- return nil if call_node.receiver.nil?
93
- return nil unless call_node.arguments.nil?
92
+ dynamic_return receivers: -> { attachment_index&.class_names || [] } do |call_node, scope|
93
+ next nil unless call_node.is_a?(Prism::CallNode)
94
+ next nil if call_node.receiver.nil?
95
+ next nil unless call_node.arguments.nil?
94
96
 
95
97
  index = attachment_index
96
- return nil if index.nil? || index.empty?
98
+ next nil if index.nil? || index.empty?
97
99
 
98
100
  receiver_type = scope.type_of(call_node.receiver)
99
- return nil unless receiver_type.is_a?(Rigor::Type::Nominal)
101
+ next nil unless receiver_type.is_a?(Rigor::Type::Nominal)
100
102
 
101
103
  attachments = index.attachments_for(receiver_type.class_name) ||
102
104
  index.attachments_for("::#{receiver_type.class_name}")
103
- return nil if attachments.nil?
105
+ next nil if attachments.nil?
104
106
 
105
107
  attachment = attachments.find { |a| a[:name] == call_node.name.to_s }
106
- return nil if attachment.nil?
108
+ next nil if attachment.nil?
107
109
 
108
110
  target = case attachment[:kind]
109
111
  when :singular then "ActiveStorage::Attached::One"
110
112
  when :collection then "ActiveStorage::Attached::Many"
111
113
  end
112
- return nil if target.nil?
113
-
114
- Rigor::FlowContribution.new(
115
- return_type: Rigor::Type::Combinator.nominal_of(target),
116
- provenance: Rigor::FlowContribution::Provenance.new(
117
- source_family: "plugin.#{manifest.id}",
118
- plugin_id: manifest.id,
119
- node: call_node,
120
- descriptor: nil
121
- )
122
- )
114
+ next nil if target.nil?
115
+
116
+ Rigor::Type::Combinator.nominal_of(target)
123
117
  end
124
118
 
125
119
  # @!visibility private
@@ -5,7 +5,7 @@ require "rigor/plugin"
5
5
  module Rigor
6
6
  module Plugin
7
7
  # ADR-25 — a pure RBS-bundle plugin. It ships NO analyzer code:
8
- # no `diagnostics_for_file`, no `flow_contribution_for`. Its
8
+ # no `diagnostics_for_file`, no `dynamic_return`. Its
9
9
  # whole contribution is the manifest's `signature_paths: ["sig"]`,
10
10
  # which declares the bundled ActiveSupport `core_ext` RBS
11
11
  # directory. `Plugin::Loader` resolves that directory against
@@ -146,8 +146,7 @@ module Rigor
146
146
 
147
147
  class_pair = kwargs.elements.find do |elem|
148
148
  elem.is_a?(Prism::AssocNode) &&
149
- elem.key.is_a?(Prism::SymbolNode) &&
150
- elem.key.value == "class"
149
+ Source::Literals.symbol_named?(elem.key, "class")
151
150
  end
152
151
  return nil if class_pair.nil?
153
152
 
@@ -281,7 +281,7 @@ module Rigor
281
281
  return false unless kwargs.is_a?(Prism::KeywordHashNode)
282
282
 
283
283
  pair = kwargs.elements.find do |el|
284
- el.is_a?(Prism::AssocNode) && el.key.is_a?(Prism::SymbolNode) && el.key.unescaped == "required"
284
+ el.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(el.key, "required")
285
285
  end
286
286
  return false if pair.nil?
287
287
 
@@ -364,7 +364,7 @@ module Rigor
364
364
  return true unless kwargs.is_a?(Prism::KeywordHashNode)
365
365
 
366
366
  null_pair = kwargs.elements.find do |el|
367
- el.is_a?(Prism::AssocNode) && el.key.is_a?(Prism::SymbolNode) && el.key.unescaped == "null"
367
+ el.is_a?(Prism::AssocNode) && Source::Literals.symbol_named?(el.key, "null")
368
368
  end
369
369
  return true if null_pair.nil?
370
370
 
@@ -21,8 +21,8 @@ module Rigor
21
21
  # { ... }` and `subject(:name) { ... }` declarations.
22
22
  # `:subject` is the key for the implicit `subject { ... }`.
23
23
  #
24
- # Pillar 2 Slice 2 — used by the plugin's
25
- # `flow_contribution_for` hook to bind `let`-named
24
+ # Pillar 2 Slice 2 — used by the plugin's let-binding
25
+ # `dynamic_return` rule to bind `let`-named
26
26
  # method-shape calls inside `it` bodies to the let
27
27
  # block's inferred type.
28
28
  class LetScopeIndex
@@ -48,6 +48,16 @@ module Rigor
48
48
  @records.select { |rec| rec.contains?(line) }
49
49
  end
50
50
 
51
+ # ADR-52 slice 5a — every `let` / `subject` name declared
52
+ # anywhere in the file, across all describe scopes. Feeds the
53
+ # plugin's `dynamic_return file_methods:` gate: the engine only
54
+ # consults the rule for a call whose name appears here; the
55
+ # precise line-scoped resolution stays in `let_block_at`.
56
+ # @return [Array<Symbol>]
57
+ def let_names
58
+ @records.flat_map { |rec| rec.lets.keys }.uniq
59
+ end
60
+
51
61
  # Resolves a `let` name at the given line by walking
52
62
  # records innermost to outermost.
53
63
  # @return [Prism::BlockNode, nil]
@@ -9,7 +9,7 @@ module Rigor
9
9
  module Plugin
10
10
  class Rspec < Rigor::Plugin::Base
11
11
  # Pillar 2 Slice 1 — recognises `expect(x).to MATCHER`
12
- # patterns at `flow_contribution_for` time and emits
12
+ # patterns at per-call recognition time and emits
13
13
  # `post_return_facts` that narrow the named local on the
14
14
  # post-call edge.
15
15
  #
@@ -72,6 +72,17 @@ module Rigor
72
72
  "`subject { described_class.new(...) }`.",
73
73
  consumes: [
74
74
  { plugin_id: "factorybot", name: :factory_index, optional: true }
75
+ ],
76
+ additional_initializers: [
77
+ # ADR-38 block-form: `before { @ivar = … }`, `let(:x) { @ivar = … }`,
78
+ # and `subject { @ivar = … }` establish ivar state before `it` bodies
79
+ # run. Declaring them here suppresses the read-before-write nil
80
+ # widening that would otherwise appear on those ivars in `it` / `specify`
81
+ # sibling blocks.
82
+ Rigor::Plugin::AdditionalInitializer.new(
83
+ receiver_constraint: "RSpec::ExampleGroup",
84
+ block_methods: %i[before let subject]
85
+ )
75
86
  ]
76
87
  )
77
88
 
@@ -79,16 +90,16 @@ module Rigor
79
90
  @services = services
80
91
  @factory_index_resolved = false
81
92
  @factory_index = nil
82
- # Per-path `LetScopeIndex` cache. The plugin's
83
- # `flow_contribution_for` is called for every call
84
- # node the dispatcher visits; building the index once
93
+ # Per-path `LetScopeIndex` cache. The let-binding
94
+ # `dynamic_return` rule (and its `file_methods:` gate)
95
+ # consult the index per call node; building it once
85
96
  # per file is essential for performance.
86
97
  @let_index_cache = {}
87
98
  end
88
99
 
89
100
  def diagnostics_for_file(path:, scope:, root:) # rubocop:disable Lint/UnusedMethodArgument
90
101
  # Build the let-scope index for this file while we
91
- # have the parsed root in hand — `flow_contribution_for`
102
+ # have the parsed root in hand — the let-binding rule
92
103
  # picks it up from `@let_index_cache` keyed on path.
93
104
  @let_index_cache[path] ||= LetScopeIndex.build(root)
94
105
  Analyzer.diagnose(path: path, root: root).map { |diag| build_diagnostic(diag) }
@@ -101,23 +112,32 @@ module Rigor
101
112
  MatcherAnalyzer.contribution_for(call_node, environment: scope&.environment)&.post_return_facts
102
113
  end
103
114
 
104
- # Pillar 2 Slice 2 — binds local reads in `it` / spec bodies to
105
- # their `let(:name) { ... }` block's inferred return type. This is
106
- # a *method-gated return type* (keyed on the let-bound name read),
107
- # which the receiver-gated `dynamic_return` cannot express, so it
108
- # stays on `flow_contribution_for` the deprecated escape valve
109
- # (ADR-37 slice 2 § "Outcome").
110
- def flow_contribution_for(call_node:, scope:)
111
- let_binding_contribution(call_node, scope)
115
+ # Pillar 2 Slice 2 / ADR-52 slice 5a — binds local reads in `it` /
116
+ # spec bodies to their `let(:name) { ... }` block's inferred return
117
+ # type. The name set varies per file (each spec file's
118
+ # `describe`/`let` structure), so the rule gates on the per-file
119
+ # `file_methods:` form: the engine resolves the file's let names
120
+ # once per analysed file and consults the block only for a listed
121
+ # name; the line-scoped shadowing resolution stays in the block.
122
+ dynamic_return file_methods: ->(path) { let_names_for(path) } do |call_node, scope|
123
+ let_binding_return_type(call_node, scope)
112
124
  end
113
125
 
114
126
  private
115
127
 
128
+ # The `file_methods:` gate set — every `let` / `subject` name the
129
+ # file declares anywhere. A safe over-approximation of the block's
130
+ # own `let_block_at` line-scoped lookup (a name read outside its
131
+ # describe scope passes the gate and is declined by the block).
132
+ def let_names_for(path)
133
+ let_scope_index_for(path)&.let_names || []
134
+ end
135
+
116
136
  # Pillar 2 Slice 2 — when the call node is a no-receiver
117
137
  # method call (`user`, `subject`, etc.) inside an RSpec
118
138
  # `describe` block whose lets include a matching name,
119
- # return a `FlowContribution(return_type: <inferred>)`.
120
- def let_binding_contribution(call_node, scope)
139
+ # return the let block's inferred type.
140
+ def let_binding_return_type(call_node, scope)
121
141
  return nil if scope.nil?
122
142
  return nil unless candidate_call?(call_node)
123
143
 
@@ -129,15 +149,12 @@ module Rigor
129
149
  return nil if block_node.nil?
130
150
 
131
151
  describe_const = index.describe_const_at(line)
132
- type = LetTypeResolver.resolve(
152
+ LetTypeResolver.resolve(
133
153
  block_node,
134
154
  describe_const: describe_const,
135
155
  factory_index: factory_index_or_nil,
136
156
  environment: scope.environment
137
157
  )
138
- return nil if type.nil?
139
-
140
- Rigor::FlowContribution.new(return_type: type)
141
158
  end
142
159
 
143
160
  def candidate_call?(call_node)
@@ -28,16 +28,15 @@ module Rigor
28
28
  #
29
29
  # ## Two-phase mechanism
30
30
  #
31
- # The recogniser is invoked from `flow_contribution_for`
31
+ # The recogniser is invoked from the plugin's `dynamic_return` rule
32
32
  # where the per-node `scope:` carries the proper narrowing
33
- # context. It returns:
33
+ # context. The rule:
34
34
  #
35
- # - A `FlowContribution` with `return_type: bot` and
36
- # `exceptional: :raises` regardless of reachability
37
- # (faithful to `T.absurd`'s runtime behaviour: it always
38
- # raises). This lets the engine's existing flow analysis
39
- # treat code after `T.absurd` as unreachable, matching
40
- # what users of Sorbet expect.
35
+ # - Contributes a `bot` return type regardless of
36
+ # reachability (faithful to `T.absurd`'s runtime
37
+ # behaviour: it always raises). This lets the engine's
38
+ # existing flow analysis treat code after `T.absurd` as
39
+ # unreachable, matching what users of Sorbet expect.
41
40
  # - When the branch is REACHABLE (the discriminant's type
42
41
  # isn't `bot`), the recogniser also records the call
43
42
  # node in a per-plugin set. The plugin's
@@ -47,7 +46,7 @@ module Rigor
47
46
  # call_node whose object identity matches the recorded
48
47
  # set. We rely on the runner only parsing each file
49
48
  # once per run, so the same Prism node object is seen
50
- # in both `flow_contribution_for` and
49
+ # in both the `dynamic_return` rule and
51
50
  # `diagnostics_for_file`.
52
51
  module AbsurdRecognizer
53
52
  # @param call_node [Prism::CallNode]
@@ -82,26 +81,6 @@ module Rigor
82
81
  # diagnostic fires conservatively.
83
82
  false
84
83
  end
85
-
86
- # The contribution every `T.absurd` call gets,
87
- # regardless of static reachability — `T.absurd` raises
88
- # at runtime, so its return type is `bot` and the call
89
- # is exceptional. This lets the engine's flow analysis
90
- # treat code after the call as unreachable (no
91
- # `flow.unreachable-branch` from us; that's an engine
92
- # rule that consults the same effect lattice).
93
- def self.contribution(call_node, plugin_id)
94
- Rigor::FlowContribution.new(
95
- return_type: Rigor::Type::Combinator.bot,
96
- exceptional: :raises,
97
- provenance: Rigor::FlowContribution::Provenance.new(
98
- source_family: "plugin.#{plugin_id}",
99
- plugin_id: plugin_id,
100
- node: call_node,
101
- descriptor: nil
102
- )
103
- )
104
- end
105
84
  end
106
85
  end
107
86
  end
@@ -6,7 +6,7 @@ module Rigor
6
6
  # Per-run table of method signatures keyed by the
7
7
  # `(class_name, method_name, kind)` triple. Built by
8
8
  # {CatalogWalker} during the plugin's lazy pre-walk; read
9
- # by {Sorbet#flow_contribution_for} at every call site.
9
+ # by the plugin's `dynamic_return` rule at every gated call site.
10
10
  #
11
11
  # The catalog is mutable while it is being built, then
12
12
  # frozen via {#freeze!} before the first read. Construction
@@ -80,6 +80,22 @@ module Rigor
80
80
  @entries.empty?
81
81
  end
82
82
 
83
+ # ADR-52 slice 4 — the distinct method names the catalog
84
+ # carries at least one signature for, across every
85
+ # `(class_name, kind)` owner. Feeds the plugin's run-time
86
+ # `dynamic_return methods:` name gate: the engine only
87
+ # consults the plugin for a call whose name appears here
88
+ # (or in the static assertion vocabulary), and the
89
+ # precise `(class, kind)` lookup stays in the rule block.
90
+ # Computed fresh per call — the plugin memoises the
91
+ # resolved set, and `freeze!` freezes the catalog itself
92
+ # so a lazy memo ivar here would raise.
93
+ #
94
+ # @return [Array<Symbol>]
95
+ def method_names
96
+ @entries.keys.map { |key| key[1] }.uniq
97
+ end
98
+
83
99
  def size
84
100
  @entries.size
85
101
  end
@@ -24,8 +24,8 @@ module Rigor
24
24
  # identically — per-call-site sigil honouring (e.g. only
25
25
  # firing `T.let` recognition in `# typed: true`+ files)
26
26
  # requires threading the file path through
27
- # `flow_contribution_for`, which lives behind a future
28
- # plugin-contract widening slice.
27
+ # the per-call recognition path, which lives behind a
28
+ # future plugin-contract widening slice.
29
29
  module SigilDetector
30
30
  # Sorbet's strictness-level names. Stored as symbols to
31
31
  # match the analyzer's existing convention for level