rigortype 0.0.9 → 0.1.1
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 +45 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +269 -7
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
- data/lib/rigor/cache/rbs_constant_table.rb +2 -2
- data/lib/rigor/cache/rbs_descriptor.rb +2 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/store.rb +2 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +20 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +286 -15
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +51 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/expression_typer.rb +87 -6
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +246 -127
- data/lib/rigor/inference/scope_indexer.rb +124 -16
- data/lib/rigor/inference/statement_evaluator.rb +406 -37
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +284 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +35 -0
- data/lib/rigor/plugin/loader.rb +307 -0
- data/lib/rigor/plugin/manifest.rb +203 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +77 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +62 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +10 -3
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +8 -2
- metadata +20 -1
|
@@ -8,12 +8,21 @@ module Rigor
|
|
|
8
8
|
# Dispatcher tier that lifts string-composition results into
|
|
9
9
|
# the `literal-string` carrier when every operand is itself
|
|
10
10
|
# literal-bearing. Sits between {ConstantFolding} (which
|
|
11
|
-
# handles all-Constant cases) and {ShapeDispatch}; runs
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
11
|
+
# handles all-Constant cases) and {ShapeDispatch}; runs for:
|
|
12
|
+
#
|
|
13
|
+
# - `String#+` / `String#*` / `String#<<` / `String#concat`
|
|
14
|
+
# on string-typed receivers whose inputs the
|
|
15
|
+
# ConstantFolding tier could not fold to a precise
|
|
16
|
+
# `Constant<String>` (e.g. one operand is `literal-string`
|
|
17
|
+
# rather than `Constant<String>`, or the multiplication
|
|
18
|
+
# exceeds the constant-fold size cap).
|
|
19
|
+
# - `Array#join` on `Tuple[…]` receivers whose every element
|
|
20
|
+
# plus the separator argument (when given) is
|
|
21
|
+
# literal-bearing.
|
|
22
|
+
# - `Kernel#format` / `Kernel#sprintf` (any receiver) and
|
|
23
|
+
# `String#%` (literal-bearing receiver) when every value
|
|
24
|
+
# argument is literal-bearing or a Type::Constant of any
|
|
25
|
+
# value.
|
|
17
26
|
#
|
|
18
27
|
# Result rule:
|
|
19
28
|
#
|
|
@@ -27,6 +36,12 @@ module Rigor
|
|
|
27
36
|
# literal-bearing too.
|
|
28
37
|
# - `*`: receiver MUST be literal-bearing; argument MUST be
|
|
29
38
|
# integer-typed. The result is `literal-string`.
|
|
39
|
+
# - `join`: receiver MUST be `Tuple[…]` with every element
|
|
40
|
+
# literal-string-compatible; the optional separator
|
|
41
|
+
# argument MUST also be literal-string-compatible.
|
|
42
|
+
# Result: `literal-string`. Empty `Tuple[]` lifts too —
|
|
43
|
+
# `[].join` is the empty string at runtime, which is
|
|
44
|
+
# literal-bearing trivially.
|
|
30
45
|
#
|
|
31
46
|
# Other receiver / argument shapes decline so the next tier
|
|
32
47
|
# (ShapeDispatch / FileFolding / RbsDispatch) takes over and
|
|
@@ -36,10 +51,40 @@ module Rigor
|
|
|
36
51
|
module_function
|
|
37
52
|
|
|
38
53
|
CONCAT_METHODS = %i[+ << concat].freeze
|
|
39
|
-
|
|
54
|
+
FORMAT_METHODS = %i[format sprintf].freeze
|
|
55
|
+
# v0.1.1 Track 1 slice 5a — methods that, called with no
|
|
56
|
+
# arguments on a literal-bearing receiver, return a value
|
|
57
|
+
# that is also literal-bearing. `#strip` / `#lstrip` /
|
|
58
|
+
# `#rstrip` / `#chomp` (no-arg) / `#chop` strip a known
|
|
59
|
+
# subset of characters from the ends, so the survivors
|
|
60
|
+
# are always a substring of an already-literal value.
|
|
61
|
+
# `#scrub` (no-arg) replaces invalid bytes; a literal-string
|
|
62
|
+
# value comes from source code and is always valid UTF-8,
|
|
63
|
+
# so the result is identical to the receiver. None of
|
|
64
|
+
# these preserve `non-empty-string`-ness (e.g. `" ".strip
|
|
65
|
+
# == ""`); the carrier collapses from `non-empty-literal-string`
|
|
66
|
+
# down to plain `literal-string`.
|
|
67
|
+
LITERAL_PRESERVING_METHODS = %i[strip lstrip rstrip chomp chop scrub].freeze
|
|
68
|
+
# v0.1.1 Track 1 slice 5c — width-padding methods. `center`
|
|
69
|
+
# / `ljust` / `rjust` take a `width` Integer plus an
|
|
70
|
+
# optional literal padding `String`. When the receiver
|
|
71
|
+
# and the (default or supplied) padding are both
|
|
72
|
+
# literal-bearing, the result is literal-bearing too.
|
|
73
|
+
WIDTH_PADDING_METHODS = %i[center ljust rjust].freeze
|
|
74
|
+
private_constant :CONCAT_METHODS, :FORMAT_METHODS,
|
|
75
|
+
:LITERAL_PRESERVING_METHODS, :WIDTH_PADDING_METHODS
|
|
76
|
+
|
|
77
|
+
def try_dispatch(receiver:, method_name:, args:, **) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
78
|
+
return fold_array_join(receiver, args) if method_name == :join
|
|
79
|
+
return fold_format(args) if FORMAT_METHODS.include?(method_name)
|
|
40
80
|
|
|
41
|
-
def try_dispatch(receiver:, method_name:, args:, **)
|
|
42
81
|
return nil unless Type::Combinator.literal_string_compatible?(receiver)
|
|
82
|
+
|
|
83
|
+
return fold_string_percent(args) if method_name == :%
|
|
84
|
+
if args.empty?
|
|
85
|
+
return LITERAL_PRESERVING_METHODS.include?(method_name) ? Type::Combinator.literal_string : nil
|
|
86
|
+
end
|
|
87
|
+
return fold_width_pad(args) if WIDTH_PADDING_METHODS.include?(method_name)
|
|
43
88
|
return nil unless args.size == 1
|
|
44
89
|
|
|
45
90
|
if CONCAT_METHODS.include?(method_name)
|
|
@@ -49,6 +94,23 @@ module Rigor
|
|
|
49
94
|
end
|
|
50
95
|
end
|
|
51
96
|
|
|
97
|
+
# `String#center` / `#ljust` / `#rjust` — first argument is
|
|
98
|
+
# the target width (Integer-typed), optional second
|
|
99
|
+
# argument is the padding string (must be literal-bearing
|
|
100
|
+
# for the result to stay literal). The default padding
|
|
101
|
+
# (a space) is always literal so the no-second-arg form
|
|
102
|
+
# passes through. Width is allowed to be any Integer
|
|
103
|
+
# because Ruby's runtime accepts negative widths and
|
|
104
|
+
# widths smaller than the receiver's length without
|
|
105
|
+
# raising.
|
|
106
|
+
def fold_width_pad(args)
|
|
107
|
+
return nil unless [1, 2].include?(args.size)
|
|
108
|
+
return nil unless integer_typed?(args[0])
|
|
109
|
+
return nil if args.size == 2 && !Type::Combinator.literal_string_compatible?(args[1])
|
|
110
|
+
|
|
111
|
+
Type::Combinator.literal_string
|
|
112
|
+
end
|
|
113
|
+
|
|
52
114
|
def fold_concat(arg_type)
|
|
53
115
|
return nil unless Type::Combinator.literal_string_compatible?(arg_type)
|
|
54
116
|
|
|
@@ -62,6 +124,69 @@ module Rigor
|
|
|
62
124
|
Type::Combinator.literal_string
|
|
63
125
|
end
|
|
64
126
|
|
|
127
|
+
# `[lit, lit].join(sep)` — receiver must be a Tuple
|
|
128
|
+
# whose every element is literal-bearing; separator
|
|
129
|
+
# (when given) must be literal-bearing too. Multi-arg
|
|
130
|
+
# forms / `Array#join(*args)` splat shapes don't reach
|
|
131
|
+
# here because the dispatcher only routes through this
|
|
132
|
+
# tier when the call resolves to a single named method.
|
|
133
|
+
def fold_array_join(receiver, args)
|
|
134
|
+
return nil unless receiver.is_a?(Type::Tuple)
|
|
135
|
+
return nil unless receiver.elements.all? { |el| Type::Combinator.literal_string_compatible?(el) }
|
|
136
|
+
return nil unless args.size <= 1
|
|
137
|
+
return nil if args.size == 1 && !Type::Combinator.literal_string_compatible?(args.first)
|
|
138
|
+
|
|
139
|
+
Type::Combinator.literal_string
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# `format("hello %s", lit)` / `sprintf(...)` — template
|
|
143
|
+
# plus every value argument must be literal-bearing
|
|
144
|
+
# ({Type::Combinator.literal_string_compatible?}) or a
|
|
145
|
+
# `Type::Constant` of any value (Constants are always
|
|
146
|
+
# provably literal). The template arg specifically must
|
|
147
|
+
# be literal-bearing — a Constant<Integer> first arg
|
|
148
|
+
# would not be a valid format template, so the
|
|
149
|
+
# `Type::Constant` allowance applies only to subsequent
|
|
150
|
+
# value args.
|
|
151
|
+
def fold_format(args)
|
|
152
|
+
return nil if args.empty?
|
|
153
|
+
return nil unless Type::Combinator.literal_string_compatible?(args.first)
|
|
154
|
+
return nil unless args.drop(1).all? { |arg| literal_or_constant?(arg) }
|
|
155
|
+
|
|
156
|
+
Type::Combinator.literal_string
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `"foo %s" % "x"` / `"foo %s" % ["x", "y"]` — receiver
|
|
160
|
+
# is the template (already verified literal-bearing by
|
|
161
|
+
# the caller); arg is either:
|
|
162
|
+
#
|
|
163
|
+
# - a single literal-bearing string / Constant value, or
|
|
164
|
+
# - a Tuple whose every element is literal-bearing or a
|
|
165
|
+
# Constant.
|
|
166
|
+
#
|
|
167
|
+
# Hash-form `%` (e.g. `"%{name}" % {name: "x"}`) is not
|
|
168
|
+
# yet folded — the analyzer's HashShape carrier could
|
|
169
|
+
# support this, but the v0.0.x catalogue declines and
|
|
170
|
+
# widens to Nominal[String].
|
|
171
|
+
def fold_string_percent(args)
|
|
172
|
+
return nil unless args.size == 1
|
|
173
|
+
|
|
174
|
+
arg = args.first
|
|
175
|
+
if arg.is_a?(Type::Tuple)
|
|
176
|
+
return nil unless arg.elements.all? { |el| literal_or_constant?(el) }
|
|
177
|
+
|
|
178
|
+
return Type::Combinator.literal_string
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
return nil unless literal_or_constant?(arg)
|
|
182
|
+
|
|
183
|
+
Type::Combinator.literal_string
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def literal_or_constant?(type)
|
|
187
|
+
Type::Combinator.literal_string_compatible?(type) || type.is_a?(Type::Constant)
|
|
188
|
+
end
|
|
189
|
+
|
|
65
190
|
def integer_typed?(type)
|
|
66
191
|
case type
|
|
67
192
|
when Type::Constant then type.value.is_a?(Integer)
|
|
@@ -80,7 +205,9 @@ module Rigor
|
|
|
80
205
|
type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
|
|
81
206
|
end
|
|
82
207
|
|
|
83
|
-
private_class_method :fold_concat, :fold_repeat, :
|
|
208
|
+
private_class_method :fold_concat, :fold_repeat, :fold_array_join,
|
|
209
|
+
:fold_format, :fold_string_percent, :fold_width_pad,
|
|
210
|
+
:literal_or_constant?, :integer_typed?
|
|
84
211
|
end
|
|
85
212
|
end
|
|
86
213
|
end
|
|
@@ -243,7 +243,13 @@ module Rigor
|
|
|
243
243
|
|
|
244
244
|
# rubocop:disable Metrics/ParameterLists
|
|
245
245
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:)
|
|
246
|
-
|
|
246
|
+
# Slice 4b-3 (ADR-7 § "Slice 4-A/4-B") — read the
|
|
247
|
+
# return-type override through the merger so future
|
|
248
|
+
# plugin / `:rbs_extended` bundles that also assert a
|
|
249
|
+
# `return_type` slot at this call site compose with
|
|
250
|
+
# the RBS::Extended directive instead of silently
|
|
251
|
+
# racing it.
|
|
252
|
+
override = merged_return_type(method_definition)
|
|
247
253
|
return override if override
|
|
248
254
|
|
|
249
255
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
@@ -274,6 +280,20 @@ module Rigor
|
|
|
274
280
|
end
|
|
275
281
|
# rubocop:enable Metrics/ParameterLists
|
|
276
282
|
|
|
283
|
+
# ADR-7 § "Slice 4-A/4-B" — folds the
|
|
284
|
+
# `RBS::Extended` `return:` directive (and any
|
|
285
|
+
# other `return_type`-bearing contribution future
|
|
286
|
+
# slices add at this call site) through the merger
|
|
287
|
+
# before consuming. Returns the merged return type
|
|
288
|
+
# or nil when no contribution overrides the
|
|
289
|
+
# RBS-declared return.
|
|
290
|
+
def merged_return_type(method_definition)
|
|
291
|
+
contribution = RbsExtended.read_flow_contribution(method_definition)
|
|
292
|
+
return nil if contribution.nil?
|
|
293
|
+
|
|
294
|
+
Rigor::FlowContribution::Merger.merge([contribution]).return_type
|
|
295
|
+
end
|
|
296
|
+
|
|
277
297
|
# When a block type is supplied, locate the method-level
|
|
278
298
|
# type parameter that the selected overload's block return
|
|
279
299
|
# type references and bind it to `block_type`. The
|
|
@@ -122,10 +122,25 @@ module Rigor
|
|
|
122
122
|
Type::Nominal => :dispatch_nominal_size,
|
|
123
123
|
Type::Difference => :dispatch_difference,
|
|
124
124
|
Type::Refined => :dispatch_refined,
|
|
125
|
-
Type::Intersection => :dispatch_intersection
|
|
125
|
+
Type::Intersection => :dispatch_intersection,
|
|
126
|
+
Type::IntegerRange => :dispatch_integer_range
|
|
126
127
|
}.freeze
|
|
127
128
|
private_constant :RECEIVER_HANDLERS
|
|
128
129
|
|
|
130
|
+
# v0.1.1 Track 1 slice 5b — `Integer#to_s(base)` on a
|
|
131
|
+
# non-negative `IntegerRange` receiver. The output of
|
|
132
|
+
# `n.to_s(b)` for `n >= 0` is digit-string-only (no
|
|
133
|
+
# leading sign), so when the base is in this table the
|
|
134
|
+
# result lifts to the matching imported refinement.
|
|
135
|
+
# Bases not listed (2, 36, ...) keep the v0.1.0 baseline
|
|
136
|
+
# since Rigor has no carrier for the resulting alphabet.
|
|
137
|
+
TO_S_BASE_REFINEMENTS = {
|
|
138
|
+
10 => :decimal_int_string,
|
|
139
|
+
8 => :octal_int_string,
|
|
140
|
+
16 => :hex_int_string
|
|
141
|
+
}.freeze
|
|
142
|
+
private_constant :TO_S_BASE_REFINEMENTS
|
|
143
|
+
|
|
129
144
|
def try_dispatch(receiver:, method_name:, args:)
|
|
130
145
|
args ||= []
|
|
131
146
|
handler = RECEIVER_HANDLERS[receiver.class]
|
|
@@ -184,6 +199,42 @@ module Rigor
|
|
|
184
199
|
Type::Combinator.non_negative_int
|
|
185
200
|
end
|
|
186
201
|
|
|
202
|
+
# `IntegerRange#to_s` precision (v0.1.1 Track 1 slice 5b).
|
|
203
|
+
# When the range's lower bound is `>= 0`, every member is
|
|
204
|
+
# a non-negative integer and `to_s(base)` returns a
|
|
205
|
+
# digit-string with no leading sign. The result lifts to
|
|
206
|
+
# the matching imported refinement (`decimal-int-string`
|
|
207
|
+
# for base 10, `octal-int-string` for 8, `hex-int-string`
|
|
208
|
+
# for 16). Signed ranges fall through (the result could
|
|
209
|
+
# carry a `-` sign that no Rigor refinement currently
|
|
210
|
+
# captures), as do bases without a digit-only refinement.
|
|
211
|
+
def dispatch_integer_range(range, method_name, args)
|
|
212
|
+
return nil unless method_name == :to_s
|
|
213
|
+
return nil unless range.lower >= 0
|
|
214
|
+
|
|
215
|
+
base = base_argument(args)
|
|
216
|
+
return nil if base.nil?
|
|
217
|
+
|
|
218
|
+
refinement = TO_S_BASE_REFINEMENTS[base]
|
|
219
|
+
return nil if refinement.nil?
|
|
220
|
+
|
|
221
|
+
Type::Combinator.public_send(refinement)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# `to_s` with no argument defaults to base 10. With one
|
|
225
|
+
# argument, the value MUST be a `Constant<Integer>` to
|
|
226
|
+
# be statically known. Anything else (Nominal[Integer]
|
|
227
|
+
# arg, multi-arg, etc.) declines.
|
|
228
|
+
def base_argument(args)
|
|
229
|
+
return 10 if args.empty?
|
|
230
|
+
return nil unless args.size == 1
|
|
231
|
+
|
|
232
|
+
arg = args.first
|
|
233
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
234
|
+
|
|
235
|
+
arg.value
|
|
236
|
+
end
|
|
237
|
+
|
|
187
238
|
# Refinement-aware projections over a `Difference[base,
|
|
188
239
|
# removed]` receiver. When the removed value is the
|
|
189
240
|
# empty witness of the base (`Constant[""]` for
|
|
@@ -289,7 +340,21 @@ module Rigor
|
|
|
289
340
|
%i[octal_int downcase] => :refined_self,
|
|
290
341
|
%i[octal_int upcase] => :refined_self,
|
|
291
342
|
%i[hex_int downcase] => :refined_self,
|
|
292
|
-
%i[hex_int upcase] => :refined_self
|
|
343
|
+
%i[hex_int upcase] => :refined_self,
|
|
344
|
+
# v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
|
|
345
|
+
# known digit-only string. `decimal-int-string`
|
|
346
|
+
# (`/\A\d+\z/`) and `numeric-string` (Rigor's
|
|
347
|
+
# numeric-string predicate, ASCII digits) are
|
|
348
|
+
# predicates over digit-only strings, so the parse
|
|
349
|
+
# is total over the carrier domain and the result
|
|
350
|
+
# is always `>= 0`. `non-negative-int` is the
|
|
351
|
+
# tightest carrier that captures both the lower
|
|
352
|
+
# bound and the integer-ness without inventing a
|
|
353
|
+
# narrower carrier.
|
|
354
|
+
%i[decimal_int to_i] => :non_negative_int,
|
|
355
|
+
%i[decimal_int to_int] => :non_negative_int,
|
|
356
|
+
%i[numeric to_i] => :non_negative_int,
|
|
357
|
+
%i[numeric to_int] => :non_negative_int
|
|
293
358
|
}.freeze
|
|
294
359
|
private_constant :REFINED_STRING_PROJECTIONS
|
|
295
360
|
|
|
@@ -313,6 +378,7 @@ module Rigor
|
|
|
313
378
|
when :refined_self then refined
|
|
314
379
|
when :uppercase_string then Type::Combinator.uppercase_string
|
|
315
380
|
when :lowercase_string then Type::Combinator.lowercase_string
|
|
381
|
+
when :non_negative_int then Type::Combinator.non_negative_int
|
|
316
382
|
end
|
|
317
383
|
end
|
|
318
384
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../reflection"
|
|
4
4
|
require_relative "../type"
|
|
5
|
+
require_relative "../flow_contribution"
|
|
6
|
+
require_relative "../flow_contribution/merger"
|
|
5
7
|
require_relative "method_dispatcher/constant_folding"
|
|
6
8
|
require_relative "method_dispatcher/literal_string_folding"
|
|
7
9
|
require_relative "method_dispatcher/shape_dispatch"
|
|
@@ -59,12 +61,25 @@ module Rigor
|
|
|
59
61
|
# @param environment [Rigor::Environment, nil] required for
|
|
60
62
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
61
63
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
62
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
64
|
+
def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/ParameterLists
|
|
65
|
+
block_type: nil, environment: nil,
|
|
66
|
+
call_node: nil, scope: nil)
|
|
63
67
|
return nil if receiver_type.nil?
|
|
64
68
|
|
|
65
69
|
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
|
|
66
70
|
return precise if precise
|
|
67
71
|
|
|
72
|
+
# v0.1.1 Track 2 slice 7 — plugin return-type contribution
|
|
73
|
+
# tier. Sits ahead of `RbsDispatch` so a plugin that
|
|
74
|
+
# understands a domain-specific dispatch (e.g. an
|
|
75
|
+
# `ActiveRecord::Base.find` returning `Nominal[<resolved
|
|
76
|
+
# model>]`) wins over the RBS-projected envelope. Only
|
|
77
|
+
# consults the registry when both `call_node` and `scope`
|
|
78
|
+
# are supplied — the dispatcher's own internal callers
|
|
79
|
+
# (per-element block fold, etc.) skip this tier.
|
|
80
|
+
plugin_result = try_plugin_contribution(call_node, scope)
|
|
81
|
+
return plugin_result if plugin_result
|
|
82
|
+
|
|
68
83
|
rbs_result = RbsDispatch.try_dispatch(
|
|
69
84
|
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
70
85
|
environment: environment, block_type: block_type
|
|
@@ -85,6 +100,40 @@ module Rigor
|
|
|
85
100
|
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
86
101
|
end
|
|
87
102
|
|
|
103
|
+
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
104
|
+
# slice 7. Walks every loaded plugin's
|
|
105
|
+
# `#flow_contribution_for(call_node:, scope:)` hook,
|
|
106
|
+
# collects the non-nil `FlowContribution` bundles, merges
|
|
107
|
+
# them through `FlowContribution::Merger`, and returns
|
|
108
|
+
# the merged `return_type` slot (or nil when no plugin
|
|
109
|
+
# contributed a return type).
|
|
110
|
+
#
|
|
111
|
+
# Plugins whose hook raises have their contribution
|
|
112
|
+
# silently dropped for this call so the dispatch chain
|
|
113
|
+
# keeps moving — the run-level diagnostic envelope (per
|
|
114
|
+
# ADR-2 § "Plugin Trust and I/O Policy") is owned by
|
|
115
|
+
# `Analysis::Runner#plugin_emitted_diagnostics`.
|
|
116
|
+
def try_plugin_contribution(call_node, scope)
|
|
117
|
+
return nil if call_node.nil? || scope.nil?
|
|
118
|
+
|
|
119
|
+
registry = scope.environment&.plugin_registry
|
|
120
|
+
return nil if registry.nil? || registry.empty?
|
|
121
|
+
|
|
122
|
+
contributions = collect_plugin_contributions(registry, call_node, scope)
|
|
123
|
+
return nil if contributions.empty?
|
|
124
|
+
|
|
125
|
+
FlowContribution::Merger.merge(contributions).return_type
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def collect_plugin_contributions(registry, call_node, scope)
|
|
129
|
+
registry.plugins.filter_map do |plugin|
|
|
130
|
+
contribution = plugin.flow_contribution_for(call_node: call_node, scope: scope)
|
|
131
|
+
contribution.is_a?(FlowContribution) ? contribution : nil
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
88
137
|
# Runs the precision tiers (constant fold, shape dispatch,
|
|
89
138
|
# file-path fold, block fold) in order and returns the first
|
|
90
139
|
# non-nil answer. Each tier owns its own receiver/argument
|
|
@@ -131,6 +131,8 @@ module Rigor
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
def bind_rest_target(splat_node, type, bindings)
|
|
134
|
+
return unless splat_node.is_a?(Prism::SplatNode)
|
|
135
|
+
|
|
134
136
|
expression = splat_node.expression
|
|
135
137
|
case expression
|
|
136
138
|
when Prism::LocalVariableTargetNode, Prism::RequiredParameterNode
|