rigortype 0.1.18 → 0.1.19
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 +159 -224
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +169 -23
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +9 -3
- data/lib/rigor/analysis/check_rules.rb +266 -63
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +2 -1
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -1
- data/lib/rigor/analysis/runner.rb +58 -21
- data/lib/rigor/analysis/worker_session.rb +21 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +3 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +9 -1
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +57 -1
- data/lib/rigor/environment/rbs_loader.rb +25 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1052 -43
- data/lib/rigor/inference/macro_block_self_type.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +72 -1
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +270 -37
- data/lib/rigor/inference/scope_indexer.rb +696 -25
- data/lib/rigor/inference/statement_evaluator.rb +963 -16
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/base.rb +235 -79
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +59 -14
- data/lib/rigor/plugin/registry.rb +12 -11
- data/lib/rigor/scope/discovery_index.rb +2 -0
- data/lib/rigor/scope.rb +132 -6
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +29 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +20 -19
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +10 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +2 -13
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +25 -0
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/plugin/base.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +10 -1
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +7 -2
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -98,7 +98,8 @@ module Rigor
|
|
|
98
98
|
uniq: :tuple_uniq,
|
|
99
99
|
index: :tuple_find_index,
|
|
100
100
|
find_index: :tuple_find_index,
|
|
101
|
-
rindex: :tuple_rindex
|
|
101
|
+
rindex: :tuple_rindex,
|
|
102
|
+
flatten: :tuple_flatten
|
|
102
103
|
}.freeze
|
|
103
104
|
|
|
104
105
|
HASH_SHAPE_HANDLERS = {
|
|
@@ -228,15 +229,8 @@ module Rigor
|
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
def dispatch_nominal_size(nominal, method_name, args)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return string_binary if string_binary
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
if nominal.class_name == "Integer" && args.size == 1
|
|
237
|
-
integer_binary = dispatch_integer_binary_from_arg(method_name, args.first)
|
|
238
|
-
return integer_binary if integer_binary
|
|
239
|
-
end
|
|
232
|
+
projection = nominal_projection(nominal, method_name, args)
|
|
233
|
+
return projection if projection
|
|
240
234
|
|
|
241
235
|
return nil unless args.empty?
|
|
242
236
|
|
|
@@ -246,6 +240,106 @@ module Rigor
|
|
|
246
240
|
Type::Combinator.non_negative_int
|
|
247
241
|
end
|
|
248
242
|
|
|
243
|
+
# Arg-/method-driven precision projections for a `Nominal`
|
|
244
|
+
# receiver, consulted ahead of the no-arg size tier. Each
|
|
245
|
+
# branch gates on the class name first so unrelated nominals
|
|
246
|
+
# skip the work. Returns nil when no projection applies.
|
|
247
|
+
def nominal_projection(nominal, method_name, args)
|
|
248
|
+
case nominal.class_name
|
|
249
|
+
when "String"
|
|
250
|
+
dispatch_string_binary_from_arg(method_name, args.first) if args.size == 1
|
|
251
|
+
when "Integer"
|
|
252
|
+
dispatch_integer_binary_from_arg(method_name, args.first) if args.size == 1
|
|
253
|
+
when "Array"
|
|
254
|
+
case method_name
|
|
255
|
+
when :flatten then array_nominal_flatten(nominal, args)
|
|
256
|
+
when :compact then array_nominal_compact(nominal, args)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# `Array[T]#compact` — `compact` removes every `nil` element,
|
|
262
|
+
# so the result element type is `T` with its `nil` constituent
|
|
263
|
+
# stripped (`Array[Node?]#compact` → `Array[Node]`). Mirrors
|
|
264
|
+
# the `Tuple#compact` constant fold for the generic element
|
|
265
|
+
# case. Declines when the receiver carries no type argument
|
|
266
|
+
# (the RBS `Array[untyped]` answer is already maximal) or when
|
|
267
|
+
# `T` has no `nil` constituent to remove (the result equals the
|
|
268
|
+
# receiver, so the RBS tier's answer is already precise).
|
|
269
|
+
def array_nominal_compact(nominal, args)
|
|
270
|
+
return nil unless args.empty?
|
|
271
|
+
|
|
272
|
+
element = nominal.type_args&.first
|
|
273
|
+
return nil if element.nil?
|
|
274
|
+
|
|
275
|
+
stripped = strip_nil_constituent(element)
|
|
276
|
+
return nil if stripped.equal?(element)
|
|
277
|
+
|
|
278
|
+
Type::Combinator.nominal_of("Array", type_args: [stripped])
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Removes the `nil` constituent from a (possibly union) type,
|
|
282
|
+
# returning the same object when there is nothing to remove so
|
|
283
|
+
# callers can detect the no-op cheaply. Kept local to the
|
|
284
|
+
# dispatch tier to avoid a dependency on the narrowing module.
|
|
285
|
+
def strip_nil_constituent(type)
|
|
286
|
+
case type
|
|
287
|
+
when Type::Constant
|
|
288
|
+
type.value.nil? ? Type::Combinator.bot : type
|
|
289
|
+
when Type::Nominal
|
|
290
|
+
type.class_name == "NilClass" ? Type::Combinator.bot : type
|
|
291
|
+
when Type::Union
|
|
292
|
+
kept = type.members.map { |m| strip_nil_constituent(m) }
|
|
293
|
+
return type if kept.zip(type.members).all? { |k, m| k.equal?(m) }
|
|
294
|
+
|
|
295
|
+
Type::Combinator.union(*kept)
|
|
296
|
+
else
|
|
297
|
+
type
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# `Array[T]#flatten` (and `flatten(depth)`). When `T` is a
|
|
302
|
+
# nested `Array[U]` nominal, one flatten level yields the
|
|
303
|
+
# joined inner element type — `Array[Array[U]]#flatten` →
|
|
304
|
+
# `Array[U]`. When `T` is non-nested the result is `Array[T]`
|
|
305
|
+
# unchanged (Ruby returns a copy with the same element type).
|
|
306
|
+
# Multi-level nesting is handled conservatively: each level
|
|
307
|
+
# joins its element types, and a `depth` argument that does
|
|
308
|
+
# not fully resolve the nesting still produces a sound
|
|
309
|
+
# superset. Declines on an `Array` with no type argument
|
|
310
|
+
# (the RBS `Array[untyped]` answer is already as precise as
|
|
311
|
+
# we can be) and on a non-static depth argument.
|
|
312
|
+
def array_nominal_flatten(nominal, args)
|
|
313
|
+
element = nominal.type_args&.first
|
|
314
|
+
return nil if element.nil?
|
|
315
|
+
|
|
316
|
+
depth = tuple_flatten_depth(args)
|
|
317
|
+
return nil if depth == :decline
|
|
318
|
+
|
|
319
|
+
flattened = flatten_nominal_element(element, depth)
|
|
320
|
+
Type::Combinator.nominal_of("Array", type_args: [flattened])
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Resolves the element type of a flattened `Array[element]`.
|
|
324
|
+
# Each `Array[U]` nesting level contributes `U`; the per-level
|
|
325
|
+
# element types are unioned. `depth < 0` recurses without
|
|
326
|
+
# bound; `depth == 0` stops (Ruby's `flatten(0)` is a no-op
|
|
327
|
+
# copy and returns the element unchanged).
|
|
328
|
+
def flatten_nominal_element(element, depth)
|
|
329
|
+
return element if depth.zero?
|
|
330
|
+
return element unless array_nominal?(element)
|
|
331
|
+
|
|
332
|
+
inner = element.type_args.first
|
|
333
|
+
return element if inner.nil?
|
|
334
|
+
|
|
335
|
+
flatten_nominal_element(inner, depth - 1)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def array_nominal?(type)
|
|
339
|
+
type.is_a?(Type::Nominal) && type.class_name == "Array" && !type.type_args.nil? &&
|
|
340
|
+
!type.type_args.empty?
|
|
341
|
+
end
|
|
342
|
+
|
|
249
343
|
# Arg-type-driven String binary projections for any String-typed
|
|
250
344
|
# receiver (including Nominal, Refined, and Difference fallbacks).
|
|
251
345
|
# Called before the no-arg size guard so binary operators are seen.
|
|
@@ -875,6 +969,50 @@ module Rigor
|
|
|
875
969
|
constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
|
|
876
970
|
end
|
|
877
971
|
|
|
972
|
+
# `tuple.flatten` / `tuple.flatten(depth)` — recursively
|
|
973
|
+
# flattens nested Tuple elements into a single Tuple. With
|
|
974
|
+
# no argument the flatten is unbounded (matching Ruby's
|
|
975
|
+
# `Array#flatten`); a `Constant[Integer]` depth bounds it.
|
|
976
|
+
# Non-Tuple elements (scalars, `Array[T]` nominals, …) pass
|
|
977
|
+
# through unchanged at their level. A non-static depth
|
|
978
|
+
# argument (or a non-Integer one) declines so RBS answers.
|
|
979
|
+
def tuple_flatten(tuple, _method_name, args)
|
|
980
|
+
depth = tuple_flatten_depth(args)
|
|
981
|
+
return nil if depth == :decline
|
|
982
|
+
|
|
983
|
+
Type::Combinator.tuple_of(*flatten_elements(tuple.elements, depth))
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# Returns the requested flatten depth: `-1` for the no-arg
|
|
987
|
+
# (unbounded) form, the Integer for a `Constant[Integer]`
|
|
988
|
+
# argument, or `:decline` for any non-static / wrong-arity
|
|
989
|
+
# argument shape.
|
|
990
|
+
def tuple_flatten_depth(args)
|
|
991
|
+
return -1 if args.empty?
|
|
992
|
+
return :decline unless args.size == 1
|
|
993
|
+
|
|
994
|
+
arg = args.first
|
|
995
|
+
return arg.value if arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
996
|
+
|
|
997
|
+
:decline
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
# Flattens a list of element types to `depth` levels.
|
|
1001
|
+
# `depth < 0` means unbounded. A Tuple element is spliced
|
|
1002
|
+
# in (recursing with `depth - 1`); everything else passes
|
|
1003
|
+
# through at this level.
|
|
1004
|
+
def flatten_elements(elements, depth)
|
|
1005
|
+
return elements if depth.zero?
|
|
1006
|
+
|
|
1007
|
+
elements.flat_map do |element|
|
|
1008
|
+
if element.is_a?(Type::Tuple)
|
|
1009
|
+
flatten_elements(element.elements, depth - 1)
|
|
1010
|
+
else
|
|
1011
|
+
[element]
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
|
|
878
1016
|
# `rindex(obj)` → the LAST matching index, same decidability gate.
|
|
879
1017
|
def tuple_rindex(tuple, _method_name, args)
|
|
880
1018
|
constant_index(tuple, args) { |elements, value| elements.rindex { |e| e.value == value } }
|
|
@@ -14,7 +14,9 @@ require_relative "method_dispatcher/shape_dispatch"
|
|
|
14
14
|
require_relative "method_dispatcher/data_folding"
|
|
15
15
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
16
16
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
17
|
+
require_relative "method_dispatcher/reduce_folding"
|
|
17
18
|
require_relative "method_dispatcher/block_folding"
|
|
19
|
+
require_relative "method_dispatcher/array_to_h_folding"
|
|
18
20
|
require_relative "method_dispatcher/file_folding"
|
|
19
21
|
require_relative "method_dispatcher/shellwords_folding"
|
|
20
22
|
require_relative "method_dispatcher/math_folding"
|
|
@@ -274,7 +276,14 @@ module Rigor
|
|
|
274
276
|
class_name, kind = discovered_method_lookup(receiver_type)
|
|
275
277
|
return nil if class_name.nil?
|
|
276
278
|
return nil unless scope.discovered_method?(class_name, method_name, kind)
|
|
279
|
+
# Decline when a re-typable body is recorded for the method, so the
|
|
280
|
+
# downstream `ExpressionTyper` inference tier can fold a precise
|
|
281
|
+
# return instead of collapsing to `Dynamic[top]` here — instance
|
|
282
|
+
# bodies via `user_def_for`, singleton bodies (`def self.x` /
|
|
283
|
+
# `module_function`) via `singleton_def_for` (module-singleton
|
|
284
|
+
# call resolution, ADR-57 follow-up).
|
|
277
285
|
return nil if kind == :instance && scope.user_def_for(class_name, method_name)
|
|
286
|
+
return nil if kind == :singleton && scope.singleton_def_for(class_name, method_name)
|
|
278
287
|
|
|
279
288
|
Type::Combinator.untyped
|
|
280
289
|
end
|
|
@@ -792,7 +801,7 @@ module Rigor
|
|
|
792
801
|
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
793
802
|
|
|
794
803
|
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
795
|
-
KernelDispatch, MethodFolding, BlockFolding
|
|
804
|
+
KernelDispatch, MethodFolding, ReduceFolding, ArrayToHFolding, BlockFolding
|
|
796
805
|
].freeze)
|
|
797
806
|
private_constant :PRECISE_TIERS_TAIL
|
|
798
807
|
|
|
@@ -952,18 +961,53 @@ module Rigor
|
|
|
952
961
|
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
953
962
|
return set_lift if set_lift
|
|
954
963
|
|
|
964
|
+
hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
|
|
965
|
+
return hash_lift if hash_lift
|
|
966
|
+
|
|
955
967
|
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
956
968
|
return regexp_lift if regexp_lift
|
|
957
969
|
|
|
958
970
|
date_lift = date_new_lift(receiver_type.class_name, arg_types)
|
|
959
971
|
return date_lift if date_lift
|
|
960
972
|
|
|
973
|
+
struct_new_lift = struct_new_lift(receiver_type.class_name, arg_types)
|
|
974
|
+
return struct_new_lift if struct_new_lift
|
|
975
|
+
|
|
961
976
|
class_new_lift = class_new_lift(receiver_type.class_name, arg_types)
|
|
962
977
|
return class_new_lift if class_new_lift
|
|
963
978
|
|
|
964
979
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
965
980
|
end
|
|
966
981
|
|
|
982
|
+
# `Struct.new(:a, :b)` synthesises an anonymous Struct *subclass*
|
|
983
|
+
# (a class object), not a Struct *instance* — so the chained
|
|
984
|
+
# idiom `Struct.new(:a, :b).new(1, 2)` must resolve `.new` again
|
|
985
|
+
# on a class-like carrier. The constant-bound form
|
|
986
|
+
# (`S = Struct.new(:a); S.new(1)`) already records `Singleton[S]`
|
|
987
|
+
# via `ScopeIndexer#record_meta_new_constant?`; this lift gives
|
|
988
|
+
# the *chained* (anonymous) position the same class-like carrier
|
|
989
|
+
# so the trailing `.new` dispatches instead of firing a spurious
|
|
990
|
+
# `undefined method 'new' for Struct`.
|
|
991
|
+
#
|
|
992
|
+
# The disambiguation mirrors `ScopeIndexer#struct_new_call?`: a
|
|
993
|
+
# call whose positionals are all `Constant<Symbol>` literals is a
|
|
994
|
+
# member-list class definition → `Singleton[Struct]`. The
|
|
995
|
+
# following `AnonStruct.new(1, 2)` carries non-symbol args, so it
|
|
996
|
+
# falls through this gate to `Nominal[Struct]` (a fresh instance)
|
|
997
|
+
# via the `meta_new` tail. ADR-48 deferred full Struct *value*
|
|
998
|
+
# folding (member-reader precision) on mutability grounds; this is
|
|
999
|
+
# the narrower `.new`-dispatch-only fix and contributes no member
|
|
1000
|
+
# layout, so `instance.a` stays at its RBS/Dynamic type.
|
|
1001
|
+
def struct_new_lift(class_name, arg_types)
|
|
1002
|
+
return nil unless class_name == "Struct"
|
|
1003
|
+
|
|
1004
|
+
positional = arg_types.grep_v(Type::HashShape)
|
|
1005
|
+
return nil if positional.empty?
|
|
1006
|
+
return nil unless positional.all? { |t| t.is_a?(Type::Constant) && t.value.is_a?(Symbol) }
|
|
1007
|
+
|
|
1008
|
+
Type::Combinator.singleton_of("Struct")
|
|
1009
|
+
end
|
|
1010
|
+
|
|
967
1011
|
# `Class.new` and `Class.new(Parent)` create a brand-new
|
|
968
1012
|
# anonymous class. Statically that class is representable as
|
|
969
1013
|
# the parent's singleton type — its singleton-method surface
|
|
@@ -1050,6 +1094,33 @@ module Rigor
|
|
|
1050
1094
|
type
|
|
1051
1095
|
end
|
|
1052
1096
|
|
|
1097
|
+
# `Hash.new(default)` — lifts the default value's type into the
|
|
1098
|
+
# Hash's value parameter so a subsequent `h[k]` read surfaces the
|
|
1099
|
+
# default type rather than `Dynamic[top]`. The common counter
|
|
1100
|
+
# idiom `h = Hash.new(0); h[k] += 1` then types the read as
|
|
1101
|
+
# `Integer`. The key parameter is left `untyped` (the default
|
|
1102
|
+
# carrier imposes no key constraint), so reads of any key resolve
|
|
1103
|
+
# through the value parameter. A value-pinned `Constant` default
|
|
1104
|
+
# (`0`) is widened to its nominal (`Integer`): the hash's values
|
|
1105
|
+
# mutate over its lifetime, so pinning the parameter to the
|
|
1106
|
+
# literal would be unsound for the aggregate.
|
|
1107
|
+
#
|
|
1108
|
+
# Only the single-argument default form folds. The zero-arg
|
|
1109
|
+
# (`Hash.new`) and the block form (`Hash.new { |h, k| … }`) keep
|
|
1110
|
+
# the bare `Nominal[Hash]` answer — the block's value type is not
|
|
1111
|
+
# available at this `:new` dispatch site, and conservatively
|
|
1112
|
+
# leaving the read as today's behaviour is precision-additive.
|
|
1113
|
+
def hash_new_lift(class_name, arg_types)
|
|
1114
|
+
return nil unless class_name == "Hash"
|
|
1115
|
+
return nil unless arg_types.size == 1
|
|
1116
|
+
|
|
1117
|
+
default = arg_types.first
|
|
1118
|
+
return nil if default.nil?
|
|
1119
|
+
|
|
1120
|
+
value = Type::Combinator.widen_value_pinned(default)
|
|
1121
|
+
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
|
|
1122
|
+
end
|
|
1123
|
+
|
|
1053
1124
|
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to
|
|
1054
1125
|
# `Constant[Range]` when both endpoints are `Constant[Integer]`
|
|
1055
1126
|
# or both are `Constant[String]`, and the optional third argument
|
|
@@ -32,6 +32,35 @@ module Rigor
|
|
|
32
32
|
# `#singleton_method`.
|
|
33
33
|
#
|
|
34
34
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
35
|
+
# Leaf-name extraction for a destructured positional parameter
|
|
36
|
+
# (`Prism::MultiTargetNode`). Stateless; lifted out of
|
|
37
|
+
# {MethodParameterBinder} so the binder's class length stays in
|
|
38
|
+
# budget.
|
|
39
|
+
module Destructure
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Collect every leaf local name a `MultiTargetNode` binds,
|
|
43
|
+
# recursing through nested destructures (`((a, b), c)`) and the
|
|
44
|
+
# splat slot (`(a, *rest)`). Targets without a `#name` (an
|
|
45
|
+
# index/call write target, vanishingly rare in a parameter
|
|
46
|
+
# position) are skipped — there is no local to bind.
|
|
47
|
+
def target_names(multi_target)
|
|
48
|
+
entries = multi_target.lefts + [multi_target.rest, *multi_target.rights].compact
|
|
49
|
+
entries.flat_map { |entry| names_for_entry(entry) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names_for_entry(entry)
|
|
53
|
+
# A splat sub-target (`*rest` inside the destructure) wraps its
|
|
54
|
+
# real target in a `SplatNode#expression`; unwrap it.
|
|
55
|
+
entry = entry.expression if entry.is_a?(Prism::SplatNode) && entry.expression
|
|
56
|
+
return [] if entry.nil?
|
|
57
|
+
return target_names(entry) if entry.is_a?(Prism::MultiTargetNode)
|
|
58
|
+
return [entry.name] if entry.respond_to?(:name) && entry.name
|
|
59
|
+
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
35
64
|
class MethodParameterBinder
|
|
36
65
|
# @param environment [Rigor::Environment]
|
|
37
66
|
# @param class_path [String, nil] the qualified name of the class
|
|
@@ -112,14 +141,39 @@ module Rigor
|
|
|
112
141
|
|
|
113
142
|
def positional_slots(params_node)
|
|
114
143
|
slots = []
|
|
115
|
-
params_node.requireds.each_with_index
|
|
144
|
+
params_node.requireds.each_with_index do |p, i|
|
|
145
|
+
append_positional_slot(slots, :required_positional, p, i)
|
|
146
|
+
end
|
|
116
147
|
params_node.optionals.each_with_index { |p, i| slots << ParamSlot.new(:optional_positional, p.name, i) }
|
|
117
148
|
rest = params_node.rest
|
|
118
149
|
slots << ParamSlot.new(:rest_positional, rest.name, nil) if rest.respond_to?(:name) && rest&.name
|
|
119
|
-
params_node.posts.each_with_index
|
|
150
|
+
params_node.posts.each_with_index do |p, i|
|
|
151
|
+
append_positional_slot(slots, :trailing_positional, p, i)
|
|
152
|
+
end
|
|
120
153
|
slots
|
|
121
154
|
end
|
|
122
155
|
|
|
156
|
+
# A destructured positional parameter — `def f((a, b))` — is a
|
|
157
|
+
# `Prism::MultiTargetNode` in the `requireds`/`posts` list, not a
|
|
158
|
+
# `RequiredParameterNode`, so it has no `#name`. Bind each leaf
|
|
159
|
+
# sub-target local to `Dynamic[Top]` (a `:destructured_positional`
|
|
160
|
+
# slot with no RBS index) instead of crashing on a blind `.name`.
|
|
161
|
+
# Binding the names at all is what matters: it keeps the
|
|
162
|
+
# destructured locals present in the entry scope so the body's
|
|
163
|
+
# reads of them don't fall through to undefined-local noise. The
|
|
164
|
+
# element types are not cheaply available from the parameter list
|
|
165
|
+
# alone (no RBS function param maps onto a destructured slot), so
|
|
166
|
+
# `Dynamic[Top]` is the sound default.
|
|
167
|
+
def append_positional_slot(slots, kind, param, index)
|
|
168
|
+
if param.is_a?(Prism::MultiTargetNode)
|
|
169
|
+
Destructure.target_names(param).each do |name|
|
|
170
|
+
slots << ParamSlot.new(:destructured_positional, name, nil)
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
slots << ParamSlot.new(kind, param.name, index)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
123
177
|
def keyword_slots(params_node)
|
|
124
178
|
params_node.keywords.filter_map do |kw|
|
|
125
179
|
case kw
|
|
@@ -100,19 +100,62 @@ module Rigor
|
|
|
100
100
|
|
|
101
101
|
def decompose_tuple(tuple, front_count, back_count, rest_present:)
|
|
102
102
|
elements = tuple.elements
|
|
103
|
-
fronts = Array.new(front_count) { |i| elements
|
|
103
|
+
fronts = Array.new(front_count) { |i| slot_type(elements, i) }
|
|
104
104
|
if rest_present
|
|
105
105
|
middle_end = [elements.size - back_count, front_count].max
|
|
106
106
|
middle = elements[front_count...middle_end] || []
|
|
107
107
|
rest_type = Type::Combinator.tuple_of(*middle)
|
|
108
|
-
backs = Array.new(back_count) { |i| elements
|
|
108
|
+
backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
|
|
109
109
|
else
|
|
110
110
|
rest_type = nil
|
|
111
|
-
backs = Array.new(back_count) { |i| elements
|
|
111
|
+
backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
|
|
112
112
|
end
|
|
113
113
|
[fronts, rest_type, backs]
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# The per-slot type for index `i` of a tuple decomposition, FP-safely
|
|
117
|
+
# softened: a missing slot is `nil` (the runtime value of an
|
|
118
|
+
# over-destructured positional), and a PRESENT but nil-bearing slot
|
|
119
|
+
# (`X | nil`) is softened to its non-`nil` part — for a heterogeneous
|
|
120
|
+
# `Tuple` whose optional slot was made optional by flow.
|
|
121
|
+
#
|
|
122
|
+
# Rationale (ADR-57 slice 3 work-item 2): a destructure of a tuple
|
|
123
|
+
# element that flow typed as optional is almost always guarded by a
|
|
124
|
+
# CORRELATED invariant the flow engine cannot prove. The canonical case
|
|
125
|
+
# is haml's `parse_tag`, which returns `[..., last_line || @line.index
|
|
126
|
+
# + 1]` — a 9-tuple whose `last_line` slot widens to `Dynamic[top]?`
|
|
127
|
+
# through a loop-nested destructure; at the call site `..., last_line =
|
|
128
|
+
# parse_tag(text); raise(..., last_line - 1) if parse && value.empty?`
|
|
129
|
+
# the `last_line` is nil ONLY when an earlier element is too, and the
|
|
130
|
+
# guard short-circuits — but that correlation lives across slots, so
|
|
131
|
+
# per-slot flow sees `last_line` as nil-able and `last_line - 1` fires a
|
|
132
|
+
# spurious `possible nil receiver`. Manufacturing a `T?` for every
|
|
133
|
+
# destructured slot frightens working code; FP discipline (the program
|
|
134
|
+
# works) outranks the worst-case per-slot reading, so we drop the `nil`
|
|
135
|
+
# from a destructured slot and keep the non-`nil` constituent (a bare
|
|
136
|
+
# `nil` slot stays `nil` — there is nothing to soften). A pure non-
|
|
137
|
+
# optional element keeps its precise type unchanged.
|
|
138
|
+
def slot_type(elements, index)
|
|
139
|
+
element = elements[index]
|
|
140
|
+
return Type::Combinator.constant_of(nil) if element.nil?
|
|
141
|
+
|
|
142
|
+
soften_optional_slot(element)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def soften_optional_slot(element)
|
|
146
|
+
return element unless element.is_a?(Type::Union)
|
|
147
|
+
return element unless element.members.any? { |m| nil_literal?(m) }
|
|
148
|
+
|
|
149
|
+
non_nil = element.members.reject { |m| nil_literal?(m) }
|
|
150
|
+
return element if non_nil.empty? # a bare `nil` slot: nothing to soften
|
|
151
|
+
|
|
152
|
+
Type::Combinator.union(*non_nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def nil_literal?(member)
|
|
156
|
+
member.is_a?(Type::Constant) && member.value.nil?
|
|
157
|
+
end
|
|
158
|
+
|
|
116
159
|
def decompose_default(front_count, back_count, rest_present:)
|
|
117
160
|
[
|
|
118
161
|
Array.new(front_count) { Type::Combinator.untyped },
|
|
@@ -280,6 +280,148 @@ module Rigor
|
|
|
280
280
|
carriers = kinds.map { |name| Type::Combinator.nominal_of(name) }
|
|
281
281
|
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
282
282
|
end
|
|
283
|
+
|
|
284
|
+
# ----------------------------------------------------------------
|
|
285
|
+
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
286
|
+
#
|
|
287
|
+
# `widen_after_block` above forgets a literal-shape carrier's arity
|
|
288
|
+
# when a captured local is content-mutated inside a block, but it
|
|
289
|
+
# keeps only the SEED's element types — an unsound under-
|
|
290
|
+
# approximation for a non-empty seed (`out = [0]; arr.each { |x|
|
|
291
|
+
# out << x }` types `Array[0]` while the runtime array is
|
|
292
|
+
# `[0, 1, 2, 3]`). Slice C joins the appended/stored element (and
|
|
293
|
+
# key/value) types INTO the continuation collection's parameter, so
|
|
294
|
+
# the result is `Array[0 | Integer]` rather than `Array[0]`.
|
|
295
|
+
#
|
|
296
|
+
# Array content-mutators that append/store ELEMENTS. The appended
|
|
297
|
+
# element type is the call's argument type(s); `[]=`'s value is its
|
|
298
|
+
# LAST argument (the keys precede it). Subset of `ARRAY_MUTATORS`:
|
|
299
|
+
# only the element-INTRODUCING methods (removers / reorderers add no
|
|
300
|
+
# new element evidence and are already covered by the arity-forget).
|
|
301
|
+
ARRAY_CONTENT_ADDERS = %i[
|
|
302
|
+
<< push append prepend unshift concat insert []= fill replace
|
|
303
|
+
].to_set.freeze
|
|
304
|
+
|
|
305
|
+
# Hash content-mutators that store a key→value pair. For `[]=` /
|
|
306
|
+
# `store` the key is the first argument and the value the last.
|
|
307
|
+
HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
|
|
308
|
+
|
|
309
|
+
# String content-mutators that append to the buffer. String carries
|
|
310
|
+
# no element parameter, so these contribute nothing to a join — they
|
|
311
|
+
# are listed so the orchestrator recognises them as content mutators
|
|
312
|
+
# (the binding already widens to `String` via normal typing); the
|
|
313
|
+
# join helpers below short-circuit on a non-collection pre-state.
|
|
314
|
+
STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
|
|
315
|
+
|
|
316
|
+
# Every method name that mutates a captured local's CONTENT — the
|
|
317
|
+
# union the orchestrator scans the block body for.
|
|
318
|
+
CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
|
|
319
|
+
|
|
320
|
+
# The element types a single content-mutator call introduces into an
|
|
321
|
+
# Array, given the per-argument types (already typed in the block
|
|
322
|
+
# body scope). `concat`/`replace` take collection arguments, so their
|
|
323
|
+
# element evidence is the arguments' OWN element types unioned; the
|
|
324
|
+
# rest append the argument values directly. Returns `[]` when no
|
|
325
|
+
# element evidence (e.g. a `<<` with no resolvable arg).
|
|
326
|
+
def array_added_elements(method_name, arg_types)
|
|
327
|
+
return [] if arg_types.empty?
|
|
328
|
+
|
|
329
|
+
case method_name
|
|
330
|
+
when :concat, :replace
|
|
331
|
+
arg_types.flat_map { |t| collection_element_types(t) }
|
|
332
|
+
when :insert
|
|
333
|
+
# `insert(index, *objs)` — first arg is the position.
|
|
334
|
+
arg_types.drop(1)
|
|
335
|
+
when :[]=
|
|
336
|
+
# `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
|
|
337
|
+
[arg_types.last]
|
|
338
|
+
when :fill
|
|
339
|
+
# `fill(value)` — only the no-block single-value form adds a
|
|
340
|
+
# concrete element; block / range forms are conservatively
|
|
341
|
+
# ignored (the arity-forget already widened the binding).
|
|
342
|
+
arg_types.size == 1 ? arg_types : []
|
|
343
|
+
else # << push append prepend unshift
|
|
344
|
+
arg_types
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Builds the continuation Array type from the pre-state binding and
|
|
349
|
+
# the appended element types. The floor is `Array[Dynamic[top]]`
|
|
350
|
+
# (the sound empty-seed behaviour) when there is no element evidence
|
|
351
|
+
# at all.
|
|
352
|
+
def join_array_content(pre_state, added_elements)
|
|
353
|
+
seed_elements = collection_element_types(pre_state)
|
|
354
|
+
added = added_elements.compact
|
|
355
|
+
# The empty-seed floor element is `Dynamic[top]` (no element
|
|
356
|
+
# evidence). When real appended evidence exists that floor carries
|
|
357
|
+
# nothing, so drop it — an empty accumulator built by `out << x*2`
|
|
358
|
+
# reads `Array[Integer]`, not `Array[Integer | Dynamic[top]]`.
|
|
359
|
+
seed_elements = drop_dynamic(seed_elements) unless added.empty?
|
|
360
|
+
elements = seed_elements + added
|
|
361
|
+
return Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped]) if elements.empty?
|
|
362
|
+
|
|
363
|
+
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.union(*elements)])
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Builds the continuation Hash type from the pre-state binding and a
|
|
367
|
+
# list of `[key_type, value_type]` pairs stored by `[]=` / `store`.
|
|
368
|
+
def join_hash_content(pre_state, added_pairs)
|
|
369
|
+
seed_keys, seed_values = hash_shape_key_values(pre_state)
|
|
370
|
+
added_keys = added_pairs.map(&:first).compact
|
|
371
|
+
added_values = added_pairs.map(&:last).compact
|
|
372
|
+
seed_keys = drop_dynamic(seed_keys) unless added_keys.empty?
|
|
373
|
+
seed_values = drop_dynamic(seed_values) unless added_values.empty?
|
|
374
|
+
keys = seed_keys + added_keys
|
|
375
|
+
values = seed_values + added_values
|
|
376
|
+
key_t = keys.empty? ? Type::Combinator.untyped : Type::Combinator.union(*keys)
|
|
377
|
+
value_t = values.empty? ? Type::Combinator.untyped : Type::Combinator.union(*values)
|
|
378
|
+
Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Drops `Dynamic` (incl. `untyped`) constituents from a type list.
|
|
382
|
+
def drop_dynamic(types)
|
|
383
|
+
types.grep_v(Type::Dynamic)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Element types carried by a collection binding, regardless of which
|
|
387
|
+
# carrier holds them: a `Tuple` lists them, a `Nominal[Array, [E]]`
|
|
388
|
+
# has one element param, a bare `Array` / anything else yields none.
|
|
389
|
+
def collection_element_types(type)
|
|
390
|
+
case type
|
|
391
|
+
when Type::Tuple
|
|
392
|
+
type.elements
|
|
393
|
+
when Type::Nominal
|
|
394
|
+
type.class_name == "Array" ? type.type_args : []
|
|
395
|
+
when Type::Union
|
|
396
|
+
# A loop's single-pass join can union the widened collection with
|
|
397
|
+
# its un-widened literal seed (`Array[0] | [0]`); pull element
|
|
398
|
+
# evidence from every Array-ish member.
|
|
399
|
+
type.members.flat_map { |m| collection_element_types(m) }
|
|
400
|
+
else
|
|
401
|
+
[]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# `[keys, values]` evidence from a Hash-ish pre-state binding —
|
|
406
|
+
# a `HashShape` (literal pairs) or a `Nominal[Hash, [K, V]]`.
|
|
407
|
+
def hash_shape_key_values(type)
|
|
408
|
+
case type
|
|
409
|
+
when Type::HashShape
|
|
410
|
+
return [[], []] if type.pairs.empty?
|
|
411
|
+
|
|
412
|
+
[[key_union_for(type.pairs.keys)], type.pairs.values]
|
|
413
|
+
when Type::Nominal
|
|
414
|
+
type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
|
|
415
|
+
when Type::Union
|
|
416
|
+
type.members.each_with_object([[], []]) do |m, (ks, vs)|
|
|
417
|
+
mk, mv = hash_shape_key_values(m)
|
|
418
|
+
ks.concat(mk)
|
|
419
|
+
vs.concat(mv)
|
|
420
|
+
end
|
|
421
|
+
else
|
|
422
|
+
[[], []]
|
|
423
|
+
end
|
|
424
|
+
end
|
|
283
425
|
end
|
|
284
426
|
end
|
|
285
427
|
end
|