rigortype 0.0.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 +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- metadata +260 -0
|
@@ -0,0 +1,1008 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "../environment"
|
|
7
|
+
require_relative "../rbs_extended"
|
|
8
|
+
require_relative "../analysis/fact_store"
|
|
9
|
+
|
|
10
|
+
module Rigor
|
|
11
|
+
module Inference
|
|
12
|
+
# Slice 6 phase 1 minimal narrowing surface.
|
|
13
|
+
#
|
|
14
|
+
# `Rigor::Inference::Narrowing` answers two related questions:
|
|
15
|
+
#
|
|
16
|
+
# 1. Type-level narrowing: given a `Rigor::Type` value, what is its
|
|
17
|
+
# truthy fragment, its falsey fragment, its nil fragment, and its
|
|
18
|
+
# non-nil fragment? These primitives understand the value-lattice
|
|
19
|
+
# algebra (`Constant`, `Nominal`, `Singleton`, `Tuple`, `HashShape`,
|
|
20
|
+
# `Union`) and stay conservative on `Top` and `Dynamic[T]`, where
|
|
21
|
+
# the analyzer cannot prove the boundary either way.
|
|
22
|
+
# 2. Predicate-level narrowing: given a Prism predicate node and an
|
|
23
|
+
# entry scope, what are the truthy-edge scope and the falsey-edge
|
|
24
|
+
# scope after the predicate has been evaluated? The phase 1
|
|
25
|
+
# catalogue covers truthiness on `LocalVariableReadNode`, `nil?`
|
|
26
|
+
# against a local, the unary `!` inverter, parenthesised
|
|
27
|
+
# predicates, and short-circuiting `&&` / `||` chains.
|
|
28
|
+
#
|
|
29
|
+
# Predicate-level narrowing is consumed by
|
|
30
|
+
# `Rigor::Inference::StatementEvaluator` to refine the `then` and
|
|
31
|
+
# `else` scopes of `IfNode`/`UnlessNode`. Phase 1 narrows local
|
|
32
|
+
# bindings on truthiness and `nil?`; phase 2 extends the catalogue
|
|
33
|
+
# with class-membership predicates (`is_a?`, `kind_of?`,
|
|
34
|
+
# `instance_of?`) and trusted equality/inequality checks against
|
|
35
|
+
# static literals.
|
|
36
|
+
#
|
|
37
|
+
# The module is pure: every public function returns fresh values and
|
|
38
|
+
# MUST NOT mutate its inputs. Unrecognised predicate shapes degrade
|
|
39
|
+
# silently to "no narrowing" by returning `nil` from the internal
|
|
40
|
+
# analyser; the public `predicate_scopes` always returns an
|
|
41
|
+
# `[truthy_scope, falsey_scope]` pair (the entry scope twice when no
|
|
42
|
+
# rule matches).
|
|
43
|
+
#
|
|
44
|
+
# See docs/internal-spec/inference-engine.md (Slice 6 — Narrowing)
|
|
45
|
+
# and docs/type-specification/control-flow-analysis.md for the
|
|
46
|
+
# binding contract.
|
|
47
|
+
# rubocop:disable Metrics/ModuleLength
|
|
48
|
+
module Narrowing
|
|
49
|
+
TRUSTED_EQUALITY_LITERAL_CLASSES = [String, Symbol, Integer, TrueClass, FalseClass, NilClass].freeze
|
|
50
|
+
SINGLETON_LITERAL_CLASSES = [TrueClass, FalseClass, NilClass].freeze
|
|
51
|
+
ClassNarrowingContext = Data.define(:exact, :polarity, :environment)
|
|
52
|
+
private_constant :TRUSTED_EQUALITY_LITERAL_CLASSES, :SINGLETON_LITERAL_CLASSES, :ClassNarrowingContext
|
|
53
|
+
|
|
54
|
+
module_function
|
|
55
|
+
|
|
56
|
+
# Truthy fragment of `type`: the subset whose inhabitants are truthy
|
|
57
|
+
# in Ruby's sense (anything other than `nil` and `false`).
|
|
58
|
+
#
|
|
59
|
+
# `Top`, `Dynamic[T]`, `Bot`, `Singleton[C]`, `Tuple[*]`, and
|
|
60
|
+
# `HashShape{*}` flow through unchanged: Top/Dynamic stay
|
|
61
|
+
# conservative because the analyzer cannot express the
|
|
62
|
+
# difference type without a richer carrier and Dynamic must
|
|
63
|
+
# preserve its provenance under the value-lattice algebra; the
|
|
64
|
+
# remaining carriers are already truthy by inhabitance.
|
|
65
|
+
def narrow_truthy(type)
|
|
66
|
+
case type
|
|
67
|
+
when Type::Constant
|
|
68
|
+
falsey_value?(type.value) ? Type::Combinator.bot : type
|
|
69
|
+
when Type::Nominal
|
|
70
|
+
falsey_nominal?(type) ? Type::Combinator.bot : type
|
|
71
|
+
when Type::Union
|
|
72
|
+
Type::Combinator.union(*type.members.map { |m| narrow_truthy(m) })
|
|
73
|
+
else
|
|
74
|
+
type
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Falsey fragment of `type`: the subset whose inhabitants are
|
|
79
|
+
# `nil` or `false`. Carriers that cannot inhabit a falsey value
|
|
80
|
+
# collapse to `Bot`.
|
|
81
|
+
def narrow_falsey(type)
|
|
82
|
+
case type
|
|
83
|
+
when Type::Constant then falsey_value?(type.value) ? type : Type::Combinator.bot
|
|
84
|
+
when Type::Nominal then falsey_nominal?(type) ? type : Type::Combinator.bot
|
|
85
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |m| narrow_falsey(m) })
|
|
86
|
+
else narrow_falsey_other(type)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Nil fragment of `type`: the subset whose inhabitants are `nil`.
|
|
91
|
+
# Used by `nil?` predicate narrowing. `Top`/`Dynamic` narrow to
|
|
92
|
+
# the canonical `Constant[nil]` so downstream dispatch resolves
|
|
93
|
+
# through `NilClass`; carriers that never inhabit `nil`
|
|
94
|
+
# (`Singleton`, `Tuple`, `HashShape`) collapse to `Bot`. `Bot`
|
|
95
|
+
# is its own nil fragment.
|
|
96
|
+
def narrow_nil(type)
|
|
97
|
+
case type
|
|
98
|
+
when Type::Constant then type.value.nil? ? type : Type::Combinator.bot
|
|
99
|
+
when Type::Nominal then type.class_name == "NilClass" ? type : Type::Combinator.bot
|
|
100
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |m| narrow_nil(m) })
|
|
101
|
+
else narrow_nil_other(type)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Non-nil fragment of `type`: the subset whose inhabitants are
|
|
106
|
+
# not `nil`. Mirror of {.narrow_nil} for the falsey edge of
|
|
107
|
+
# `x.nil?`.
|
|
108
|
+
def narrow_non_nil(type)
|
|
109
|
+
case type
|
|
110
|
+
when Type::Constant
|
|
111
|
+
type.value.nil? ? Type::Combinator.bot : type
|
|
112
|
+
when Type::Nominal
|
|
113
|
+
type.class_name == "NilClass" ? Type::Combinator.bot : type
|
|
114
|
+
when Type::Union
|
|
115
|
+
Type::Combinator.union(*type.members.map { |m| narrow_non_nil(m) })
|
|
116
|
+
else
|
|
117
|
+
# Top, Dynamic, Singleton, Tuple, HashShape, Bot: there is
|
|
118
|
+
# no nil contribution to remove, so the type is its own
|
|
119
|
+
# non-nil fragment.
|
|
120
|
+
type
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Equality fragment of `type` against a trusted literal.
|
|
125
|
+
#
|
|
126
|
+
# String/Symbol/Integer equality narrows only when the current
|
|
127
|
+
# domain is already a finite union of trusted literals. Nil and
|
|
128
|
+
# booleans are singleton values, so they can be extracted from a
|
|
129
|
+
# mixed union such as `Integer | nil` without manufacturing a new
|
|
130
|
+
# positive domain from the comparison alone.
|
|
131
|
+
def narrow_equal(type, literal)
|
|
132
|
+
return type unless trusted_equality_literal?(literal)
|
|
133
|
+
|
|
134
|
+
if singleton_literal?(literal)
|
|
135
|
+
narrow_singleton_equal(type, literal)
|
|
136
|
+
elsif finite_trusted_literal_domain?(type)
|
|
137
|
+
narrow_finite_equal(type, literal)
|
|
138
|
+
else
|
|
139
|
+
type
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Complement of {.narrow_equal}. Negative facts are domain-relative:
|
|
144
|
+
# they remove a literal from an already-known domain but do not create
|
|
145
|
+
# an unbounded difference type when the domain is broad or dynamic.
|
|
146
|
+
def narrow_not_equal(type, literal)
|
|
147
|
+
return type unless trusted_equality_literal?(literal)
|
|
148
|
+
|
|
149
|
+
if singleton_literal?(literal)
|
|
150
|
+
narrow_singleton_not_equal(type, literal)
|
|
151
|
+
elsif finite_trusted_literal_domain?(type)
|
|
152
|
+
narrow_finite_not_equal(type, literal)
|
|
153
|
+
else
|
|
154
|
+
type
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Class-membership fragment of `type`: the subset whose
|
|
159
|
+
# inhabitants are instances of `class_name` (or its subclasses
|
|
160
|
+
# when `exact: false`). `class_name` is the qualified name of
|
|
161
|
+
# the class as it appears in source (`"Integer"`, `"Foo::Bar"`).
|
|
162
|
+
# Slice 6 phase 2 sub-phase 1 narrows the `if x.is_a?(C)`
|
|
163
|
+
# / `if x.kind_of?(C)` / `if x.instance_of?(C)` truthy edge.
|
|
164
|
+
#
|
|
165
|
+
# Nominal narrowing is hierarchy-aware through the analyzer
|
|
166
|
+
# environment: when the bound type is a supertype of
|
|
167
|
+
# `class_name` the result narrows DOWN to `Nominal[class_name]`
|
|
168
|
+
# (e.g., `Numeric & Integer = Integer`); when the bound type is
|
|
169
|
+
# already a subtype it is preserved; disjoint hierarchies
|
|
170
|
+
# collapse to `Bot`. Classes the environment cannot resolve
|
|
171
|
+
# fall back to the conservative answer (the type unchanged) so
|
|
172
|
+
# the analyzer never asserts narrowing it cannot prove.
|
|
173
|
+
def narrow_class(type, class_name, exact: false, environment: Environment.default)
|
|
174
|
+
context = ClassNarrowingContext.new(exact: exact, polarity: :positive, environment: environment)
|
|
175
|
+
narrow_class_dispatch(type, class_name, context)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Mirror of {.narrow_class} for the falsey edge of
|
|
179
|
+
# `is_a?`/`kind_of?`/`instance_of?`. Inhabitants that DO
|
|
180
|
+
# satisfy the predicate are removed; inhabitants that do not
|
|
181
|
+
# are preserved. Conservative on Top/Dynamic/Bot (preserved
|
|
182
|
+
# unchanged) because the analyzer cannot prove the negative
|
|
183
|
+
# without a richer carrier.
|
|
184
|
+
def narrow_not_class(type, class_name, exact: false, environment: Environment.default)
|
|
185
|
+
context = ClassNarrowingContext.new(exact: exact, polarity: :negative, environment: environment)
|
|
186
|
+
narrow_class_dispatch(type, class_name, context)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
|
|
190
|
+
# always; when no narrowing rule matches the predicate node both
|
|
191
|
+
# entries are the receiver scope unchanged.
|
|
192
|
+
#
|
|
193
|
+
# @param node [Prism::Node, nil]
|
|
194
|
+
# @param scope [Rigor::Scope]
|
|
195
|
+
# @return [Array(Rigor::Scope, Rigor::Scope)]
|
|
196
|
+
def predicate_scopes(node, scope)
|
|
197
|
+
return [scope, scope] if node.nil?
|
|
198
|
+
|
|
199
|
+
result = analyse(node, scope)
|
|
200
|
+
result || [scope, scope]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Slice 7 phase 5 — `case`/`when` narrowing.
|
|
204
|
+
#
|
|
205
|
+
# Given the subject of a `case` (the expression after the
|
|
206
|
+
# `case` keyword) and an array of `when`-clause condition
|
|
207
|
+
# nodes (`when_clause.conditions`), returns a pair of
|
|
208
|
+
# scopes:
|
|
209
|
+
#
|
|
210
|
+
# - `body_scope`: the scope under which the body of the
|
|
211
|
+
# `when` clause MUST be evaluated. The subject local is
|
|
212
|
+
# narrowed by the union of every condition's truthy
|
|
213
|
+
# edge so the body sees the most specific type
|
|
214
|
+
# compatible with "any of the conditions matched".
|
|
215
|
+
# - `falsey_scope`: the scope under which the next branch
|
|
216
|
+
# (the next `when` or the `else`) MUST be evaluated.
|
|
217
|
+
# The subject is narrowed by the conjunction of every
|
|
218
|
+
# condition's falsey edge.
|
|
219
|
+
#
|
|
220
|
+
# The narrowing is best-effort: if the subject is not a
|
|
221
|
+
# `Prism::LocalVariableReadNode` or none of the condition
|
|
222
|
+
# shapes are recognised, both returned scopes equal the
|
|
223
|
+
# input scope. The catalogue mirrors
|
|
224
|
+
# {.case_equality_target_class}: static class/module
|
|
225
|
+
# constants narrow as `is_a?`; integer/float-endpoint
|
|
226
|
+
# ranges narrow to `Numeric`; string-endpoint ranges and
|
|
227
|
+
# regexp literals narrow to `String`.
|
|
228
|
+
#
|
|
229
|
+
# @param subject [Prism::Node, nil] the `case` subject.
|
|
230
|
+
# @param conditions [Array<Prism::Node>] the `when`
|
|
231
|
+
# clause's `conditions` array.
|
|
232
|
+
# @param scope [Rigor::Scope]
|
|
233
|
+
# @return [Array(Rigor::Scope, Rigor::Scope)]
|
|
234
|
+
def case_when_scopes(subject, conditions, scope)
|
|
235
|
+
return [scope, scope] unless subject.is_a?(Prism::LocalVariableReadNode)
|
|
236
|
+
|
|
237
|
+
local_name = subject.name
|
|
238
|
+
current = scope.local(local_name)
|
|
239
|
+
return [scope, scope] if current.nil?
|
|
240
|
+
|
|
241
|
+
accumulate_case_when_scopes(scope, local_name, current, conditions)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Internal analyser. Returns `[truthy_scope, falsey_scope]` when
|
|
245
|
+
# the predicate shape is recognised, or `nil` to signal "no
|
|
246
|
+
# narrowing" so the public surface can fall back to the entry
|
|
247
|
+
# scope.
|
|
248
|
+
def analyse(node, scope)
|
|
249
|
+
case node
|
|
250
|
+
when Prism::ParenthesesNode
|
|
251
|
+
analyse_parentheses(node, scope)
|
|
252
|
+
when Prism::StatementsNode
|
|
253
|
+
analyse_statements(node, scope)
|
|
254
|
+
when Prism::LocalVariableReadNode
|
|
255
|
+
analyse_local_read(node, scope)
|
|
256
|
+
when Prism::CallNode
|
|
257
|
+
analyse_call(node, scope)
|
|
258
|
+
when Prism::AndNode
|
|
259
|
+
analyse_and(node, scope)
|
|
260
|
+
when Prism::OrNode
|
|
261
|
+
analyse_or(node, scope)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# rubocop:disable Metrics/ClassLength
|
|
266
|
+
class << self
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
def falsey_value?(value)
|
|
270
|
+
value.nil? || value == false
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def falsey_nominal?(nominal)
|
|
274
|
+
%w[NilClass FalseClass].include?(nominal.class_name)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Carriers that the {.narrow_falsey} fast path does not handle
|
|
278
|
+
# by structural inspection. Singleton/Tuple/HashShape inhabit
|
|
279
|
+
# truthy values, so their falsey fragment is empty; everything
|
|
280
|
+
# else (Top, Dynamic, Bot, and any future carrier) stays
|
|
281
|
+
# conservative and is returned unchanged.
|
|
282
|
+
def narrow_falsey_other(type)
|
|
283
|
+
case type
|
|
284
|
+
when Type::Singleton, Type::Tuple, Type::HashShape then Type::Combinator.bot
|
|
285
|
+
else type
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Carriers that the {.narrow_nil} fast path does not handle by
|
|
290
|
+
# structural inspection. Top/Dynamic narrow to `Constant[nil]`
|
|
291
|
+
# so dispatch resolves through `NilClass`; Bot is its own nil
|
|
292
|
+
# fragment; the remaining carriers (Singleton, Tuple,
|
|
293
|
+
# HashShape, and any future carrier whose inhabitants exclude
|
|
294
|
+
# nil) collapse to `Bot`.
|
|
295
|
+
def narrow_nil_other(type)
|
|
296
|
+
case type
|
|
297
|
+
when Type::Dynamic, Type::Top then Type::Combinator.constant_of(nil)
|
|
298
|
+
when Type::Bot then type
|
|
299
|
+
else Type::Combinator.bot
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def trusted_equality_literal?(literal)
|
|
304
|
+
literal.is_a?(Type::Constant) &&
|
|
305
|
+
TRUSTED_EQUALITY_LITERAL_CLASSES.include?(literal.value.class)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def singleton_literal?(literal)
|
|
309
|
+
SINGLETON_LITERAL_CLASSES.include?(literal.value.class)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def finite_trusted_literal_domain?(type)
|
|
313
|
+
case type
|
|
314
|
+
when Type::Bot then true
|
|
315
|
+
when Type::Constant then trusted_equality_literal?(type)
|
|
316
|
+
when Type::Union then type.members.all? { |member| finite_trusted_literal_domain?(member) }
|
|
317
|
+
else false
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def narrow_finite_equal(type, literal)
|
|
322
|
+
case type
|
|
323
|
+
when Type::Bot then type
|
|
324
|
+
when Type::Constant then type == literal ? type : Type::Combinator.bot
|
|
325
|
+
when Type::Union
|
|
326
|
+
Type::Combinator.union(*type.members.map { |member| narrow_finite_equal(member, literal) })
|
|
327
|
+
else Type::Combinator.bot
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def narrow_finite_not_equal(type, literal)
|
|
332
|
+
case type
|
|
333
|
+
when Type::Constant then type == literal ? Type::Combinator.bot : type
|
|
334
|
+
when Type::Union
|
|
335
|
+
Type::Combinator.union(*type.members.map { |member| narrow_finite_not_equal(member, literal) })
|
|
336
|
+
else type
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def narrow_singleton_equal(type, literal)
|
|
341
|
+
case type
|
|
342
|
+
when Type::Constant then type == literal ? type : Type::Combinator.bot
|
|
343
|
+
when Type::Nominal then singleton_nominal_matches?(type, literal) ? type : Type::Combinator.bot
|
|
344
|
+
when Type::Union
|
|
345
|
+
Type::Combinator.union(*type.members.map { |member| narrow_singleton_equal(member, literal) })
|
|
346
|
+
else narrow_singleton_equal_other(type)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def narrow_singleton_equal_other(type)
|
|
351
|
+
case type
|
|
352
|
+
when Type::Singleton, Type::Tuple, Type::HashShape then Type::Combinator.bot
|
|
353
|
+
else type
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def narrow_singleton_not_equal(type, literal)
|
|
358
|
+
case type
|
|
359
|
+
when Type::Constant then type == literal ? Type::Combinator.bot : type
|
|
360
|
+
when Type::Nominal then singleton_nominal_matches?(type, literal) ? Type::Combinator.bot : type
|
|
361
|
+
when Type::Union
|
|
362
|
+
Type::Combinator.union(*type.members.map { |member| narrow_singleton_not_equal(member, literal) })
|
|
363
|
+
else type
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def singleton_nominal_matches?(nominal, literal)
|
|
368
|
+
case literal.value
|
|
369
|
+
when nil then nominal.class_name == "NilClass"
|
|
370
|
+
when true then nominal.class_name == "TrueClass"
|
|
371
|
+
when false then nominal.class_name == "FalseClass"
|
|
372
|
+
else false
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def analyse_parentheses(node, scope)
|
|
377
|
+
return nil if node.body.nil?
|
|
378
|
+
|
|
379
|
+
analyse(node.body, scope)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# The truthiness of a `StatementsNode` is determined by its
|
|
383
|
+
# last statement (intermediate statements run for effect and
|
|
384
|
+
# then the predicate's value is the tail's). Earlier
|
|
385
|
+
# statements MAY have scope effects, but Slice 6 phase 1 does
|
|
386
|
+
# NOT thread those through the analyser (the StatementEvaluator
|
|
387
|
+
# has already produced `post_pred` for the call site, and
|
|
388
|
+
# narrowing is layered on that scope).
|
|
389
|
+
def analyse_statements(node, scope)
|
|
390
|
+
return nil if node.body.empty?
|
|
391
|
+
|
|
392
|
+
analyse(node.body.last, scope)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def analyse_local_read(node, scope)
|
|
396
|
+
current = scope.local(node.name)
|
|
397
|
+
return nil if current.nil?
|
|
398
|
+
|
|
399
|
+
[
|
|
400
|
+
scope.with_local(node.name, narrow_truthy(current)),
|
|
401
|
+
scope.with_local(node.name, narrow_falsey(current))
|
|
402
|
+
]
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Recognised CallNode predicates:
|
|
406
|
+
# - `recv.nil?` (Slice 6 phase 1, no args, no block)
|
|
407
|
+
# - unary `!recv` (`name == :!`, no args, no block)
|
|
408
|
+
# - `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
|
|
409
|
+
# with a single static-constant argument and no block
|
|
410
|
+
# (Slice 6 phase 2 sub-phase 1).
|
|
411
|
+
# - `local == literal` / `literal == local` and the `!=` mirror
|
|
412
|
+
# for trusted static literals (Slice 6 phase 2 sub-phase 2).
|
|
413
|
+
# Anything else returns nil so the surrounding analyser falls
|
|
414
|
+
# through to the no-narrowing fallback.
|
|
415
|
+
def analyse_call(node, scope)
|
|
416
|
+
return nil if node.block
|
|
417
|
+
return nil if node.receiver.nil?
|
|
418
|
+
|
|
419
|
+
shape_result = dispatch_call(node, scope, node.name)
|
|
420
|
+
return shape_result if shape_result
|
|
421
|
+
|
|
422
|
+
# Slice 7 phase 15 — RBS::Extended predicate
|
|
423
|
+
# effects. When the method's RBS signature carries
|
|
424
|
+
# `rigor:v1:predicate-if-true` / `predicate-if-false`
|
|
425
|
+
# annotations, apply them to narrow the corresponding
|
|
426
|
+
# local-variable arguments on each edge.
|
|
427
|
+
analyse_rbs_extended_predicate(node, scope)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def dispatch_call(node, scope, name)
|
|
431
|
+
case name
|
|
432
|
+
when :nil?, :! then dispatch_unary_predicate(node, scope, name)
|
|
433
|
+
when :is_a?, :kind_of? then analyse_class_predicate(node, scope, exact: false)
|
|
434
|
+
when :instance_of? then analyse_class_predicate(node, scope, exact: true)
|
|
435
|
+
when :==, :!= then analyse_equality_predicate(node, scope, equality: name)
|
|
436
|
+
when :=== then analyse_case_equality_predicate(node, scope)
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def dispatch_unary_predicate(node, scope, name)
|
|
441
|
+
return nil unless argument_free?(node)
|
|
442
|
+
|
|
443
|
+
case name
|
|
444
|
+
when :nil? then analyse_nil_predicate(node.receiver, scope)
|
|
445
|
+
when :! then analyse(node.receiver, scope)&.reverse
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def argument_free?(node)
|
|
450
|
+
node.arguments.nil? || node.arguments.arguments.empty?
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def analyse_equality_predicate(node, scope, equality:)
|
|
454
|
+
return nil if node.arguments.nil?
|
|
455
|
+
return nil unless node.arguments.arguments.size == 1
|
|
456
|
+
|
|
457
|
+
match = equality_local_literal(node.receiver, node.arguments.arguments.first, scope)
|
|
458
|
+
return nil if match.nil?
|
|
459
|
+
|
|
460
|
+
name, literal = match
|
|
461
|
+
current = scope.local(name)
|
|
462
|
+
return nil if current.nil?
|
|
463
|
+
|
|
464
|
+
positive = equality_scope(scope, name, current, literal, predicate: :==)
|
|
465
|
+
negative = equality_scope(scope, name, current, literal, predicate: :!=)
|
|
466
|
+
equality == :== ? [positive, negative] : [negative, positive]
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def equality_local_literal(left, right, scope)
|
|
470
|
+
if left.is_a?(Prism::LocalVariableReadNode)
|
|
471
|
+
literal = static_literal_type(right, scope)
|
|
472
|
+
return [left.name, literal] if trusted_equality_literal?(literal)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
return nil unless right.is_a?(Prism::LocalVariableReadNode)
|
|
476
|
+
|
|
477
|
+
literal = static_literal_type(left, scope)
|
|
478
|
+
return [right.name, literal] if trusted_equality_literal?(literal)
|
|
479
|
+
|
|
480
|
+
nil
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def static_literal_type(node, scope)
|
|
484
|
+
case node
|
|
485
|
+
when Prism::IntegerNode,
|
|
486
|
+
Prism::StringNode,
|
|
487
|
+
Prism::SymbolNode,
|
|
488
|
+
Prism::TrueNode,
|
|
489
|
+
Prism::FalseNode,
|
|
490
|
+
Prism::NilNode
|
|
491
|
+
scope.type_of(node)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def equality_scope(scope, name, current, literal, predicate:)
|
|
496
|
+
narrowed =
|
|
497
|
+
if predicate == :==
|
|
498
|
+
narrow_equal(current, literal)
|
|
499
|
+
else
|
|
500
|
+
narrow_not_equal(current, literal)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
scope
|
|
504
|
+
.with_local(name, narrowed)
|
|
505
|
+
.with_fact(equality_fact(name, current, narrowed, literal, predicate: predicate))
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def equality_fact(name, original, narrowed, literal, predicate:)
|
|
509
|
+
Analysis::FactStore::Fact.new(
|
|
510
|
+
bucket: equality_fact_bucket(original, narrowed),
|
|
511
|
+
target: Analysis::FactStore::Target.local(name),
|
|
512
|
+
predicate: predicate,
|
|
513
|
+
payload: literal,
|
|
514
|
+
polarity: predicate == :== ? :positive : :negative,
|
|
515
|
+
stability: :local_binding
|
|
516
|
+
)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def equality_fact_bucket(original, narrowed)
|
|
520
|
+
narrowed == original ? :relational : :local_binding
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# `recv.is_a?(C)` / `recv.kind_of?(C)` / `recv.instance_of?(C)`
|
|
524
|
+
# narrowing. The receiver MUST be a `LocalVariableReadNode`
|
|
525
|
+
# (so we have a name to rebind), and the argument MUST be a
|
|
526
|
+
# single static constant reference (`Foo` or `Foo::Bar`) we
|
|
527
|
+
# can resolve to a qualified class name. Anything else falls
|
|
528
|
+
# through to "no narrowing".
|
|
529
|
+
def analyse_class_predicate(node, scope, exact:)
|
|
530
|
+
return nil unless node.receiver.is_a?(Prism::LocalVariableReadNode)
|
|
531
|
+
return nil if node.arguments.nil?
|
|
532
|
+
return nil unless node.arguments.arguments.size == 1
|
|
533
|
+
|
|
534
|
+
class_name = static_class_name(node.arguments.arguments.first)
|
|
535
|
+
return nil if class_name.nil?
|
|
536
|
+
|
|
537
|
+
current = scope.local(node.receiver.name)
|
|
538
|
+
return nil if current.nil?
|
|
539
|
+
|
|
540
|
+
class_predicate_scopes(scope, node.receiver.name, current, class_name, exact: exact)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def class_predicate_scopes(scope, name, current, class_name, exact:)
|
|
544
|
+
[
|
|
545
|
+
scope.with_local(
|
|
546
|
+
name,
|
|
547
|
+
narrow_class(current, class_name, exact: exact, environment: scope.environment)
|
|
548
|
+
),
|
|
549
|
+
scope.with_local(
|
|
550
|
+
name,
|
|
551
|
+
narrow_not_class(current, class_name, exact: exact, environment: scope.environment)
|
|
552
|
+
)
|
|
553
|
+
]
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Slice 7 phase 4 — `===`-narrowing. The case-equality
|
|
557
|
+
# predicate `<receiver> === local` is the operator that
|
|
558
|
+
# backs Ruby's `case`/`when` dispatch. Three receiver
|
|
559
|
+
# shapes produce sound narrowing rules:
|
|
560
|
+
#
|
|
561
|
+
# - **Class / Module receiver**: `Foo === x` is
|
|
562
|
+
# isomorphic to `x.is_a?(Foo)` (the default behaviour
|
|
563
|
+
# of `Module#===`). Reuse `class_predicate_scopes` so
|
|
564
|
+
# the truthy edge narrows down to `Foo` and the falsey
|
|
565
|
+
# edge subtracts `Foo` from a union.
|
|
566
|
+
# - **Range literal receiver** (`(1..10) === x`): the
|
|
567
|
+
# default `Range#===` includes `x` iff the endpoints
|
|
568
|
+
# compare it. We conservatively narrow `x` to
|
|
569
|
+
# `Numeric` for integer-endpoint ranges and to
|
|
570
|
+
# `String` for string-endpoint ranges; other endpoint
|
|
571
|
+
# types fall through.
|
|
572
|
+
# - **Regexp literal receiver** (`/foo/ === x`): the
|
|
573
|
+
# match operator coerces `x` to a String, so the
|
|
574
|
+
# truthy edge narrows `x` to `String`. The falsey
|
|
575
|
+
# edge keeps the entry type unchanged because
|
|
576
|
+
# `Regexp#===` returns false for non-Strings AND for
|
|
577
|
+
# Strings that simply do not match.
|
|
578
|
+
#
|
|
579
|
+
# Anything else — non-local LHS argument, dynamic
|
|
580
|
+
# receiver, custom `===` method — falls through to
|
|
581
|
+
# the no-narrowing branch (nil), preserving the
|
|
582
|
+
# entry scope on both edges.
|
|
583
|
+
def analyse_case_equality_predicate(node, scope)
|
|
584
|
+
return nil if node.arguments.nil?
|
|
585
|
+
return nil unless node.arguments.arguments.size == 1
|
|
586
|
+
|
|
587
|
+
local_arg = node.arguments.arguments.first
|
|
588
|
+
return nil unless local_arg.is_a?(Prism::LocalVariableReadNode)
|
|
589
|
+
|
|
590
|
+
current = scope.local(local_arg.name)
|
|
591
|
+
return nil if current.nil?
|
|
592
|
+
|
|
593
|
+
analyse_case_equality_receiver(node.receiver, scope, local_arg.name, current)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def analyse_case_equality_receiver(receiver, scope, local_name, current)
|
|
597
|
+
if (class_name = static_class_name(receiver))
|
|
598
|
+
return class_predicate_scopes(scope, local_name, current, class_name, exact: false)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
target_class = case_equality_target_class(receiver)
|
|
602
|
+
return nil if target_class.nil?
|
|
603
|
+
|
|
604
|
+
narrowed = narrow_class(current, target_class, exact: false, environment: scope.environment)
|
|
605
|
+
[
|
|
606
|
+
scope.with_local(local_name, narrowed),
|
|
607
|
+
scope.with_local(local_name, current)
|
|
608
|
+
]
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Maps a case-equality literal receiver to the class
|
|
612
|
+
# whose membership is implied by the truthy edge.
|
|
613
|
+
# `Prism::ParenthesesNode` wrappers are transparently
|
|
614
|
+
# unwrapped (`(1..10) === x` is parsed with the range
|
|
615
|
+
# inside parentheses). Range literals: integer
|
|
616
|
+
# endpoints → `Numeric`; string endpoints → `String`.
|
|
617
|
+
# Regexp literals → `String`. Other shapes return nil
|
|
618
|
+
# so the caller falls through.
|
|
619
|
+
def case_equality_target_class(receiver)
|
|
620
|
+
receiver = unwrap_parens(receiver)
|
|
621
|
+
case receiver
|
|
622
|
+
when Prism::RangeNode then range_target_class(receiver)
|
|
623
|
+
when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode then "String"
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def unwrap_parens(node)
|
|
628
|
+
while node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode) &&
|
|
629
|
+
node.body.body.size == 1
|
|
630
|
+
node = node.body.body.first
|
|
631
|
+
end
|
|
632
|
+
node
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Slice 7 phase 15 — RBS::Extended predicate-effect
|
|
636
|
+
# analyser. Resolves the called method through the
|
|
637
|
+
# RBS environment, reads any `rigor:v1:predicate-if-*`
|
|
638
|
+
# annotations, and applies them to the call's
|
|
639
|
+
# local-variable arguments.
|
|
640
|
+
#
|
|
641
|
+
# Conservative envelope:
|
|
642
|
+
# - Receiver type must be `Type::Nominal`,
|
|
643
|
+
# `Type::Singleton`, or `Type::Constant`.
|
|
644
|
+
# - The method must be present in the loader.
|
|
645
|
+
# - For each predicate effect, the corresponding
|
|
646
|
+
# positional argument (matched by parameter name in
|
|
647
|
+
# the selected overload) MUST be a
|
|
648
|
+
# `Prism::LocalVariableReadNode` for narrowing to
|
|
649
|
+
# apply.
|
|
650
|
+
# - When the target is `self`, narrowing applies to
|
|
651
|
+
# the receiver — but the engine does not yet narrow
|
|
652
|
+
# `self` itself (Slice A-engine self-typing is
|
|
653
|
+
# read-only), so `self`-targeted effects are
|
|
654
|
+
# accepted by the parser but currently produce no
|
|
655
|
+
# scope edits.
|
|
656
|
+
def analyse_rbs_extended_predicate(node, scope)
|
|
657
|
+
method_def = resolve_rbs_extended_method(node, scope)
|
|
658
|
+
return nil if method_def.nil?
|
|
659
|
+
|
|
660
|
+
effects = RbsExtended.read_predicate_effects(method_def)
|
|
661
|
+
return nil if effects.empty?
|
|
662
|
+
|
|
663
|
+
truthy_scope = scope
|
|
664
|
+
falsey_scope = scope
|
|
665
|
+
effects.each do |effect|
|
|
666
|
+
truthy_scope, falsey_scope =
|
|
667
|
+
apply_predicate_effect(effect, node, scope, truthy_scope, falsey_scope, method_def)
|
|
668
|
+
end
|
|
669
|
+
[truthy_scope, falsey_scope]
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def resolve_rbs_extended_method(node, scope)
|
|
673
|
+
loader = scope.environment.rbs_loader
|
|
674
|
+
return nil if loader.nil?
|
|
675
|
+
|
|
676
|
+
receiver_type = scope.type_of(node.receiver)
|
|
677
|
+
class_name = rbs_extended_class_name(receiver_type)
|
|
678
|
+
return nil if class_name.nil?
|
|
679
|
+
return nil unless loader.class_known?(class_name)
|
|
680
|
+
|
|
681
|
+
if receiver_type.is_a?(Type::Singleton)
|
|
682
|
+
loader.singleton_method(class_name: class_name, method_name: node.name)
|
|
683
|
+
else
|
|
684
|
+
loader.instance_method(class_name: class_name, method_name: node.name)
|
|
685
|
+
end
|
|
686
|
+
rescue StandardError
|
|
687
|
+
nil
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def rbs_extended_class_name(receiver_type)
|
|
691
|
+
case receiver_type
|
|
692
|
+
when Type::Nominal, Type::Singleton then receiver_type.class_name
|
|
693
|
+
when Type::Constant then rbs_extended_constant_class(receiver_type.value)
|
|
694
|
+
end
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
CONSTANT_CLASSES = {
|
|
698
|
+
Integer => "Integer", Float => "Float", String => "String",
|
|
699
|
+
Symbol => "Symbol",
|
|
700
|
+
TrueClass => "TrueClass", FalseClass => "FalseClass",
|
|
701
|
+
NilClass => "NilClass"
|
|
702
|
+
}.freeze
|
|
703
|
+
private_constant :CONSTANT_CLASSES
|
|
704
|
+
|
|
705
|
+
def rbs_extended_constant_class(value)
|
|
706
|
+
CONSTANT_CLASSES.each { |klass, name| return name if value.is_a?(klass) }
|
|
707
|
+
nil
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# rubocop:disable Metrics/ParameterLists
|
|
711
|
+
def apply_predicate_effect(effect, call_node, entry_scope, truthy_scope, falsey_scope, method_def)
|
|
712
|
+
arg_node = lookup_positional_arg(call_node, method_def, effect.target_name)
|
|
713
|
+
return [truthy_scope, falsey_scope] if effect.target_kind != :parameter
|
|
714
|
+
return [truthy_scope, falsey_scope] unless arg_node.is_a?(Prism::LocalVariableReadNode)
|
|
715
|
+
|
|
716
|
+
local_name = arg_node.name
|
|
717
|
+
current = entry_scope.local(local_name)
|
|
718
|
+
return [truthy_scope, falsey_scope] if current.nil?
|
|
719
|
+
|
|
720
|
+
narrowed = narrow_class(current, effect.class_name, exact: false, environment: entry_scope.environment)
|
|
721
|
+
if effect.truthy_only?
|
|
722
|
+
[truthy_scope.with_local(local_name, narrowed), falsey_scope]
|
|
723
|
+
else
|
|
724
|
+
[truthy_scope, falsey_scope.with_local(local_name, narrowed)]
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
# rubocop:enable Metrics/ParameterLists
|
|
728
|
+
|
|
729
|
+
# Maps the effect's target parameter name to the call
|
|
730
|
+
# site argument by inspecting the selected overload's
|
|
731
|
+
# required-positional parameter list. Returns the Prism
|
|
732
|
+
# arg node at that position, or nil when the overload
|
|
733
|
+
# shape does not allow a precise match.
|
|
734
|
+
def lookup_positional_arg(call_node, method_def, target_name)
|
|
735
|
+
arguments = call_node.arguments&.arguments || []
|
|
736
|
+
method_def.method_types.each do |mt|
|
|
737
|
+
params = mt.type.required_positionals + mt.type.optional_positionals
|
|
738
|
+
index = params.find_index { |param| param.name == target_name }
|
|
739
|
+
return arguments[index] if index && arguments[index]
|
|
740
|
+
end
|
|
741
|
+
nil
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Slice 7 phase 5 — case/when accumulator. Walks each
|
|
745
|
+
# `when` condition, computes the narrowed type for the
|
|
746
|
+
# subject as if `condition === subject`, and accumulates
|
|
747
|
+
# them. The body's narrowed type is the union across
|
|
748
|
+
# all conditions; the falsey type is the running result
|
|
749
|
+
# after subtracting every condition's class. Conditions
|
|
750
|
+
# whose shape we cannot statically classify are treated
|
|
751
|
+
# as "no narrowing": the body falls back to the union of
|
|
752
|
+
# what we did learn (or the entry type when nothing
|
|
753
|
+
# learned), and the falsey edge is the entry type
|
|
754
|
+
# (because we cannot prove the unknown condition didn't
|
|
755
|
+
# match).
|
|
756
|
+
def accumulate_case_when_scopes(scope, local_name, current, conditions)
|
|
757
|
+
truthy_members = []
|
|
758
|
+
falsey_type = current
|
|
759
|
+
fully_narrowable = true
|
|
760
|
+
|
|
761
|
+
conditions.each do |condition|
|
|
762
|
+
target = static_class_name(condition) || case_equality_target_class(condition)
|
|
763
|
+
if target
|
|
764
|
+
truthy_members << narrow_class(current, target, exact: false, environment: scope.environment)
|
|
765
|
+
falsey_type = narrow_not_class(falsey_type, target, exact: false, environment: scope.environment)
|
|
766
|
+
else
|
|
767
|
+
fully_narrowable = false
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
truthy = truthy_members.empty? ? current : Type::Combinator.union(*truthy_members)
|
|
772
|
+
[
|
|
773
|
+
scope.with_local(local_name, truthy),
|
|
774
|
+
scope.with_local(local_name, fully_narrowable ? falsey_type : current)
|
|
775
|
+
]
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
def range_target_class(range_node)
|
|
779
|
+
left = range_node.left
|
|
780
|
+
right = range_node.right
|
|
781
|
+
return "Numeric" if integer_endpoint?(left) || integer_endpoint?(right)
|
|
782
|
+
|
|
783
|
+
"String" if string_endpoint?(left) || string_endpoint?(right)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def integer_endpoint?(node)
|
|
787
|
+
node.is_a?(Prism::IntegerNode) || node.is_a?(Prism::FloatNode)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def string_endpoint?(node)
|
|
791
|
+
node.is_a?(Prism::StringNode)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Walks a constant-reference subtree (`Prism::ConstantReadNode`,
|
|
795
|
+
# `Prism::ConstantPathNode`) and renders its qualified name.
|
|
796
|
+
# Returns nil for any non-constant argument shape so the
|
|
797
|
+
# caller can fall through.
|
|
798
|
+
def static_class_name(node)
|
|
799
|
+
case node
|
|
800
|
+
when Prism::ConstantReadNode
|
|
801
|
+
node.name.to_s
|
|
802
|
+
when Prism::ConstantPathNode
|
|
803
|
+
parent = node.parent
|
|
804
|
+
return node.name.to_s if parent.nil?
|
|
805
|
+
|
|
806
|
+
parent_name = static_class_name(parent)
|
|
807
|
+
return nil if parent_name.nil?
|
|
808
|
+
|
|
809
|
+
"#{parent_name}::#{node.name}"
|
|
810
|
+
end
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# ----- narrow_class / narrow_not_class helpers -----
|
|
814
|
+
|
|
815
|
+
# Polarity-aware dispatch table for {.narrow_class} /
|
|
816
|
+
# {.narrow_not_class}. Avoids duplicating the per-carrier
|
|
817
|
+
# case statement and keeps each public surface a thin
|
|
818
|
+
# delegate; the per-carrier helpers know which polarity to
|
|
819
|
+
# apply by looking at `polarity:`.
|
|
820
|
+
def narrow_class_dispatch(type, class_name, context)
|
|
821
|
+
case type
|
|
822
|
+
when Type::Constant then narrow_constant_class(type, class_name, context)
|
|
823
|
+
when Type::Nominal then narrow_nominal_class(type, class_name, context)
|
|
824
|
+
when Type::Union then narrow_union_class(type, class_name, context)
|
|
825
|
+
when Type::Tuple then narrow_shape_class(type, "Array", class_name, context)
|
|
826
|
+
when Type::HashShape then narrow_shape_class(type, "Hash", class_name, context)
|
|
827
|
+
when Type::Singleton then narrow_singleton_class(type, class_name, context)
|
|
828
|
+
else narrow_other_class(type, class_name, context)
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
def narrow_constant_class(constant, class_name, context)
|
|
833
|
+
if context.polarity == :positive
|
|
834
|
+
narrow_constant_to_class(constant, class_name, context)
|
|
835
|
+
else
|
|
836
|
+
narrow_constant_not_class(constant, class_name, context)
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def narrow_nominal_class(nominal, class_name, context)
|
|
841
|
+
if context.polarity == :positive
|
|
842
|
+
narrow_nominal_to_class(nominal, class_name, context)
|
|
843
|
+
else
|
|
844
|
+
narrow_nominal_not_class(nominal, class_name, context)
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def narrow_union_class(union, class_name, context)
|
|
849
|
+
Type::Combinator.union(
|
|
850
|
+
*union.members.map { |member| narrow_class_dispatch(member, class_name, context) }
|
|
851
|
+
)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def narrow_shape_class(shape, projected_class, class_name, context)
|
|
855
|
+
if context.polarity == :positive
|
|
856
|
+
narrow_shape_to_class(shape, projected_class, class_name, context)
|
|
857
|
+
else
|
|
858
|
+
narrow_shape_not_class(shape, projected_class, class_name, context)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def narrow_singleton_class(singleton, class_name, context)
|
|
863
|
+
if context.polarity == :positive
|
|
864
|
+
narrow_singleton_to_class(singleton, class_name, context)
|
|
865
|
+
else
|
|
866
|
+
narrow_singleton_not_class(singleton, class_name, context)
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def narrow_other_class(type, class_name, context)
|
|
871
|
+
context.polarity == :positive ? narrow_class_other(type, class_name) : type
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def narrow_constant_to_class(constant, class_name, context)
|
|
875
|
+
rigor_class = constant.value.class.name
|
|
876
|
+
subclass_of?(rigor_class, class_name, context) ? constant : Type::Combinator.bot
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def narrow_constant_not_class(constant, class_name, context)
|
|
880
|
+
rigor_class = constant.value.class.name
|
|
881
|
+
subclass_of?(rigor_class, class_name, context) ? Type::Combinator.bot : constant
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
# Narrow a Nominal under `is_a?(class_name)`: when the
|
|
885
|
+
# nominal's class is already a subclass of `class_name`
|
|
886
|
+
# (or matches under `exact: true`) preserve it; when
|
|
887
|
+
# `class_name` is a subclass of the nominal's class
|
|
888
|
+
# (`Nominal[Numeric]` under `is_a?(Integer)`) narrow DOWN
|
|
889
|
+
# to `Nominal[class_name]`; otherwise (disjoint hierarchies
|
|
890
|
+
# under `is_a?`, mismatch under `instance_of?`) collapse to
|
|
891
|
+
# `Bot`. Conservative when the analyzer environment cannot
|
|
892
|
+
# resolve either class.
|
|
893
|
+
def narrow_nominal_to_class(nominal, class_name, context)
|
|
894
|
+
return nominal if nominal.class_name == class_name
|
|
895
|
+
return Type::Combinator.bot if context.exact
|
|
896
|
+
|
|
897
|
+
case class_ordering(nominal.class_name, class_name, context)
|
|
898
|
+
when :superclass then Type::Combinator.nominal_of(class_name)
|
|
899
|
+
when :disjoint then Type::Combinator.bot
|
|
900
|
+
else nominal # :subclass preserves the bound; :unknown stays conservative
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def narrow_nominal_not_class(nominal, class_name, context)
|
|
905
|
+
return Type::Combinator.bot if nominal.class_name == class_name
|
|
906
|
+
return nominal if context.exact
|
|
907
|
+
|
|
908
|
+
ordering = class_ordering(nominal.class_name, class_name, context)
|
|
909
|
+
case ordering
|
|
910
|
+
when :subclass then Type::Combinator.bot
|
|
911
|
+
else nominal
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
|
|
915
|
+
def narrow_shape_to_class(shape, projected_class, class_name, context)
|
|
916
|
+
subclass_of?(projected_class, class_name, context) ? shape : Type::Combinator.bot
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def narrow_shape_not_class(shape, projected_class, class_name, context)
|
|
920
|
+
subclass_of?(projected_class, class_name, context) ? Type::Combinator.bot : shape
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# `Singleton[Foo]` is the *class object* `Foo`, an instance of
|
|
924
|
+
# `Class` (which is a subclass of `Module`). Asking
|
|
925
|
+
# `Foo.is_a?(Class)` returns true; `Foo.is_a?(Foo)` returns
|
|
926
|
+
# false unless `Foo` is `Class` itself. We approximate this
|
|
927
|
+
# by treating singletons uniformly as `Class` instances.
|
|
928
|
+
def narrow_singleton_to_class(singleton, class_name, context)
|
|
929
|
+
subclass_of?("Class", class_name, context) ? singleton : Type::Combinator.bot
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
def narrow_singleton_not_class(singleton, class_name, context)
|
|
933
|
+
subclass_of?("Class", class_name, context) ? Type::Combinator.bot : singleton
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# Top/Dynamic narrow to `Nominal[class_name]` so dispatch
|
|
937
|
+
# can resolve through the asked class; Bot stays Bot.
|
|
938
|
+
def narrow_class_other(type, class_name)
|
|
939
|
+
case type
|
|
940
|
+
when Type::Dynamic, Type::Top then Type::Combinator.nominal_of(class_name)
|
|
941
|
+
else type
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# Returns `true` when an instance of `rigor_class_name`
|
|
946
|
+
# satisfies `is_a?(target_class_name)` (or
|
|
947
|
+
# `instance_of?(target_class_name)` when `exact: true`).
|
|
948
|
+
# Falls back to the safe `false` when either name does not
|
|
949
|
+
# resolve through the analyzer environment.
|
|
950
|
+
def subclass_of?(rigor_class_name, target_class_name, context)
|
|
951
|
+
return rigor_class_name == target_class_name if context.exact
|
|
952
|
+
|
|
953
|
+
%i[subclass equal].include?(
|
|
954
|
+
class_ordering(rigor_class_name, target_class_name, context)
|
|
955
|
+
)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Compares two class names through the analyzer environment.
|
|
959
|
+
# Returns `:equal` when they resolve to the same class,
|
|
960
|
+
# `:subclass` when `lhs <= rhs`, `:superclass` when
|
|
961
|
+
# `rhs <= lhs`, `:disjoint` when neither, and `:unknown` when
|
|
962
|
+
# either name does not resolve.
|
|
963
|
+
def class_ordering(lhs, rhs, context)
|
|
964
|
+
return :equal if lhs == rhs
|
|
965
|
+
|
|
966
|
+
context.environment.class_ordering(lhs, rhs)
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def analyse_nil_predicate(receiver, scope)
|
|
970
|
+
return nil unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
971
|
+
|
|
972
|
+
current = scope.local(receiver.name)
|
|
973
|
+
return nil if current.nil?
|
|
974
|
+
|
|
975
|
+
[
|
|
976
|
+
scope.with_local(receiver.name, narrow_nil(current)),
|
|
977
|
+
scope.with_local(receiver.name, narrow_non_nil(current))
|
|
978
|
+
]
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
# `a && b` short-circuits: the truthy edge is the truthy edge
|
|
982
|
+
# of `b` evaluated under `a`'s truthy scope; the falsey edge
|
|
983
|
+
# is the union of `a`'s falsey scope (b skipped) and `b`'s
|
|
984
|
+
# falsey scope (b ran but returned falsey). When a sub-edge
|
|
985
|
+
# cannot be narrowed we fall back to the entry scope so the
|
|
986
|
+
# caller still sees consistent keys across the two output
|
|
987
|
+
# scopes.
|
|
988
|
+
def analyse_and(node, scope)
|
|
989
|
+
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
990
|
+
truthy_b, falsey_b = analyse(node.right, truthy_a) || [truthy_a, truthy_a]
|
|
991
|
+
[truthy_b, falsey_a.join(falsey_b)]
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# `a || b` short-circuits: the truthy edge is the union of
|
|
995
|
+
# `a`'s truthy scope (b skipped) and `b`'s truthy scope (b
|
|
996
|
+
# ran and was truthy); the falsey edge is `b`'s falsey scope
|
|
997
|
+
# evaluated under `a`'s falsey scope.
|
|
998
|
+
def analyse_or(node, scope)
|
|
999
|
+
truthy_a, falsey_a = analyse(node.left, scope) || [scope, scope]
|
|
1000
|
+
truthy_b, falsey_b = analyse(node.right, falsey_a) || [falsey_a, falsey_a]
|
|
1001
|
+
[truthy_a.join(truthy_b), falsey_b]
|
|
1002
|
+
end
|
|
1003
|
+
end
|
|
1004
|
+
# rubocop:enable Metrics/ClassLength
|
|
1005
|
+
end
|
|
1006
|
+
# rubocop:enable Metrics/ModuleLength
|
|
1007
|
+
end
|
|
1008
|
+
end
|