rigortype 0.0.8 → 0.1.0
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 +234 -22
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +199 -4
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +55 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +81 -15
- data/lib/rigor/cli.rb +45 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +220 -32
- data/lib/rigor/environment.rb +11 -2
- 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 +179 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +110 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +134 -144
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +103 -0
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +13 -0
- data/sig/rigor/environment.rbs +7 -1
- 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 +3 -1
- metadata +38 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Dispatcher tier that lifts string-composition results into
|
|
9
|
+
# the `literal-string` carrier when every operand is itself
|
|
10
|
+
# literal-bearing. Sits between {ConstantFolding} (which
|
|
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.
|
|
26
|
+
#
|
|
27
|
+
# Result rule:
|
|
28
|
+
#
|
|
29
|
+
# - `+`, `<<`, `concat`: receiver and argument MUST both be
|
|
30
|
+
# `Type::Combinator.literal_string_compatible?`. The result
|
|
31
|
+
# is `literal-string`. `<<` and `concat` mutate the
|
|
32
|
+
# receiver at runtime; the analyzer does not track that
|
|
33
|
+
# mutation against the local's binding, but the call's
|
|
34
|
+
# *return value* is the receiver itself, and the receiver
|
|
35
|
+
# stays literal-bearing because every appended slice was
|
|
36
|
+
# literal-bearing too.
|
|
37
|
+
# - `*`: receiver MUST be literal-bearing; argument MUST be
|
|
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.
|
|
45
|
+
#
|
|
46
|
+
# Other receiver / argument shapes decline so the next tier
|
|
47
|
+
# (ShapeDispatch / FileFolding / RbsDispatch) takes over and
|
|
48
|
+
# the call site widens to the RBS-declared `Nominal[String]`
|
|
49
|
+
# as before.
|
|
50
|
+
module LiteralStringFolding
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
CONCAT_METHODS = %i[+ << concat].freeze
|
|
54
|
+
FORMAT_METHODS = %i[format sprintf].freeze
|
|
55
|
+
private_constant :CONCAT_METHODS, :FORMAT_METHODS
|
|
56
|
+
|
|
57
|
+
def try_dispatch(receiver:, method_name:, args:, **) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
58
|
+
return fold_array_join(receiver, args) if method_name == :join
|
|
59
|
+
return fold_format(args) if FORMAT_METHODS.include?(method_name)
|
|
60
|
+
|
|
61
|
+
return nil unless Type::Combinator.literal_string_compatible?(receiver)
|
|
62
|
+
|
|
63
|
+
return fold_string_percent(args) if method_name == :%
|
|
64
|
+
return nil unless args.size == 1
|
|
65
|
+
|
|
66
|
+
if CONCAT_METHODS.include?(method_name)
|
|
67
|
+
fold_concat(args.first)
|
|
68
|
+
elsif method_name == :*
|
|
69
|
+
fold_repeat(args.first)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fold_concat(arg_type)
|
|
74
|
+
return nil unless Type::Combinator.literal_string_compatible?(arg_type)
|
|
75
|
+
|
|
76
|
+
Type::Combinator.literal_string
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def fold_repeat(arg_type)
|
|
80
|
+
return nil unless integer_typed?(arg_type)
|
|
81
|
+
return nil if known_negative_integer?(arg_type)
|
|
82
|
+
|
|
83
|
+
Type::Combinator.literal_string
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `[lit, lit].join(sep)` — receiver must be a Tuple
|
|
87
|
+
# whose every element is literal-bearing; separator
|
|
88
|
+
# (when given) must be literal-bearing too. Multi-arg
|
|
89
|
+
# forms / `Array#join(*args)` splat shapes don't reach
|
|
90
|
+
# here because the dispatcher only routes through this
|
|
91
|
+
# tier when the call resolves to a single named method.
|
|
92
|
+
def fold_array_join(receiver, args)
|
|
93
|
+
return nil unless receiver.is_a?(Type::Tuple)
|
|
94
|
+
return nil unless receiver.elements.all? { |el| Type::Combinator.literal_string_compatible?(el) }
|
|
95
|
+
return nil unless args.size <= 1
|
|
96
|
+
return nil if args.size == 1 && !Type::Combinator.literal_string_compatible?(args.first)
|
|
97
|
+
|
|
98
|
+
Type::Combinator.literal_string
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# `format("hello %s", lit)` / `sprintf(...)` — template
|
|
102
|
+
# plus every value argument must be literal-bearing
|
|
103
|
+
# ({Type::Combinator.literal_string_compatible?}) or a
|
|
104
|
+
# `Type::Constant` of any value (Constants are always
|
|
105
|
+
# provably literal). The template arg specifically must
|
|
106
|
+
# be literal-bearing — a Constant<Integer> first arg
|
|
107
|
+
# would not be a valid format template, so the
|
|
108
|
+
# `Type::Constant` allowance applies only to subsequent
|
|
109
|
+
# value args.
|
|
110
|
+
def fold_format(args)
|
|
111
|
+
return nil if args.empty?
|
|
112
|
+
return nil unless Type::Combinator.literal_string_compatible?(args.first)
|
|
113
|
+
return nil unless args.drop(1).all? { |arg| literal_or_constant?(arg) }
|
|
114
|
+
|
|
115
|
+
Type::Combinator.literal_string
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# `"foo %s" % "x"` / `"foo %s" % ["x", "y"]` — receiver
|
|
119
|
+
# is the template (already verified literal-bearing by
|
|
120
|
+
# the caller); arg is either:
|
|
121
|
+
#
|
|
122
|
+
# - a single literal-bearing string / Constant value, or
|
|
123
|
+
# - a Tuple whose every element is literal-bearing or a
|
|
124
|
+
# Constant.
|
|
125
|
+
#
|
|
126
|
+
# Hash-form `%` (e.g. `"%{name}" % {name: "x"}`) is not
|
|
127
|
+
# yet folded — the analyzer's HashShape carrier could
|
|
128
|
+
# support this, but the v0.0.x catalogue declines and
|
|
129
|
+
# widens to Nominal[String].
|
|
130
|
+
def fold_string_percent(args)
|
|
131
|
+
return nil unless args.size == 1
|
|
132
|
+
|
|
133
|
+
arg = args.first
|
|
134
|
+
if arg.is_a?(Type::Tuple)
|
|
135
|
+
return nil unless arg.elements.all? { |el| literal_or_constant?(el) }
|
|
136
|
+
|
|
137
|
+
return Type::Combinator.literal_string
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
return nil unless literal_or_constant?(arg)
|
|
141
|
+
|
|
142
|
+
Type::Combinator.literal_string
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def literal_or_constant?(type)
|
|
146
|
+
Type::Combinator.literal_string_compatible?(type) || type.is_a?(Type::Constant)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def integer_typed?(type)
|
|
150
|
+
case type
|
|
151
|
+
when Type::Constant then type.value.is_a?(Integer)
|
|
152
|
+
when Type::Nominal then type.class_name == "Integer"
|
|
153
|
+
when Type::IntegerRange then true
|
|
154
|
+
else false
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# `String#*` raises ArgumentError on a negative multiplier, so a
|
|
159
|
+
# `Constant<-1>` argument is not a valid lift target. Decline so
|
|
160
|
+
# the call site keeps the existing nil-result behaviour rather
|
|
161
|
+
# than promising a `literal-string` value that could never
|
|
162
|
+
# exist at runtime.
|
|
163
|
+
def known_negative_integer?(type)
|
|
164
|
+
type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private_class_method :fold_concat, :fold_repeat, :fold_array_join,
|
|
168
|
+
:fold_format, :fold_string_percent,
|
|
169
|
+
:literal_or_constant?, :integer_typed?
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
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
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../reflection"
|
|
4
4
|
require_relative "../type"
|
|
5
5
|
require_relative "method_dispatcher/constant_folding"
|
|
6
|
+
require_relative "method_dispatcher/literal_string_folding"
|
|
6
7
|
require_relative "method_dispatcher/shape_dispatch"
|
|
7
8
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
8
9
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
@@ -101,6 +102,7 @@ module Rigor
|
|
|
101
102
|
return meta_result if meta_result
|
|
102
103
|
|
|
103
104
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
105
|
+
LiteralStringFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
104
106
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
105
107
|
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
106
108
|
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
@@ -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
|
|
@@ -293,6 +293,29 @@ module Rigor
|
|
|
293
293
|
end
|
|
294
294
|
end
|
|
295
295
|
|
|
296
|
+
# ADR-7 § "Slice 4-A" — public Fact-shaped narrowing
|
|
297
|
+
# entry. Distinguishes a `Nominal[<class>]`-typed Fact
|
|
298
|
+
# (uses `narrow_class` / `narrow_not_class` for
|
|
299
|
+
# hierarchy-aware narrowing) from a refinement-shaped
|
|
300
|
+
# Fact (refined types, IntegerRange, Difference, …).
|
|
301
|
+
# The implementation lives next to its sibling helpers
|
|
302
|
+
# `narrow_class` and `narrow_not_refinement`; consumers
|
|
303
|
+
# outside `Narrowing` (today: `StatementEvaluator`'s
|
|
304
|
+
# post-return assertion path) reach for it via
|
|
305
|
+
# `Rigor::Inference::Narrowing.narrow_for_fact`.
|
|
306
|
+
def narrow_for_fact(current, fact, environment)
|
|
307
|
+
if fact.type.is_a?(Type::Nominal) && fact.type.type_args.empty?
|
|
308
|
+
class_name = fact.type.class_name
|
|
309
|
+
return narrow_not_class(current, class_name, exact: false, environment: environment) if fact.negative?
|
|
310
|
+
|
|
311
|
+
return narrow_class(current, class_name, exact: false, environment: environment)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
return narrow_not_refinement(current, fact.type) if fact.negative?
|
|
315
|
+
|
|
316
|
+
fact.type
|
|
317
|
+
end
|
|
318
|
+
|
|
296
319
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
|
|
297
320
|
# always; when no narrowing rule matches the predicate node both
|
|
298
321
|
# entries are the receiver scope unchanged.
|
|
@@ -352,7 +375,7 @@ module Rigor
|
|
|
352
375
|
# the predicate shape is recognised, or `nil` to signal "no
|
|
353
376
|
# narrowing" so the public surface can fall back to the entry
|
|
354
377
|
# scope.
|
|
355
|
-
def analyse(node, scope)
|
|
378
|
+
def analyse(node, scope) # rubocop:disable Metrics/CyclomaticComplexity
|
|
356
379
|
case node
|
|
357
380
|
when Prism::ParenthesesNode
|
|
358
381
|
analyse_parentheses(node, scope)
|
|
@@ -366,6 +389,8 @@ module Rigor
|
|
|
366
389
|
analyse_and(node, scope)
|
|
367
390
|
when Prism::OrNode
|
|
368
391
|
analyse_or(node, scope)
|
|
392
|
+
when Prism::MatchWriteNode
|
|
393
|
+
analyse_match_write(node, scope)
|
|
369
394
|
end
|
|
370
395
|
end
|
|
371
396
|
|
|
@@ -539,26 +564,41 @@ module Rigor
|
|
|
539
564
|
# downstream narrowing knows the refinement subset is
|
|
540
565
|
# excluded.
|
|
541
566
|
#
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
546
|
-
#
|
|
547
|
-
#
|
|
567
|
+
# v0.0.9 — when the predicate has a registered
|
|
568
|
+
# complement (see {Type::Refined::COMPLEMENT_PAIRS}) and
|
|
569
|
+
# the part is exactly the refinement's base, the
|
|
570
|
+
# narrowing returns `Refined[base, complement_predicate]`
|
|
571
|
+
# instead of `Difference[base, refined]`. This is the
|
|
572
|
+
# `~T` symmetry the spec promises: `~lowercase-string`
|
|
573
|
+
# narrows `String` to `non-lowercase-string` rather than
|
|
574
|
+
# `Difference[String, lowercase-string]`.
|
|
575
|
+
#
|
|
576
|
+
# Predicates without a registered complement still fall
|
|
577
|
+
# back to the imprecise but sound `Difference[part,
|
|
578
|
+
# refined]` carrier so behaviour is unchanged for
|
|
579
|
+
# untouched call sites.
|
|
548
580
|
def complement_refined(current_type, refined)
|
|
549
|
-
|
|
581
|
+
complement = registered_complement_for(refined)
|
|
550
582
|
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
583
|
+
survivors = parts.filter_map { |part| complement_refined_part(part, refined, complement) }
|
|
584
|
+
return current_type if survivors.empty?
|
|
585
|
+
|
|
586
|
+
Type::Combinator.union(*survivors)
|
|
587
|
+
end
|
|
551
588
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
589
|
+
def complement_refined_part(part, refined, complement)
|
|
590
|
+
return nil if part == refined
|
|
591
|
+
return part if base_disjoint?(refined.base, part)
|
|
592
|
+
return complement if complement && part == refined.base
|
|
555
593
|
|
|
556
|
-
|
|
557
|
-
|
|
594
|
+
Type::Combinator.difference(part, refined)
|
|
595
|
+
end
|
|
558
596
|
|
|
559
|
-
|
|
597
|
+
def registered_complement_for(refined)
|
|
598
|
+
complement_id = refined.complement_predicate_id
|
|
599
|
+
return nil if complement_id.nil?
|
|
560
600
|
|
|
561
|
-
Type::Combinator.
|
|
601
|
+
Type::Combinator.refined(refined.base, complement_id)
|
|
562
602
|
end
|
|
563
603
|
|
|
564
604
|
def falsey_value?(value)
|
|
@@ -697,6 +737,29 @@ module Rigor
|
|
|
697
737
|
]
|
|
698
738
|
end
|
|
699
739
|
|
|
740
|
+
# `if /(?<x>...)/ =~ str` — Prism wraps the `=~` call in a
|
|
741
|
+
# `MatchWriteNode` listing the named-capture targets. The
|
|
742
|
+
# parent `eval_match_write` has already bound each target
|
|
743
|
+
# to `String | nil`; in the truthy branch (the regex
|
|
744
|
+
# matched) every named capture is guaranteed `String`,
|
|
745
|
+
# and in the falsey branch (no match) every capture is
|
|
746
|
+
# `nil`. Subtract the dead half on each edge so callers
|
|
747
|
+
# like `year.upcase` inside the truthy branch no longer
|
|
748
|
+
# fire `possible-nil-receiver`.
|
|
749
|
+
def analyse_match_write(node, scope)
|
|
750
|
+
string_t = Type::Combinator.nominal_of("String")
|
|
751
|
+
nil_t = Type::Combinator.constant_of(nil)
|
|
752
|
+
truthy = scope
|
|
753
|
+
falsey = scope
|
|
754
|
+
node.targets.each do |target|
|
|
755
|
+
next unless target.is_a?(Prism::LocalVariableTargetNode)
|
|
756
|
+
|
|
757
|
+
truthy = truthy.with_local(target.name, string_t)
|
|
758
|
+
falsey = falsey.with_local(target.name, nil_t)
|
|
759
|
+
end
|
|
760
|
+
[truthy, falsey]
|
|
761
|
+
end
|
|
762
|
+
|
|
700
763
|
# Recognised CallNode predicates:
|
|
701
764
|
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
702
765
|
# - unary `!recv` (`name == :!`, no args, no block)
|
|
@@ -719,32 +782,7 @@ module Rigor
|
|
|
719
782
|
# `rigor:v1:predicate-if-true` / `predicate-if-false`
|
|
720
783
|
# annotations, apply them to narrow the corresponding
|
|
721
784
|
# local-variable arguments on each edge.
|
|
722
|
-
|
|
723
|
-
assert_result = analyse_rbs_extended_assert_if(node, scope)
|
|
724
|
-
merge_extended_results(predicate_result, assert_result, scope)
|
|
725
|
-
end
|
|
726
|
-
|
|
727
|
-
# Combines two `[truthy_scope, falsey_scope]` pair
|
|
728
|
-
# results from sibling RBS::Extended analysers
|
|
729
|
-
# (`predicate-if-*` and `assert-if-*`). When only one
|
|
730
|
-
# side fires, return it directly; when both fire the
|
|
731
|
-
# right side's per-local deltas are applied on top of
|
|
732
|
-
# the left side's edges so the rules compose.
|
|
733
|
-
def merge_extended_results(left, right, base_scope)
|
|
734
|
-
return left if right.nil?
|
|
735
|
-
return right if left.nil?
|
|
736
|
-
|
|
737
|
-
[
|
|
738
|
-
merge_scope_pair(left[0], right[0], base_scope),
|
|
739
|
-
merge_scope_pair(left[1], right[1], base_scope)
|
|
740
|
-
]
|
|
741
|
-
end
|
|
742
|
-
|
|
743
|
-
def merge_scope_pair(left_scope, right_scope, base_scope)
|
|
744
|
-
right_scope.locals.reduce(left_scope) do |acc, (name, type)|
|
|
745
|
-
base_type = base_scope.local(name)
|
|
746
|
-
type.equal?(base_type) ? acc : acc.with_local(name, type)
|
|
747
|
-
end
|
|
785
|
+
analyse_rbs_extended_contribution(node, scope)
|
|
748
786
|
end
|
|
749
787
|
|
|
750
788
|
ZERO_CLASS_PREDICATES = %i[positive? negative? zero? nonzero?].freeze
|
|
@@ -1172,113 +1210,83 @@ module Rigor
|
|
|
1172
1210
|
node
|
|
1173
1211
|
end
|
|
1174
1212
|
|
|
1175
|
-
# Slice 7
|
|
1176
|
-
# analyser.
|
|
1177
|
-
#
|
|
1178
|
-
#
|
|
1179
|
-
#
|
|
1213
|
+
# Slice 4b-1 (ADR-7 § "Slice 4-A/4-B") — single-point
|
|
1214
|
+
# RBS::Extended contribution analyser. Replaces the
|
|
1215
|
+
# earlier sibling pair (`analyse_rbs_extended_predicate`
|
|
1216
|
+
# + `analyse_rbs_extended_assert_if`) with one path that
|
|
1217
|
+
# routes through `RbsExtended.read_flow_contribution` and
|
|
1218
|
+
# `Rigor::FlowContribution::Merger.merge`. The bundle's
|
|
1219
|
+
# `truthy_facts` slot already includes both the
|
|
1220
|
+
# `predicate-if-true` and `assert-if-true` Facts (slice
|
|
1221
|
+
# 4a routing closed the v0.0.9 imperfection); the
|
|
1222
|
+
# `falsey_facts` slot mirrors that. The merger composes
|
|
1223
|
+
# any future plugin contribution at the same site
|
|
1224
|
+
# alongside the RBS::Extended bundle without changing
|
|
1225
|
+
# this analyser.
|
|
1180
1226
|
#
|
|
1181
|
-
# Conservative envelope
|
|
1227
|
+
# Conservative envelope (carried over from the previous
|
|
1228
|
+
# implementation):
|
|
1182
1229
|
# - Receiver type must be `Type::Nominal`,
|
|
1183
1230
|
# `Type::Singleton`, or `Type::Constant`.
|
|
1184
1231
|
# - The method must be present in the loader.
|
|
1185
|
-
# - For each
|
|
1186
|
-
#
|
|
1187
|
-
#
|
|
1188
|
-
#
|
|
1189
|
-
#
|
|
1190
|
-
#
|
|
1191
|
-
#
|
|
1192
|
-
#
|
|
1193
|
-
# read-only), so `self`-targeted effects are
|
|
1194
|
-
# accepted by the parser but currently produce no
|
|
1232
|
+
# - For each fact, the corresponding positional
|
|
1233
|
+
# argument (matched by parameter name in the selected
|
|
1234
|
+
# overload) MUST be a `Prism::LocalVariableReadNode`
|
|
1235
|
+
# for narrowing to apply.
|
|
1236
|
+
# - When the target is `self`, narrowing applies to the
|
|
1237
|
+
# receiver — but the engine does not yet narrow
|
|
1238
|
+
# `self` itself, so `self`-targeted facts are
|
|
1239
|
+
# accepted by the merger but currently produce no
|
|
1195
1240
|
# scope edits.
|
|
1196
|
-
def
|
|
1241
|
+
def analyse_rbs_extended_contribution(node, scope)
|
|
1197
1242
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
1198
1243
|
return nil if method_def.nil?
|
|
1199
1244
|
|
|
1200
|
-
|
|
1201
|
-
return nil if
|
|
1245
|
+
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
1246
|
+
return nil if contribution.nil?
|
|
1202
1247
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
apply_predicate_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
|
|
1208
|
-
end
|
|
1209
|
-
[truthy_scope, falsey_scope]
|
|
1210
|
-
end
|
|
1248
|
+
result = Rigor::FlowContribution::Merger.merge([contribution])
|
|
1249
|
+
truthy_facts = result.truthy_facts
|
|
1250
|
+
falsey_facts = result.falsey_facts
|
|
1251
|
+
return nil if truthy_facts.empty? && falsey_facts.empty?
|
|
1211
1252
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
# method and narrows the matching argument on the
|
|
1215
|
-
# corresponding edge. The unconditional `assert`
|
|
1216
|
-
# variant is NOT applied here; `StatementEvaluator`
|
|
1217
|
-
# applies it directly to the post-call scope.
|
|
1218
|
-
def analyse_rbs_extended_assert_if(node, scope)
|
|
1219
|
-
method_def = resolve_rbs_extended_method(node, scope)
|
|
1220
|
-
return nil if method_def.nil?
|
|
1221
|
-
|
|
1222
|
-
effects = RbsExtended.read_assert_effects(method_def).reject(&:always?)
|
|
1223
|
-
return nil if effects.empty?
|
|
1253
|
+
apply_facts_to_scope_pair(node, scope, truthy_facts, falsey_facts, method_def)
|
|
1254
|
+
end
|
|
1224
1255
|
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1256
|
+
def apply_facts_to_scope_pair(call_node, entry_scope, truthy_facts, falsey_facts, method_def)
|
|
1257
|
+
truthy_scope = entry_scope
|
|
1258
|
+
falsey_scope = entry_scope
|
|
1259
|
+
truthy_facts.each do |fact|
|
|
1260
|
+
truthy_scope = apply_fact_to_scope(fact, call_node, entry_scope, truthy_scope, method_def)
|
|
1261
|
+
end
|
|
1262
|
+
falsey_facts.each do |fact|
|
|
1263
|
+
falsey_scope = apply_fact_to_scope(fact, call_node, entry_scope, falsey_scope, method_def)
|
|
1230
1264
|
end
|
|
1231
1265
|
[truthy_scope, falsey_scope]
|
|
1232
1266
|
end
|
|
1233
1267
|
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
1268
|
+
def apply_fact_to_scope(fact, call_node, entry_scope, target_scope, method_def)
|
|
1269
|
+
target_node = fact_target_node(fact, call_node, method_def)
|
|
1270
|
+
return target_scope unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
1238
1271
|
|
|
1239
1272
|
local_name = target_node.name
|
|
1240
1273
|
current = entry_scope.local(local_name)
|
|
1241
|
-
return
|
|
1274
|
+
return target_scope if current.nil?
|
|
1242
1275
|
|
|
1243
|
-
narrowed =
|
|
1244
|
-
|
|
1245
|
-
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
1246
|
-
else
|
|
1247
|
-
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
1248
|
-
end
|
|
1249
|
-
end
|
|
1250
|
-
# rubocop:enable Metrics/ParameterLists
|
|
1251
|
-
|
|
1252
|
-
# v0.0.2 #3 — resolves an effect's target node. For
|
|
1253
|
-
# `target: <param>` we look up the matching positional
|
|
1254
|
-
# argument; for `target: self` we use the call's
|
|
1255
|
-
# receiver. In both cases the caller still requires a
|
|
1256
|
-
# `Prism::LocalVariableReadNode` for narrowing to
|
|
1257
|
-
# actually fire (the engine's narrowing surface only
|
|
1258
|
-
# rebinds locals).
|
|
1259
|
-
def effect_target_node(effect, call_node, method_def)
|
|
1260
|
-
if effect.target_kind == :self
|
|
1261
|
-
call_node.receiver
|
|
1262
|
-
else
|
|
1263
|
-
lookup_positional_arg(call_node, method_def, effect.target_name)
|
|
1264
|
-
end
|
|
1276
|
+
narrowed = narrow_for_fact(current, fact, entry_scope.environment)
|
|
1277
|
+
target_scope.with_local(local_name, narrowed)
|
|
1265
1278
|
end
|
|
1266
1279
|
|
|
1267
|
-
#
|
|
1268
|
-
# `
|
|
1269
|
-
#
|
|
1270
|
-
#
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
return effect.refinement_type
|
|
1276
|
-
end
|
|
1277
|
-
|
|
1278
|
-
if effect.negative?
|
|
1279
|
-
narrow_not_class(current, effect.class_name, exact: false, environment: environment)
|
|
1280
|
+
# Resolves a Fact's target node. Mirrors the earlier
|
|
1281
|
+
# `effect_target_node` helper but reads from the
|
|
1282
|
+
# canonical Fact carrier; `:self` routes to the call
|
|
1283
|
+
# receiver, otherwise we look up the matching
|
|
1284
|
+
# positional argument by parameter name.
|
|
1285
|
+
def fact_target_node(fact, call_node, method_def)
|
|
1286
|
+
if fact.target_kind == :self
|
|
1287
|
+
call_node.receiver
|
|
1280
1288
|
else
|
|
1281
|
-
|
|
1289
|
+
lookup_positional_arg(call_node, method_def, fact.target_name)
|
|
1282
1290
|
end
|
|
1283
1291
|
end
|
|
1284
1292
|
|
|
@@ -1317,24 +1325,6 @@ module Rigor
|
|
|
1317
1325
|
nil
|
|
1318
1326
|
end
|
|
1319
1327
|
|
|
1320
|
-
# rubocop:disable Metrics/ParameterLists
|
|
1321
|
-
def apply_predicate_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
|
|
1322
|
-
target_node = effect_target_node(effect, call_node, method_def)
|
|
1323
|
-
return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
1324
|
-
|
|
1325
|
-
local_name = target_node.name
|
|
1326
|
-
current = entry_scope.local(local_name)
|
|
1327
|
-
return [truthy_scope, falsey_scope] if current.nil?
|
|
1328
|
-
|
|
1329
|
-
narrowed = narrow_for_effect(current, effect, entry_scope.environment)
|
|
1330
|
-
if effect.truthy_only?
|
|
1331
|
-
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
1332
|
-
else
|
|
1333
|
-
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
1334
|
-
end
|
|
1335
|
-
end
|
|
1336
|
-
# rubocop:enable Metrics/ParameterLists
|
|
1337
|
-
|
|
1338
1328
|
# Maps the effect's target parameter name to the call
|
|
1339
1329
|
# site argument by inspecting the selected overload's
|
|
1340
1330
|
# required-positional parameter list. Returns the Prism
|