rigortype 0.0.6 → 0.0.7
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/lib/rigor/analysis/check_rules.rb +38 -41
- data/lib/rigor/builtins/imported_refinements.rb +93 -3
- data/lib/rigor/inference/expression_typer.rb +25 -2
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +247 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +45 -4
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +409 -0
- data/lib/rigor/inference/method_dispatcher.rb +70 -10
- data/lib/rigor/inference/method_parameter_binder.rb +3 -5
- data/lib/rigor/inference/narrowing.rb +38 -6
- data/lib/rigor/inference/statement_evaluator.rb +5 -7
- data/lib/rigor/reflection.rb +203 -0
- data/lib/rigor/type/combinator.rb +244 -1
- data/lib/rigor/type/constant.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/sig/rigor/reflection.rbs +17 -0
- data/sig/rigor/type.rbs +5 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0aa63fadd57282a307bf4664cce576c1b955617b6643c64d436827595a751ccb
|
|
4
|
+
data.tar.gz: 4e7cc6e58c5fcb45eb201182cbb87f723a558972334add650aecdeb80ed7c502
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9972f4ac5258340ab534ce5bd3aad4dfda7f23e080013f0f3fdc96d4ea0fb45c458cfa3c68e14b5e2b7ffdf273e7fa81f76d7d80761cd0a5b3b6291f903a5157
|
|
7
|
+
data.tar.gz: 3009bf4ab97ee163ec05fc262bbdc130dc5e5ebf877bea9b1e220f0607251d96bf81ad10e1f6505d2f2999e8f12bb6bb823577dd142645295e2fdc2fc7d29fe2
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
|
+
require_relative "../reflection"
|
|
5
6
|
require_relative "../source/node_walker"
|
|
6
7
|
require_relative "../type"
|
|
7
8
|
require_relative "diagnostic"
|
|
@@ -172,9 +173,7 @@ module Rigor
|
|
|
172
173
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
173
174
|
return nil if scope.discovered_method?(class_name, call_node.name, kind)
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
return nil if loader.nil?
|
|
177
|
-
return nil unless loader.class_known?(class_name)
|
|
176
|
+
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
178
177
|
|
|
179
178
|
# When the loader cannot build a class definition for a
|
|
180
179
|
# name it nominally knows (constant-decl aliases such
|
|
@@ -182,9 +181,9 @@ module Rigor
|
|
|
182
181
|
# malformed signatures), we cannot enumerate methods
|
|
183
182
|
# so we MUST NOT emit a false positive. Skip the rule
|
|
184
183
|
# in that case.
|
|
185
|
-
return nil unless definition_available?(
|
|
184
|
+
return nil unless definition_available?(receiver_type, class_name, scope)
|
|
186
185
|
|
|
187
|
-
method_def = lookup_method(
|
|
186
|
+
method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
|
|
188
187
|
return nil if method_def
|
|
189
188
|
|
|
190
189
|
build_undefined_method_diagnostic(path, call_node, receiver_type)
|
|
@@ -219,27 +218,29 @@ module Rigor
|
|
|
219
218
|
nil
|
|
220
219
|
end
|
|
221
220
|
|
|
222
|
-
def definition_available?(
|
|
221
|
+
def definition_available?(receiver_type, class_name, scope)
|
|
223
222
|
if receiver_type.is_a?(Type::Singleton)
|
|
224
|
-
!
|
|
223
|
+
!Rigor::Reflection.singleton_definition(class_name, scope: scope).nil?
|
|
225
224
|
else
|
|
226
|
-
!
|
|
225
|
+
!Rigor::Reflection.instance_definition(class_name, scope: scope).nil?
|
|
227
226
|
end
|
|
228
|
-
rescue StandardError
|
|
229
|
-
false
|
|
230
227
|
end
|
|
231
228
|
|
|
232
|
-
def lookup_method(
|
|
229
|
+
def lookup_method(receiver_type, class_name, method_name, scope)
|
|
233
230
|
if receiver_type.is_a?(Type::Singleton)
|
|
234
|
-
|
|
231
|
+
Rigor::Reflection.singleton_method_definition(class_name, method_name, scope: scope)
|
|
235
232
|
else
|
|
236
|
-
|
|
233
|
+
Rigor::Reflection.instance_method_definition(class_name, method_name, scope: scope)
|
|
237
234
|
end
|
|
238
235
|
rescue StandardError
|
|
239
|
-
# The
|
|
240
|
-
#
|
|
241
|
-
# NOT emit a false positive
|
|
242
|
-
# receiver class is
|
|
236
|
+
# The Reflection facade catches loader exceptions and
|
|
237
|
+
# returns nil. The wrapper here treats failures as
|
|
238
|
+
# "method exists" so we do NOT emit a false positive
|
|
239
|
+
# when our knowledge of the receiver class is
|
|
240
|
+
# structurally incomplete (Reflection's own rescue
|
|
241
|
+
# already returns nil; this catch is a defensive
|
|
242
|
+
# double-net for any future call shape that might
|
|
243
|
+
# raise).
|
|
243
244
|
true
|
|
244
245
|
end
|
|
245
246
|
|
|
@@ -271,12 +272,10 @@ module Rigor
|
|
|
271
272
|
kind = receiver_type.is_a?(Type::Singleton) ? :singleton : :instance
|
|
272
273
|
return nil if scope.discovered_method?(class_name, call_node.name, kind)
|
|
273
274
|
|
|
274
|
-
|
|
275
|
-
return nil
|
|
276
|
-
return nil unless loader.class_known?(class_name)
|
|
277
|
-
return nil unless definition_available?(loader, receiver_type, class_name)
|
|
275
|
+
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
276
|
+
return nil unless definition_available?(receiver_type, class_name, scope)
|
|
278
277
|
|
|
279
|
-
method_def = lookup_method(
|
|
278
|
+
method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
|
|
280
279
|
return nil if method_def.nil? || method_def == true
|
|
281
280
|
|
|
282
281
|
arity_envelope = compute_arity_envelope(method_def)
|
|
@@ -383,12 +382,14 @@ module Rigor
|
|
|
383
382
|
receiver_type = scope.type_of(call_node.receiver)
|
|
384
383
|
return nil unless receiver_type.is_a?(Type::Union)
|
|
385
384
|
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
# The rule only fires when the analyzer has access to
|
|
386
|
+
# an RBS loader; without it, the per-member method-
|
|
387
|
+
# presence checks below cannot rule out a sound call.
|
|
388
|
+
return nil unless Rigor::Reflection.rbs_class_known?("NilClass", scope: scope)
|
|
388
389
|
|
|
389
390
|
return nil unless union_contains_nil?(receiver_type)
|
|
390
|
-
return nil unless union_method_present_on_non_nil?(receiver_type, call_node.name,
|
|
391
|
-
return nil if nil_class_has_method?(call_node.name,
|
|
391
|
+
return nil unless union_method_present_on_non_nil?(receiver_type, call_node.name, scope)
|
|
392
|
+
return nil if nil_class_has_method?(call_node.name, scope)
|
|
392
393
|
|
|
393
394
|
build_nil_receiver_diagnostic(path, call_node)
|
|
394
395
|
end
|
|
@@ -409,27 +410,25 @@ module Rigor
|
|
|
409
410
|
# that are unsound on the non-nil branch — that is the
|
|
410
411
|
# `undefined_method_diagnostic` rule's job, and we want
|
|
411
412
|
# exactly one diagnostic per offending call site.
|
|
412
|
-
def union_method_present_on_non_nil?(union, method_name,
|
|
413
|
+
def union_method_present_on_non_nil?(union, method_name, scope)
|
|
413
414
|
non_nil_members = union.members.reject { |m| nil_member?(m) }
|
|
414
415
|
return false if non_nil_members.empty?
|
|
415
416
|
|
|
416
|
-
non_nil_members.all? { |m| method_present_anywhere?(m, method_name,
|
|
417
|
+
non_nil_members.all? { |m| method_present_anywhere?(m, method_name, scope) }
|
|
417
418
|
end
|
|
418
419
|
|
|
419
|
-
def method_present_anywhere?(member, method_name,
|
|
420
|
+
def method_present_anywhere?(member, method_name, scope)
|
|
420
421
|
class_name = concrete_class_name(member)
|
|
421
422
|
return true if class_name.nil? # Dynamic / Top / Bot — be permissive.
|
|
422
423
|
return true if scope.discovered_method?(class_name, method_name, :instance)
|
|
423
|
-
return true unless
|
|
424
|
-
return true unless definition_available?(
|
|
424
|
+
return true unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
425
|
+
return true unless definition_available?(member, class_name, scope)
|
|
425
426
|
|
|
426
|
-
!lookup_method(
|
|
427
|
+
!lookup_method(member, class_name, method_name, scope).nil?
|
|
427
428
|
end
|
|
428
429
|
|
|
429
|
-
def nil_class_has_method?(method_name,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
definition = loader.instance_definition("NilClass")
|
|
430
|
+
def nil_class_has_method?(method_name, scope)
|
|
431
|
+
definition = Rigor::Reflection.instance_definition("NilClass", scope: scope)
|
|
433
432
|
return false if definition.nil?
|
|
434
433
|
|
|
435
434
|
!definition.methods[method_name.to_sym].nil?
|
|
@@ -684,12 +683,10 @@ module Rigor
|
|
|
684
683
|
# supplies BOTH a `def` and an RBS sig, the sig is
|
|
685
684
|
# the authoritative parameter contract and we
|
|
686
685
|
# should validate calls against it.
|
|
687
|
-
|
|
688
|
-
return nil
|
|
689
|
-
return nil unless loader.class_known?(class_name)
|
|
690
|
-
return nil unless definition_available?(loader, receiver_type, class_name)
|
|
686
|
+
return nil unless Rigor::Reflection.rbs_class_known?(class_name, scope: scope)
|
|
687
|
+
return nil unless definition_available?(receiver_type, class_name, scope)
|
|
691
688
|
|
|
692
|
-
method_def = lookup_method(
|
|
689
|
+
method_def = lookup_method(receiver_type, class_name, call_node.name, scope)
|
|
693
690
|
return nil if method_def.nil? || method_def == true
|
|
694
691
|
return nil unless method_def.method_types.size == 1
|
|
695
692
|
|
|
@@ -78,6 +78,41 @@ module Rigor
|
|
|
78
78
|
return nil unless args.size == 2
|
|
79
79
|
|
|
80
80
|
Type::Combinator.non_empty_hash(args[0], args[1])
|
|
81
|
+
},
|
|
82
|
+
# v0.0.7 — `key_of[T]` and `value_of[T]` type functions.
|
|
83
|
+
# Each takes a single type argument and projects the
|
|
84
|
+
# known-keys (resp. known-values) union out of `T`. See
|
|
85
|
+
# `Type::Combinator.key_of` for the per-shape projection
|
|
86
|
+
# rules. Use `lower_snake` per the
|
|
87
|
+
# imported-built-in-types.md type-function naming rule.
|
|
88
|
+
"key_of" => lambda { |args|
|
|
89
|
+
return nil unless args.size == 1
|
|
90
|
+
|
|
91
|
+
Type::Combinator.key_of(args.first)
|
|
92
|
+
},
|
|
93
|
+
"value_of" => lambda { |args|
|
|
94
|
+
return nil unless args.size == 1
|
|
95
|
+
|
|
96
|
+
Type::Combinator.value_of(args.first)
|
|
97
|
+
},
|
|
98
|
+
# `int_mask[1, 2, 4]` — every integer representable by
|
|
99
|
+
# a bitwise OR over the listed flags. Each arg must be a
|
|
100
|
+
# `Constant<Integer>`; the parser wraps integer literals
|
|
101
|
+
# for this purpose. Builder declines on any non-integer
|
|
102
|
+
# arg.
|
|
103
|
+
"int_mask" => lambda { |args|
|
|
104
|
+
flags = args.map { |arg| arg.is_a?(Type::Constant) && arg.value.is_a?(Integer) ? arg.value : nil }
|
|
105
|
+
return nil if flags.any?(&:nil?)
|
|
106
|
+
|
|
107
|
+
Type::Combinator.int_mask(flags)
|
|
108
|
+
},
|
|
109
|
+
# `int_mask_of[T]` — derives the closure from a finite
|
|
110
|
+
# integer literal type (single Constant<Integer> or a
|
|
111
|
+
# Union of them).
|
|
112
|
+
"int_mask_of" => lambda { |args|
|
|
113
|
+
return nil unless args.size == 1
|
|
114
|
+
|
|
115
|
+
Type::Combinator.int_mask_of(args.first)
|
|
81
116
|
}
|
|
82
117
|
}.freeze
|
|
83
118
|
private_constant :PARAMETERISED_TYPE_BUILDERS
|
|
@@ -145,7 +180,7 @@ module Rigor
|
|
|
145
180
|
# soft (returns `nil` from `parse`) on any deviation so the
|
|
146
181
|
# `RBS::Extended` directive site can fall back to the
|
|
147
182
|
# RBS-declared type rather than crash on a typo.
|
|
148
|
-
class Parser
|
|
183
|
+
class Parser # rubocop:disable Metrics/ClassLength
|
|
149
184
|
def initialize(input)
|
|
150
185
|
@scanner = StringScanner.new(input.strip)
|
|
151
186
|
end
|
|
@@ -153,6 +188,12 @@ module Rigor
|
|
|
153
188
|
def parse
|
|
154
189
|
type = parse_type
|
|
155
190
|
return nil if type.nil?
|
|
191
|
+
|
|
192
|
+
# v0.0.7 — trailing `[K]` indexed-access projects
|
|
193
|
+
# into the parsed type. Multiple `[K]` segments
|
|
194
|
+
# chain (`Tuple[A, B, C][1][0]`).
|
|
195
|
+
type = parse_indexed_access_chain(type)
|
|
196
|
+
return nil if type.nil?
|
|
156
197
|
return nil unless @scanner.eos?
|
|
157
198
|
|
|
158
199
|
type
|
|
@@ -160,12 +201,21 @@ module Rigor
|
|
|
160
201
|
|
|
161
202
|
private
|
|
162
203
|
|
|
163
|
-
|
|
204
|
+
# Refinement names use kebab-case (`non-empty-string`),
|
|
205
|
+
# type-function names use lower_snake (`key_of`,
|
|
206
|
+
# `value_of`, `int_mask`). The regex accepts both shapes;
|
|
207
|
+
# the registry lookup decides which family the name
|
|
208
|
+
# belongs to.
|
|
209
|
+
SIMPLE_NAME = /[a-z][a-z0-9_-]*/
|
|
164
210
|
CLASS_NAME = /[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*/
|
|
165
211
|
SIGNED_INT = /-?\d+/
|
|
166
212
|
private_constant :SIMPLE_NAME, :CLASS_NAME, :SIGNED_INT
|
|
167
213
|
|
|
168
214
|
def parse_type
|
|
215
|
+
if (class_name = @scanner.scan(CLASS_NAME))
|
|
216
|
+
return parse_class_arg_tail(class_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
169
219
|
name = @scanner.scan(SIMPLE_NAME)
|
|
170
220
|
return nil if name.nil?
|
|
171
221
|
|
|
@@ -176,6 +226,25 @@ module Rigor
|
|
|
176
226
|
end
|
|
177
227
|
end
|
|
178
228
|
|
|
229
|
+
# `T[K]` — keep applying `[K]` indexes until no more
|
|
230
|
+
# opening brackets are present. Each index consumes one
|
|
231
|
+
# type argument; multi-arg `[K1, K2]` fails (the spec
|
|
232
|
+
# specifies a single key).
|
|
233
|
+
def parse_indexed_access_chain(type)
|
|
234
|
+
loop do
|
|
235
|
+
skip_ws
|
|
236
|
+
break unless @scanner.peek(1) == "["
|
|
237
|
+
|
|
238
|
+
@scanner.getch
|
|
239
|
+
args = parse_type_arg_list
|
|
240
|
+
return nil if args.nil? || args.size != 1
|
|
241
|
+
return nil unless @scanner.getch == "]"
|
|
242
|
+
|
|
243
|
+
type = Type::Combinator.indexed_access(type, args.first)
|
|
244
|
+
end
|
|
245
|
+
type
|
|
246
|
+
end
|
|
247
|
+
|
|
179
248
|
def parse_parametric_type_args(name)
|
|
180
249
|
builder = PARAMETERISED_TYPE_BUILDERS[name]
|
|
181
250
|
return nil if builder.nil?
|
|
@@ -227,12 +296,33 @@ module Rigor
|
|
|
227
296
|
def parse_type_arg
|
|
228
297
|
skip_ws
|
|
229
298
|
if (class_name = @scanner.scan(CLASS_NAME))
|
|
230
|
-
|
|
299
|
+
parse_class_arg_tail(class_name)
|
|
300
|
+
elsif (literal = @scanner.scan(SIGNED_INT))
|
|
301
|
+
# Integer-literal arg, used by `int_mask[1, 2, 4]`.
|
|
302
|
+
# Wrapped as `Constant<Integer>` so type-arg builders
|
|
303
|
+
# see a uniform `Array<Type::t>`.
|
|
304
|
+
Type::Combinator.constant_of(Integer(literal))
|
|
231
305
|
else
|
|
232
306
|
parse_type
|
|
233
307
|
end
|
|
234
308
|
end
|
|
235
309
|
|
|
310
|
+
# Class-name-headed type argument with optional `[T_1,
|
|
311
|
+
# …]` type-args tail. Used so `key_of[Hash[Symbol,
|
|
312
|
+
# Integer]]` parses as the projection of a parameterised
|
|
313
|
+
# nominal carrier rather than rejecting the inner
|
|
314
|
+
# brackets.
|
|
315
|
+
def parse_class_arg_tail(class_name)
|
|
316
|
+
return Type::Combinator.nominal_of(class_name) unless @scanner.peek(1) == "["
|
|
317
|
+
|
|
318
|
+
@scanner.getch # consume '['
|
|
319
|
+
args = parse_type_arg_list
|
|
320
|
+
return nil if args.nil?
|
|
321
|
+
return nil unless @scanner.getch == "]"
|
|
322
|
+
|
|
323
|
+
Type::Combinator.nominal_of(class_name, type_args: args)
|
|
324
|
+
end
|
|
325
|
+
|
|
236
326
|
def parse_int_bound
|
|
237
327
|
skip_ws
|
|
238
328
|
literal = @scanner.scan(SIGNED_INT)
|
|
@@ -52,6 +52,12 @@ module Rigor
|
|
|
52
52
|
# Literals
|
|
53
53
|
Prism::IntegerNode => :type_of_literal_value,
|
|
54
54
|
Prism::FloatNode => :type_of_literal_value,
|
|
55
|
+
# `1i` / `2.5ri` lift via `node.value` which is already a
|
|
56
|
+
# `Complex` Ruby value; same for `1r` / `1.5r` whose
|
|
57
|
+
# value is a `Rational`. `Type::Constant` accepts both
|
|
58
|
+
# via `SCALAR_CLASSES`.
|
|
59
|
+
Prism::ImaginaryNode => :type_of_literal_value,
|
|
60
|
+
Prism::RationalNode => :type_of_literal_value,
|
|
55
61
|
Prism::SymbolNode => :symbol_type_for,
|
|
56
62
|
Prism::StringNode => :string_type_for,
|
|
57
63
|
Prism::TrueNode => :type_of_true,
|
|
@@ -401,7 +407,13 @@ module Rigor
|
|
|
401
407
|
# so callers stay backward compatible.
|
|
402
408
|
def type_of_hash(node)
|
|
403
409
|
elements = node.respond_to?(:elements) ? node.elements : []
|
|
404
|
-
|
|
410
|
+
# v0.0.7 — `{}` resolves to the empty `HashShape{}` carrier
|
|
411
|
+
# rather than `Nominal[Hash]`, mirroring the v0.0.6 empty-
|
|
412
|
+
# array literal change. Both forms erase to plain `Hash`,
|
|
413
|
+
# but `HashShape{}` pins the literal's known size (zero)
|
|
414
|
+
# so HashShape projections (`empty?`, `first`, `count`,
|
|
415
|
+
# …) fold against it.
|
|
416
|
+
return Type::Combinator.hash_shape_of({}) if elements.empty?
|
|
405
417
|
|
|
406
418
|
shape = static_hash_shape_for(elements)
|
|
407
419
|
return shape if shape
|
|
@@ -691,7 +703,18 @@ module Rigor
|
|
|
691
703
|
Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?))
|
|
692
704
|
end
|
|
693
705
|
|
|
694
|
-
|
|
706
|
+
# v0.0.7 — non-interpolated regex literals lift to
|
|
707
|
+
# `Constant<Regexp>` so `Constant<String>#scan(/regex/)`
|
|
708
|
+
# / `#match(/regex/)` etc. can fold through the catalog
|
|
709
|
+
# tier. Interpolated regexes (`/foo#{x}/`) reach the
|
|
710
|
+
# second `Prism::InterpolatedRegularExpressionNode` arm
|
|
711
|
+
# which keeps the conservative `Nominal[Regexp]` answer.
|
|
712
|
+
def type_of_regexp(node)
|
|
713
|
+
return Type::Combinator.nominal_of(Regexp) unless node.is_a?(Prism::RegularExpressionNode)
|
|
714
|
+
|
|
715
|
+
regex = Regexp.new(node.unescaped, node.options)
|
|
716
|
+
Type::Combinator.constant_of(regex)
|
|
717
|
+
rescue StandardError
|
|
695
718
|
Type::Combinator.nominal_of(Regexp)
|
|
696
719
|
end
|
|
697
720
|
|
|
@@ -109,6 +109,16 @@ module Rigor
|
|
|
109
109
|
|
|
110
110
|
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
111
111
|
def try_fold(receiver:, method_name:, args:)
|
|
112
|
+
# v0.0.7 — `String#%` against a `Tuple` / `HashShape`
|
|
113
|
+
# argument runs Ruby's format-string engine when both
|
|
114
|
+
# sides are statically constant. The standard
|
|
115
|
+
# `numeric_set_of` path bails on Tuple / HashShape
|
|
116
|
+
# arguments because they are not scalar-Constant
|
|
117
|
+
# carriers, so the special-case sits ahead of the
|
|
118
|
+
# numeric path.
|
|
119
|
+
format_lift = try_fold_string_format(receiver, method_name, args)
|
|
120
|
+
return format_lift if format_lift
|
|
121
|
+
|
|
112
122
|
receiver_set = numeric_set_of(receiver)
|
|
113
123
|
return nil unless receiver_set
|
|
114
124
|
|
|
@@ -118,6 +128,59 @@ module Rigor
|
|
|
118
128
|
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
119
129
|
end
|
|
120
130
|
|
|
131
|
+
# `Constant<String> % …` — runs the actual `String#%`
|
|
132
|
+
# operation when both sides are statically known. The
|
|
133
|
+
# argument may be:
|
|
134
|
+
# - A `Type::Constant` whose value is a scalar (Integer
|
|
135
|
+
# / Float / String / Symbol). Already handled by the
|
|
136
|
+
# numeric path; this method declines so the standard
|
|
137
|
+
# binary path picks it up.
|
|
138
|
+
# - A `Type::Tuple` whose elements are all `Constant`.
|
|
139
|
+
# Materialises the elements as a Ruby Array and runs
|
|
140
|
+
# the format.
|
|
141
|
+
# - A `Type::HashShape` with no optional keys whose
|
|
142
|
+
# values are all `Constant`. Materialises a Ruby Hash
|
|
143
|
+
# and runs the format. Symbol keys are kept as
|
|
144
|
+
# Symbols (matching Ruby's `%{key}` resolution).
|
|
145
|
+
# Anything else declines so the RBS tier widens.
|
|
146
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
147
|
+
def try_fold_string_format(receiver, method_name, args)
|
|
148
|
+
return nil unless method_name == :%
|
|
149
|
+
return nil unless args.size == 1
|
|
150
|
+
return nil unless receiver.is_a?(Type::Constant) && receiver.value.is_a?(String)
|
|
151
|
+
|
|
152
|
+
arg = args.first
|
|
153
|
+
ruby_arg = format_argument_value(arg)
|
|
154
|
+
return nil if ruby_arg.nil?
|
|
155
|
+
|
|
156
|
+
result = receiver.value % ruby_arg
|
|
157
|
+
return nil unless foldable_constant_value?(result)
|
|
158
|
+
|
|
159
|
+
Type::Combinator.constant_of(result)
|
|
160
|
+
rescue StandardError
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
164
|
+
|
|
165
|
+
def format_argument_value(arg)
|
|
166
|
+
case arg
|
|
167
|
+
when Type::Tuple
|
|
168
|
+
return nil unless arg.elements.all?(Type::Constant)
|
|
169
|
+
|
|
170
|
+
arg.elements.map(&:value)
|
|
171
|
+
when Type::HashShape
|
|
172
|
+
hash_shape_format_value(arg)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def hash_shape_format_value(shape)
|
|
177
|
+
return nil unless shape.closed?
|
|
178
|
+
return nil unless shape.optional_keys.empty?
|
|
179
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
180
|
+
|
|
181
|
+
shape.pairs.transform_values(&:value)
|
|
182
|
+
end
|
|
183
|
+
|
|
121
184
|
def dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
122
185
|
case arg_sets.size
|
|
123
186
|
when 0 then try_fold_unary(receiver_set, method_name)
|
|
@@ -206,7 +269,17 @@ module Rigor
|
|
|
206
269
|
[result]
|
|
207
270
|
end
|
|
208
271
|
|
|
272
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
209
273
|
def try_fold_unary_set(receiver_values, method_name)
|
|
274
|
+
range_lift = try_fold_range_constant_unary(receiver_values, method_name)
|
|
275
|
+
return range_lift if range_lift
|
|
276
|
+
|
|
277
|
+
string_lift = try_fold_string_array_unary(receiver_values, method_name)
|
|
278
|
+
return string_lift if string_lift
|
|
279
|
+
|
|
280
|
+
pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
|
|
281
|
+
return pathname_lift if pathname_lift
|
|
282
|
+
|
|
210
283
|
# Type-level allow check on every receiver. If one member's
|
|
211
284
|
# type does not have the method in its allow list (e.g.
|
|
212
285
|
# `Union[String, nil].nil?` — `:nil?` is not in
|
|
@@ -220,8 +293,68 @@ module Rigor
|
|
|
220
293
|
end
|
|
221
294
|
build_constant_type(results, source: receiver_values)
|
|
222
295
|
end
|
|
296
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
297
|
+
|
|
298
|
+
# v0.0.7 — `Constant<Range>#to_a` and the no-arg
|
|
299
|
+
# `first` / `last` / `min` / `max` short-circuit through a
|
|
300
|
+
# Range-specific arm that catalog dispatch cannot reach:
|
|
301
|
+
# - `to_a` returns an Array (not foldable through
|
|
302
|
+
# `foldable_constant_value?`) — lift to `Tuple[Constant…]`
|
|
303
|
+
# when the cardinality fits within `RANGE_TO_A_LIMIT`.
|
|
304
|
+
# - `first` / `last` / `min` / `max` are catalog-classified
|
|
305
|
+
# `:block_dependent` because of the optional-block forms,
|
|
306
|
+
# but the no-arg form is pure for finite integer ranges.
|
|
307
|
+
#
|
|
308
|
+
# Only fires on a single-receiver Range with finite integer
|
|
309
|
+
# endpoints; mixed unions fall through so the existing
|
|
310
|
+
# union-of-Constants path keeps the rest of the arms.
|
|
311
|
+
RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length].freeze
|
|
312
|
+
RANGE_TO_A_LIMIT = 16
|
|
313
|
+
private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
|
|
314
|
+
|
|
315
|
+
def try_fold_range_constant_unary(receiver_values, method_name)
|
|
316
|
+
return nil unless RANGE_FOLD_METHODS.include?(method_name)
|
|
317
|
+
return nil unless receiver_values.size == 1
|
|
223
318
|
|
|
319
|
+
range = receiver_values.first
|
|
320
|
+
return nil unless range.is_a?(Range)
|
|
321
|
+
return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
|
|
322
|
+
|
|
323
|
+
range_constant_unary(range, method_name)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def range_constant_unary(range, method_name)
|
|
327
|
+
case method_name
|
|
328
|
+
when :to_a then range_to_a_tuple(range)
|
|
329
|
+
when :first, :min then range_endpoint_constant(range, :first)
|
|
330
|
+
when :last, :max then range_endpoint_constant(range, :last)
|
|
331
|
+
when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def range_to_a_tuple(range)
|
|
336
|
+
values = range.to_a
|
|
337
|
+
return Type::Combinator.tuple_of if values.empty?
|
|
338
|
+
return nil if values.size > RANGE_TO_A_LIMIT
|
|
339
|
+
|
|
340
|
+
Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def range_endpoint_constant(range, edge)
|
|
344
|
+
values = range.to_a
|
|
345
|
+
return Type::Combinator.constant_of(nil) if values.empty?
|
|
346
|
+
|
|
347
|
+
Type::Combinator.constant_of(edge == :first ? values.first : values.last)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
224
351
|
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
352
|
+
string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
353
|
+
return string_lift if string_lift
|
|
354
|
+
|
|
355
|
+
pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
|
|
356
|
+
return pathname_lift if pathname_lift
|
|
357
|
+
|
|
225
358
|
return nil if receiver_values.size * arg_values.size > UNION_FOLD_INPUT_LIMIT
|
|
226
359
|
return nil unless receiver_values.all? { |rv| binary_method_allowed?(rv, method_name) }
|
|
227
360
|
|
|
@@ -230,6 +363,119 @@ module Rigor
|
|
|
230
363
|
end
|
|
231
364
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
232
365
|
end
|
|
366
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
367
|
+
|
|
368
|
+
# v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
|
|
369
|
+
# `split` (no-arg) return a Ruby Array of foldable
|
|
370
|
+
# scalars; `foldable_constant_value?` rejects Array
|
|
371
|
+
# results, so the standard unary path declines. Lift the
|
|
372
|
+
# Array to a per-position `Tuple[Constant…]` directly,
|
|
373
|
+
# capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
|
|
374
|
+
# bounded for long strings.
|
|
375
|
+
STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :lines, :split].freeze
|
|
376
|
+
STRING_ARRAY_BINARY_METHODS = Set[:split, :scan].freeze
|
|
377
|
+
STRING_ARRAY_LIFT_LIMIT = 32
|
|
378
|
+
private_constant :STRING_ARRAY_UNARY_METHODS,
|
|
379
|
+
:STRING_ARRAY_BINARY_METHODS,
|
|
380
|
+
:STRING_ARRAY_LIFT_LIMIT
|
|
381
|
+
|
|
382
|
+
# v0.0.7 — `Constant<Pathname>` delegates to a curated set
|
|
383
|
+
# of pure path-manipulation methods. Pathname is immutable
|
|
384
|
+
# in Ruby (per its docstring) and the catalog classifies
|
|
385
|
+
# most methods `:dispatch` because the C body delegates to
|
|
386
|
+
# File / Dir / FileTest. The methods listed here are
|
|
387
|
+
# filesystem-independent — they read only `@path` — so
|
|
388
|
+
# invoking them at fold time produces a deterministic
|
|
389
|
+
# result regardless of the host filesystem state.
|
|
390
|
+
#
|
|
391
|
+
# Filesystem-touching methods (`exist?`, `file?`, `read`,
|
|
392
|
+
# `stat`, …) are intentionally NOT folded: their answer
|
|
393
|
+
# depends on the analysis machine's filesystem, which is
|
|
394
|
+
# neither stable nor relevant to the analyzed program.
|
|
395
|
+
PATHNAME_PURE_UNARY = Set[
|
|
396
|
+
:to_s, :to_path, :to_str,
|
|
397
|
+
:basename, :dirname, :extname, :cleanpath,
|
|
398
|
+
:parent, :sub_ext, :root?, :absolute?, :relative?,
|
|
399
|
+
:hash, :inspect
|
|
400
|
+
].freeze
|
|
401
|
+
PATHNAME_PURE_BINARY = Set[
|
|
402
|
+
:+, :join, :sub_ext, :<=>, :==, :eql?, :===,
|
|
403
|
+
:relative_path_from
|
|
404
|
+
].freeze
|
|
405
|
+
private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
|
|
406
|
+
|
|
407
|
+
def try_fold_pathname_unary(receiver_values, method_name)
|
|
408
|
+
return nil unless PATHNAME_PURE_UNARY.include?(method_name)
|
|
409
|
+
return nil unless receiver_values.size == 1
|
|
410
|
+
|
|
411
|
+
receiver = receiver_values.first
|
|
412
|
+
return nil unless receiver.is_a?(Pathname)
|
|
413
|
+
|
|
414
|
+
result = receiver.public_send(method_name)
|
|
415
|
+
return nil unless foldable_constant_value?(result)
|
|
416
|
+
|
|
417
|
+
Type::Combinator.constant_of(result)
|
|
418
|
+
rescue StandardError
|
|
419
|
+
nil
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
423
|
+
def try_fold_pathname_binary(receiver_values, method_name, arg_values)
|
|
424
|
+
return nil unless PATHNAME_PURE_BINARY.include?(method_name)
|
|
425
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
426
|
+
|
|
427
|
+
receiver = receiver_values.first
|
|
428
|
+
arg = arg_values.first
|
|
429
|
+
return nil unless receiver.is_a?(Pathname)
|
|
430
|
+
return nil unless arg.is_a?(Pathname) || arg.is_a?(String)
|
|
431
|
+
|
|
432
|
+
result = receiver.public_send(method_name, arg)
|
|
433
|
+
return nil unless foldable_constant_value?(result)
|
|
434
|
+
|
|
435
|
+
Type::Combinator.constant_of(result)
|
|
436
|
+
rescue StandardError
|
|
437
|
+
nil
|
|
438
|
+
end
|
|
439
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
440
|
+
|
|
441
|
+
def try_fold_string_array_unary(receiver_values, method_name)
|
|
442
|
+
return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
|
|
443
|
+
return nil unless receiver_values.size == 1
|
|
444
|
+
|
|
445
|
+
receiver = receiver_values.first
|
|
446
|
+
return nil unless receiver.is_a?(String)
|
|
447
|
+
|
|
448
|
+
lift_array_result(receiver.public_send(method_name))
|
|
449
|
+
rescue StandardError
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# `Constant<String>#split(arg)` / `#scan(arg)` — lift the
|
|
454
|
+
# Array result to a Tuple when both sides are statically
|
|
455
|
+
# known and the cardinality fits.
|
|
456
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
457
|
+
def try_fold_string_array_binary(receiver_values, method_name, arg_values)
|
|
458
|
+
return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
|
|
459
|
+
return nil unless receiver_values.size == 1 && arg_values.size == 1
|
|
460
|
+
|
|
461
|
+
receiver = receiver_values.first
|
|
462
|
+
arg = arg_values.first
|
|
463
|
+
return nil unless receiver.is_a?(String)
|
|
464
|
+
return nil unless arg.is_a?(String) || arg.is_a?(Regexp)
|
|
465
|
+
|
|
466
|
+
lift_array_result(receiver.public_send(method_name, arg))
|
|
467
|
+
rescue StandardError
|
|
468
|
+
nil
|
|
469
|
+
end
|
|
470
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
471
|
+
|
|
472
|
+
def lift_array_result(result)
|
|
473
|
+
return nil unless result.is_a?(Array)
|
|
474
|
+
return nil if result.size > STRING_ARRAY_LIFT_LIMIT
|
|
475
|
+
return nil unless result.all? { |v| foldable_constant_value?(v) }
|
|
476
|
+
|
|
477
|
+
Type::Combinator.tuple_of(*result.map { |v| Type::Combinator.constant_of(v) })
|
|
478
|
+
end
|
|
233
479
|
|
|
234
480
|
# 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
|
|
235
481
|
# `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
|
|
@@ -879,7 +1125,7 @@ module Rigor
|
|
|
879
1125
|
# round-trip through `Type::Combinator.constant_of`".
|
|
880
1126
|
def foldable_constant_value?(value)
|
|
881
1127
|
case value
|
|
882
|
-
when Integer, Float, String, Symbol, true, false, nil then true
|
|
1128
|
+
when Integer, Float, Rational, Complex, String, Symbol, Regexp, Pathname, true, false, nil then true
|
|
883
1129
|
else false
|
|
884
1130
|
end
|
|
885
1131
|
end
|