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.
- checksums.yaml +4 -4
- data/README.md +135 -31
- data/lib/rigor/analysis/check_rules.rb +10 -18
- data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +113 -0
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +72 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +139 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +200 -0
- data/lib/rigor/analysis/dependency_source_inference.rb +38 -0
- data/lib/rigor/analysis/diagnostic.rb +0 -2
- data/lib/rigor/analysis/fact_store.rb +11 -3
- data/lib/rigor/analysis/rule_catalog.rb +2 -2
- data/lib/rigor/analysis/runner.rb +206 -6
- data/lib/rigor/builtins/imported_refinements.rb +360 -55
- data/lib/rigor/cache/descriptor.rb +59 -6
- data/lib/rigor/cache/store.rb +1 -1
- data/lib/rigor/cli/diff_command.rb +1 -1
- data/lib/rigor/cli/sig_gen_command.rb +173 -0
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_renderer.rb +1 -1
- data/lib/rigor/cli/type_scan_report.rb +2 -2
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/dependencies.rb +235 -0
- data/lib/rigor/configuration.rb +45 -11
- data/lib/rigor/environment.rb +47 -4
- data/lib/rigor/flow_contribution/conflict.rb +2 -2
- data/lib/rigor/flow_contribution/element.rb +1 -1
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution/merge_result.rb +1 -1
- data/lib/rigor/flow_contribution/merger.rb +7 -3
- data/lib/rigor/flow_contribution.rb +2 -2
- data/lib/rigor/inference/block_parameter_binder.rb +0 -2
- data/lib/rigor/inference/coverage_scanner.rb +1 -1
- data/lib/rigor/inference/expression_typer.rb +67 -11
- data/lib/rigor/inference/fallback.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +0 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +118 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +0 -4
- data/lib/rigor/inference/method_dispatcher.rb +233 -2
- data/lib/rigor/inference/method_parameter_binder.rb +1 -3
- data/lib/rigor/inference/narrowing.rb +2 -4
- data/lib/rigor/inference/rbs_type_translator.rb +0 -2
- data/lib/rigor/inference/scope_indexer.rb +14 -9
- data/lib/rigor/inference/statement_evaluator.rb +70 -6
- data/lib/rigor/plugin/io_boundary.rb +0 -2
- data/lib/rigor/plugin/loader.rb +2 -2
- data/lib/rigor/plugin/manifest.rb +49 -7
- data/lib/rigor/plugin/registry.rb +11 -0
- data/lib/rigor/plugin/services.rb +1 -1
- data/lib/rigor/plugin/type_node_resolver.rb +52 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/reporter.rb +91 -0
- data/lib/rigor/rbs_extended.rb +131 -32
- data/lib/rigor/scope.rb +25 -8
- data/lib/rigor/sig_gen/classification.rb +36 -0
- data/lib/rigor/sig_gen/generator.rb +1048 -0
- data/lib/rigor/sig_gen/layout_index.rb +108 -0
- data/lib/rigor/sig_gen/method_candidate.rb +62 -0
- data/lib/rigor/sig_gen/observation_collector.rb +391 -0
- data/lib/rigor/sig_gen/observed_call.rb +62 -0
- data/lib/rigor/sig_gen/path_mapper.rb +116 -0
- data/lib/rigor/sig_gen/renderer.rb +157 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
- data/lib/rigor/sig_gen/write_result.rb +48 -0
- data/lib/rigor/sig_gen/writer.rb +530 -0
- data/lib/rigor/sig_gen.rb +25 -0
- data/lib/rigor/type/bound_method.rb +79 -0
- data/lib/rigor/type/combinator.rb +195 -2
- data/lib/rigor/type/constant.rb +13 -0
- data/lib/rigor/type/hash_shape.rb +0 -2
- data/lib/rigor/type/union.rb +20 -1
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_node/generic.rb +62 -0
- data/lib/rigor/type_node/identifier.rb +30 -0
- data/lib/rigor/type_node/indexed_access.rb +41 -0
- data/lib/rigor/type_node/integer_literal.rb +29 -0
- data/lib/rigor/type_node/name_scope.rb +52 -0
- data/lib/rigor/type_node/resolver_chain.rb +56 -0
- data/lib/rigor/type_node/string_literal.rb +29 -0
- data/lib/rigor/type_node/symbol_literal.rb +28 -0
- data/lib/rigor/type_node/union.rb +42 -0
- data/lib/rigor/type_node.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +2 -0
- data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
- data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
- data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
- data/sig/rigor/cli/diff_command.rbs +4 -0
- data/sig/rigor/cli/explain_command.rbs +4 -0
- data/sig/rigor/cli/sig_gen_command.rbs +4 -0
- data/sig/rigor/cli/type_scan_command.rbs +3 -0
- data/sig/rigor/environment.rbs +6 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
- data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
- data/sig/rigor/inference/builtins.rbs +2 -0
- data/sig/rigor/plugin/access_denied_error.rbs +3 -0
- data/sig/rigor/plugin/base.rbs +6 -0
- data/sig/rigor/plugin/fact_store.rbs +11 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/plugin/load_error.rbs +6 -0
- data/sig/rigor/plugin/loader.rbs +20 -0
- data/sig/rigor/plugin/manifest.rbs +9 -0
- data/sig/rigor/plugin/registry.rbs +3 -0
- data/sig/rigor/plugin/services.rbs +3 -0
- data/sig/rigor/plugin/trust_policy.rbs +4 -0
- data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
- data/sig/rigor/plugin.rbs +8 -0
- data/sig/rigor/scope.rbs +4 -2
- data/sig/rigor/type.rbs +28 -6
- 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
|
-
|
|
245
|
-
|
|
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:,
|
|
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
|
-
|
|
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)
|
|
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(
|
|
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)
|
|
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
|
|
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
|
|
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?
|