rigortype 0.0.9 → 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 +40 -2
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +183 -4
- 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.rb +9 -3
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- 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 +84 -5
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +95 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +105 -130
- 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 +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -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
- metadata +18 -1
|
@@ -65,6 +65,7 @@ module Rigor
|
|
|
65
65
|
Prism::NilNode => :type_of_nil,
|
|
66
66
|
# Locals
|
|
67
67
|
Prism::LocalVariableReadNode => :local_read,
|
|
68
|
+
Prism::ItLocalVariableReadNode => :it_read,
|
|
68
69
|
Prism::LocalVariableWriteNode => :type_of_assignment_write,
|
|
69
70
|
# Containers and pass-throughs
|
|
70
71
|
Prism::ArrayNode => :array_type_for,
|
|
@@ -171,9 +172,11 @@ module Rigor
|
|
|
171
172
|
Prism::WhileNode => :type_of_loop,
|
|
172
173
|
Prism::UntilNode => :type_of_loop,
|
|
173
174
|
Prism::ForNode => :type_of_dynamic_top,
|
|
174
|
-
Prism::DefinedNode => :
|
|
175
|
-
Prism::
|
|
176
|
-
Prism::
|
|
175
|
+
Prism::DefinedNode => :type_of_defined,
|
|
176
|
+
Prism::NumberedReferenceReadNode => :type_of_string_or_nil,
|
|
177
|
+
Prism::BackReferenceReadNode => :type_of_string_or_nil,
|
|
178
|
+
Prism::MatchPredicateNode => :type_of_match_predicate,
|
|
179
|
+
Prism::MatchRequiredNode => :type_of_match_required,
|
|
177
180
|
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
178
181
|
# Literal containers
|
|
179
182
|
Prism::LambdaNode => :type_of_lambda,
|
|
@@ -298,6 +301,45 @@ module Rigor
|
|
|
298
301
|
dynamic_top
|
|
299
302
|
end
|
|
300
303
|
|
|
304
|
+
# `defined?(expr)` returns `String | nil` per Ruby semantics —
|
|
305
|
+
# a description of the expression's category (`"local-variable"`,
|
|
306
|
+
# `"method"`, ...) when defined, or `nil` when not. The argument
|
|
307
|
+
# is not evaluated (it is statically inspected by the runtime),
|
|
308
|
+
# so the typer does not recurse into it.
|
|
309
|
+
def type_of_defined(_node)
|
|
310
|
+
Type::Combinator.union(
|
|
311
|
+
Type::Combinator.nominal_of("String"),
|
|
312
|
+
Type::Combinator.constant_of(nil)
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
|
|
317
|
+
# numbered-capture globals each carry `String | nil`. They share
|
|
318
|
+
# the typer because the typing rule is identical regardless of
|
|
319
|
+
# which back-reference shape Prism emitted.
|
|
320
|
+
def type_of_string_or_nil(_node)
|
|
321
|
+
Type::Combinator.union(
|
|
322
|
+
Type::Combinator.nominal_of("String"),
|
|
323
|
+
Type::Combinator.constant_of(nil)
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
328
|
+
# when the pattern matches, `false` otherwise.
|
|
329
|
+
def type_of_match_predicate(_node)
|
|
330
|
+
Type::Combinator.union(
|
|
331
|
+
Type::Combinator.constant_of(true),
|
|
332
|
+
Type::Combinator.constant_of(false)
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# `expr => pattern` — one-line pattern-match assertion. Raises
|
|
337
|
+
# `NoMatchingPatternError` on mismatch; on success the expression
|
|
338
|
+
# itself evaluates to `nil`.
|
|
339
|
+
def type_of_match_required(_node)
|
|
340
|
+
Type::Combinator.constant_of(nil)
|
|
341
|
+
end
|
|
342
|
+
|
|
301
343
|
# The expression `Foo` evaluates to the *class object* `Foo`, not
|
|
302
344
|
# an instance. From Slice 4 phase 2b on we therefore type a
|
|
303
345
|
# bare-constant reference as `Singleton[Foo]`; method dispatch on
|
|
@@ -723,9 +765,38 @@ module Rigor
|
|
|
723
765
|
def type_of_range(node)
|
|
724
766
|
left_static, left = static_range_endpoint(node.left)
|
|
725
767
|
right_static, right = static_range_endpoint(node.right)
|
|
726
|
-
return Type::Combinator.
|
|
768
|
+
return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
|
|
727
769
|
|
|
728
|
-
|
|
770
|
+
nominal_range_for_endpoints(node.left, node.right)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Derives `Nominal[Range, [T]]` from the endpoint expression
|
|
774
|
+
# types when at least one endpoint is statically typeable. The
|
|
775
|
+
# element parameter is the union of the endpoint types (lifted
|
|
776
|
+
# from `Constant<v>` to `Nominal<v.class>` so the carrier matches
|
|
777
|
+
# what `Range#each` would yield). Falls back to bare
|
|
778
|
+
# `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
779
|
+
def nominal_range_for_endpoints(left_node, right_node)
|
|
780
|
+
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
781
|
+
endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
|
|
782
|
+
return Type::Combinator.nominal_of("Range") if endpoints.empty?
|
|
783
|
+
|
|
784
|
+
Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def range_endpoint_element_type(node)
|
|
788
|
+
type = type_of(node)
|
|
789
|
+
case type
|
|
790
|
+
when Type::Constant
|
|
791
|
+
value = type.value
|
|
792
|
+
return Type::Combinator.untyped if value.nil?
|
|
793
|
+
|
|
794
|
+
Type::Combinator.nominal_of(value.class.name)
|
|
795
|
+
when Type::IntegerRange
|
|
796
|
+
Type::Combinator.nominal_of("Integer")
|
|
797
|
+
else
|
|
798
|
+
type
|
|
799
|
+
end
|
|
729
800
|
end
|
|
730
801
|
|
|
731
802
|
# v0.0.7 — non-interpolated regex literals lift to
|
|
@@ -746,6 +817,7 @@ module Rigor
|
|
|
746
817
|
def static_range_endpoint(node)
|
|
747
818
|
return [true, nil] if node.nil?
|
|
748
819
|
return [true, node.value] if node.is_a?(Prism::IntegerNode)
|
|
820
|
+
return [true, node.unescaped] if node.is_a?(Prism::StringNode) && node.respond_to?(:unescaped)
|
|
749
821
|
|
|
750
822
|
[false, nil]
|
|
751
823
|
end
|
|
@@ -805,6 +877,13 @@ module Rigor
|
|
|
805
877
|
scope.local(node.name) || dynamic_top
|
|
806
878
|
end
|
|
807
879
|
|
|
880
|
+
# `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
|
|
881
|
+
# field; the implicit name is always `:it`, matching the binding
|
|
882
|
+
# `BlockParameterBinder` installs for `Prism::ItParametersNode`.
|
|
883
|
+
def it_read(_node)
|
|
884
|
+
scope.local(:it) || dynamic_top
|
|
885
|
+
end
|
|
886
|
+
|
|
808
887
|
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
|
|
809
888
|
# when every element is a non-splat value. Splatted entries
|
|
810
889
|
# (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
|
|
@@ -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,16 @@ module Rigor
|
|
|
36
51
|
module_function
|
|
37
52
|
|
|
38
53
|
CONCAT_METHODS = %i[+ << concat].freeze
|
|
39
|
-
|
|
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)
|
|
40
60
|
|
|
41
|
-
def try_dispatch(receiver:, method_name:, args:, **)
|
|
42
61
|
return nil unless Type::Combinator.literal_string_compatible?(receiver)
|
|
62
|
+
|
|
63
|
+
return fold_string_percent(args) if method_name == :%
|
|
43
64
|
return nil unless args.size == 1
|
|
44
65
|
|
|
45
66
|
if CONCAT_METHODS.include?(method_name)
|
|
@@ -62,6 +83,69 @@ module Rigor
|
|
|
62
83
|
Type::Combinator.literal_string
|
|
63
84
|
end
|
|
64
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
|
+
|
|
65
149
|
def integer_typed?(type)
|
|
66
150
|
case type
|
|
67
151
|
when Type::Constant then type.value.is_a?(Integer)
|
|
@@ -80,7 +164,9 @@ module Rigor
|
|
|
80
164
|
type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
|
|
81
165
|
end
|
|
82
166
|
|
|
83
|
-
private_class_method :fold_concat, :fold_repeat, :
|
|
167
|
+
private_class_method :fold_concat, :fold_repeat, :fold_array_join,
|
|
168
|
+
:fold_format, :fold_string_percent,
|
|
169
|
+
:literal_or_constant?, :integer_typed?
|
|
84
170
|
end
|
|
85
171
|
end
|
|
86
172
|
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
|
|
@@ -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
|
|
|
@@ -712,6 +737,29 @@ module Rigor
|
|
|
712
737
|
]
|
|
713
738
|
end
|
|
714
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
|
+
|
|
715
763
|
# Recognised CallNode predicates:
|
|
716
764
|
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
717
765
|
# - unary `!recv` (`name == :!`, no args, no block)
|
|
@@ -734,32 +782,7 @@ module Rigor
|
|
|
734
782
|
# `rigor:v1:predicate-if-true` / `predicate-if-false`
|
|
735
783
|
# annotations, apply them to narrow the corresponding
|
|
736
784
|
# local-variable arguments on each edge.
|
|
737
|
-
|
|
738
|
-
assert_result = analyse_rbs_extended_assert_if(node, scope)
|
|
739
|
-
merge_extended_results(predicate_result, assert_result, scope)
|
|
740
|
-
end
|
|
741
|
-
|
|
742
|
-
# Combines two `[truthy_scope, falsey_scope]` pair
|
|
743
|
-
# results from sibling RBS::Extended analysers
|
|
744
|
-
# (`predicate-if-*` and `assert-if-*`). When only one
|
|
745
|
-
# side fires, return it directly; when both fire the
|
|
746
|
-
# right side's per-local deltas are applied on top of
|
|
747
|
-
# the left side's edges so the rules compose.
|
|
748
|
-
def merge_extended_results(left, right, base_scope)
|
|
749
|
-
return left if right.nil?
|
|
750
|
-
return right if left.nil?
|
|
751
|
-
|
|
752
|
-
[
|
|
753
|
-
merge_scope_pair(left[0], right[0], base_scope),
|
|
754
|
-
merge_scope_pair(left[1], right[1], base_scope)
|
|
755
|
-
]
|
|
756
|
-
end
|
|
757
|
-
|
|
758
|
-
def merge_scope_pair(left_scope, right_scope, base_scope)
|
|
759
|
-
right_scope.locals.reduce(left_scope) do |acc, (name, type)|
|
|
760
|
-
base_type = base_scope.local(name)
|
|
761
|
-
type.equal?(base_type) ? acc : acc.with_local(name, type)
|
|
762
|
-
end
|
|
785
|
+
analyse_rbs_extended_contribution(node, scope)
|
|
763
786
|
end
|
|
764
787
|
|
|
765
788
|
ZERO_CLASS_PREDICATES = %i[positive? negative? zero? nonzero?].freeze
|
|
@@ -1187,113 +1210,83 @@ module Rigor
|
|
|
1187
1210
|
node
|
|
1188
1211
|
end
|
|
1189
1212
|
|
|
1190
|
-
# Slice 7
|
|
1191
|
-
# analyser.
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
#
|
|
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.
|
|
1195
1226
|
#
|
|
1196
|
-
# Conservative envelope
|
|
1227
|
+
# Conservative envelope (carried over from the previous
|
|
1228
|
+
# implementation):
|
|
1197
1229
|
# - Receiver type must be `Type::Nominal`,
|
|
1198
1230
|
# `Type::Singleton`, or `Type::Constant`.
|
|
1199
1231
|
# - The method must be present in the loader.
|
|
1200
|
-
# - For each
|
|
1201
|
-
#
|
|
1202
|
-
#
|
|
1203
|
-
#
|
|
1204
|
-
#
|
|
1205
|
-
#
|
|
1206
|
-
#
|
|
1207
|
-
#
|
|
1208
|
-
# read-only), so `self`-targeted effects are
|
|
1209
|
-
# 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
|
|
1210
1240
|
# scope edits.
|
|
1211
|
-
def
|
|
1241
|
+
def analyse_rbs_extended_contribution(node, scope)
|
|
1212
1242
|
method_def = resolve_rbs_extended_method(node, scope)
|
|
1213
1243
|
return nil if method_def.nil?
|
|
1214
1244
|
|
|
1215
|
-
|
|
1216
|
-
return nil if
|
|
1217
|
-
|
|
1218
|
-
truthy_scope = scope
|
|
1219
|
-
falsey_scope = scope
|
|
1220
|
-
effects.each do |effect|
|
|
1221
|
-
truthy_scope, falsey_scope =
|
|
1222
|
-
apply_predicate_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
|
|
1223
|
-
end
|
|
1224
|
-
[truthy_scope, falsey_scope]
|
|
1225
|
-
end
|
|
1245
|
+
contribution = RbsExtended.read_flow_contribution(method_def)
|
|
1246
|
+
return nil if contribution.nil?
|
|
1226
1247
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
# variant is NOT applied here; `StatementEvaluator`
|
|
1232
|
-
# applies it directly to the post-call scope.
|
|
1233
|
-
def analyse_rbs_extended_assert_if(node, scope)
|
|
1234
|
-
method_def = resolve_rbs_extended_method(node, scope)
|
|
1235
|
-
return nil if method_def.nil?
|
|
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?
|
|
1236
1252
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1253
|
+
apply_facts_to_scope_pair(node, scope, truthy_facts, falsey_facts, method_def)
|
|
1254
|
+
end
|
|
1239
1255
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
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)
|
|
1245
1264
|
end
|
|
1246
1265
|
[truthy_scope, falsey_scope]
|
|
1247
1266
|
end
|
|
1248
1267
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
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)
|
|
1253
1271
|
|
|
1254
1272
|
local_name = target_node.name
|
|
1255
1273
|
current = entry_scope.local(local_name)
|
|
1256
|
-
return
|
|
1274
|
+
return target_scope if current.nil?
|
|
1257
1275
|
|
|
1258
|
-
narrowed =
|
|
1259
|
-
|
|
1260
|
-
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
1261
|
-
else
|
|
1262
|
-
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
1263
|
-
end
|
|
1264
|
-
end
|
|
1265
|
-
# rubocop:enable Metrics/ParameterLists
|
|
1266
|
-
|
|
1267
|
-
# v0.0.2 #3 — resolves an effect's target node. For
|
|
1268
|
-
# `target: <param>` we look up the matching positional
|
|
1269
|
-
# argument; for `target: self` we use the call's
|
|
1270
|
-
# receiver. In both cases the caller still requires a
|
|
1271
|
-
# `Prism::LocalVariableReadNode` for narrowing to
|
|
1272
|
-
# actually fire (the engine's narrowing surface only
|
|
1273
|
-
# rebinds locals).
|
|
1274
|
-
def effect_target_node(effect, call_node, method_def)
|
|
1275
|
-
if effect.target_kind == :self
|
|
1276
|
-
call_node.receiver
|
|
1277
|
-
else
|
|
1278
|
-
lookup_positional_arg(call_node, method_def, effect.target_name)
|
|
1279
|
-
end
|
|
1276
|
+
narrowed = narrow_for_fact(current, fact, entry_scope.environment)
|
|
1277
|
+
target_scope.with_local(local_name, narrowed)
|
|
1280
1278
|
end
|
|
1281
1279
|
|
|
1282
|
-
#
|
|
1283
|
-
# `
|
|
1284
|
-
#
|
|
1285
|
-
#
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
return effect.refinement_type
|
|
1291
|
-
end
|
|
1292
|
-
|
|
1293
|
-
if effect.negative?
|
|
1294
|
-
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
|
|
1295
1288
|
else
|
|
1296
|
-
|
|
1289
|
+
lookup_positional_arg(call_node, method_def, fact.target_name)
|
|
1297
1290
|
end
|
|
1298
1291
|
end
|
|
1299
1292
|
|
|
@@ -1332,24 +1325,6 @@ module Rigor
|
|
|
1332
1325
|
nil
|
|
1333
1326
|
end
|
|
1334
1327
|
|
|
1335
|
-
# rubocop:disable Metrics/ParameterLists
|
|
1336
|
-
def apply_predicate_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
|
|
1337
|
-
target_node = effect_target_node(effect, call_node, method_def)
|
|
1338
|
-
return [truthy_scope, falsey_scope] unless target_node.is_a?(Prism::LocalVariableReadNode)
|
|
1339
|
-
|
|
1340
|
-
local_name = target_node.name
|
|
1341
|
-
current = entry_scope.local(local_name)
|
|
1342
|
-
return [truthy_scope, falsey_scope] if current.nil?
|
|
1343
|
-
|
|
1344
|
-
narrowed = narrow_for_effect(current, effect, entry_scope.environment)
|
|
1345
|
-
if effect.truthy_only?
|
|
1346
|
-
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
1347
|
-
else
|
|
1348
|
-
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
1349
|
-
end
|
|
1350
|
-
end
|
|
1351
|
-
# rubocop:enable Metrics/ParameterLists
|
|
1352
|
-
|
|
1353
1328
|
# Maps the effect's target parameter name to the call
|
|
1354
1329
|
# site argument by inspecting the selected overload's
|
|
1355
1330
|
# required-positional parameter list. Returns the Prism
|