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.
- checksums.yaml +4 -4
- data/README.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules.rb +34 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +160 -1190
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli.rb +15 -614
- data/lib/rigor/configuration.rb +9 -6
- data/lib/rigor/environment/rbs_loader.rb +53 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/expression_typer.rb +28 -62
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher.rb +115 -54
- data/lib/rigor/inference/narrowing.rb +60 -0
- data/lib/rigor/inference/scope_indexer.rb +75 -15
- data/lib/rigor/inference/statement_evaluator.rb +35 -52
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +282 -41
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +263 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +67 -198
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/combinator.rb +5 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/scope.rbs +41 -29
- data/sig/rigor/source.rbs +1 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +15 -2
- 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
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
# the
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
# `
|
|
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 `
|
|
54
|
-
#
|
|
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,
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
98
|
+
next nil if index.nil? || index.empty?
|
|
97
99
|
|
|
98
100
|
receiver_type = scope.type_of(call_node.receiver)
|
|
99
|
-
|
|
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
|
-
|
|
105
|
+
next nil if attachments.nil?
|
|
104
106
|
|
|
105
107
|
attachment = attachments.find { |a| a[:name] == call_node.name.to_s }
|
|
106
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
Rigor::
|
|
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 `
|
|
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
|
|
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) &&
|
|
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) &&
|
|
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
|
-
# `
|
|
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
|
|
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
|
|
83
|
-
# `
|
|
84
|
-
#
|
|
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 —
|
|
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` /
|
|
105
|
-
# their `let(:name) { ... }` block's inferred return
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
120
|
-
def
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
33
|
+
# context. The rule:
|
|
34
34
|
#
|
|
35
|
-
# -
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
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 `
|
|
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
|
|
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
|
-
#
|
|
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
|