rigortype 0.1.7 → 0.1.9
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 +186 -513
- data/lib/rigor/analysis/check_rules.rb +23 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -3
- data/lib/rigor/analysis/runner.rb +178 -3
- data/lib/rigor/analysis/worker_session.rb +14 -3
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +71 -5
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +300 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +179 -4
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +43 -21
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/scope.rb +46 -0
- data/lib/rigor/triage/catalogue.rb +296 -0
- data/lib/rigor/triage/hint.rb +27 -0
- data/lib/rigor/triage.rb +89 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- metadata +16 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds `Regexp` class-method calls on statically known arguments.
|
|
9
|
+
#
|
|
10
|
+
# === Supported methods
|
|
11
|
+
#
|
|
12
|
+
# * `escape(str)` / `quote(str)` — escapes regexp meta-characters.
|
|
13
|
+
# Returns `Constant[String]`.
|
|
14
|
+
# * `new(str)` / `new(str, opts)` — constructs a Regexp at fold time
|
|
15
|
+
# when the pattern argument is a `Constant[String]`. The optional
|
|
16
|
+
# second argument may be a `Constant[Integer]` (flag bits), a
|
|
17
|
+
# `Constant[true/false]` (IGNORECASE shorthand), or absent.
|
|
18
|
+
# Returns `Constant[Regexp]`.
|
|
19
|
+
#
|
|
20
|
+
# === Non-constant / unsupported cases
|
|
21
|
+
#
|
|
22
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
23
|
+
# - the receiver is not `Singleton[Regexp]`,
|
|
24
|
+
# - the required pattern argument is not a `Constant[String]`,
|
|
25
|
+
# - the method is not in the supported set.
|
|
26
|
+
module RegexpFolding
|
|
27
|
+
REGEXP_ESCAPE_METHODS = Set[:escape, :quote].freeze
|
|
28
|
+
private_constant :REGEXP_ESCAPE_METHODS
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
33
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
34
|
+
return nil unless dispatch_target?(receiver)
|
|
35
|
+
return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
|
|
36
|
+
return fold_new(args) if method_name == :new
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dispatch_target?(receiver)
|
|
42
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Regexp"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `Regexp.escape(str)` / `.quote(str)` — one String arg.
|
|
46
|
+
def fold_escape(args)
|
|
47
|
+
return nil unless args.size == 1
|
|
48
|
+
|
|
49
|
+
arg = args.first
|
|
50
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
51
|
+
|
|
52
|
+
Type::Combinator.constant_of(Regexp.escape(arg.value))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — constructs
|
|
56
|
+
# the pattern at inference time. Delegates to Ruby's real
|
|
57
|
+
# `Regexp.new` so all option forms (Integer flags, `true`/`false`,
|
|
58
|
+
# option strings) are handled without case-analysis; non-constant or
|
|
59
|
+
# invalid arguments decline through to the RBS tier.
|
|
60
|
+
def fold_new(args)
|
|
61
|
+
return nil if args.empty? || args.size > 2
|
|
62
|
+
|
|
63
|
+
pattern_arg = args.first
|
|
64
|
+
return nil unless pattern_arg.is_a?(Type::Constant) &&
|
|
65
|
+
pattern_arg.value.is_a?(String)
|
|
66
|
+
|
|
67
|
+
opts = args.size == 2 ? constant_value_or_nil(args[1]) : 0
|
|
68
|
+
return nil if args.size == 2 && opts.nil?
|
|
69
|
+
|
|
70
|
+
Type::Combinator.constant_of(Regexp.new(pattern_arg.value, opts))
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def constant_value_or_nil(type)
|
|
76
|
+
type.is_a?(Type::Constant) ? type.value : nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds `Set` constructor calls into `Constant[Set]` when all
|
|
9
|
+
# arguments are statically known.
|
|
10
|
+
#
|
|
11
|
+
# Ruby 3.2+ implements Set in C (`set.c`). The constructor is
|
|
12
|
+
# deterministic — it reads only its arguments and writes nothing
|
|
13
|
+
# to global state.
|
|
14
|
+
#
|
|
15
|
+
# === Supported methods
|
|
16
|
+
#
|
|
17
|
+
# * `Set[]` — variadic constructor; each argument must be a
|
|
18
|
+
# `Constant[T]`. Returns `Constant[Set]`.
|
|
19
|
+
# * `Set.new` — zero-argument form; returns `Constant[Set.new]`.
|
|
20
|
+
# * `Set.new(tuple)` — the argument must be a `Tuple` whose
|
|
21
|
+
# every element is a `Constant[T]`. Returns `Constant[Set]`.
|
|
22
|
+
#
|
|
23
|
+
# === Non-constant / unsupported cases
|
|
24
|
+
#
|
|
25
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
26
|
+
# - the receiver is not `Singleton[Set]`,
|
|
27
|
+
# - the method is not `[]` or `new`,
|
|
28
|
+
# - any argument is non-constant or structurally opaque.
|
|
29
|
+
module SetFolding
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
33
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
34
|
+
return nil unless dispatch_target?(receiver)
|
|
35
|
+
|
|
36
|
+
case method_name
|
|
37
|
+
when :[] then fold_bracket(args)
|
|
38
|
+
when :new then fold_new(args)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def dispatch_target?(receiver)
|
|
43
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Set"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `Set["a", "b", "c"]` — all positional args must be Constant.
|
|
47
|
+
def fold_bracket(args)
|
|
48
|
+
values = args.map do |a|
|
|
49
|
+
return nil unless a.is_a?(Type::Constant)
|
|
50
|
+
|
|
51
|
+
a.value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `Set.new` / `Set.new(tuple_of_constants)`.
|
|
60
|
+
def fold_new(args)
|
|
61
|
+
return Type::Combinator.constant_of(::Set.new) if args.empty?
|
|
62
|
+
return nil if args.size > 1
|
|
63
|
+
|
|
64
|
+
arg = args.first
|
|
65
|
+
values = case arg
|
|
66
|
+
when Type::Tuple
|
|
67
|
+
return nil unless arg.elements.all?(Type::Constant)
|
|
68
|
+
|
|
69
|
+
arg.elements.map(&:value)
|
|
70
|
+
else
|
|
71
|
+
return nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
75
|
+
rescue StandardError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -87,7 +87,11 @@ module Rigor
|
|
|
87
87
|
zip: :tuple_zip,
|
|
88
88
|
:[] => :tuple_index,
|
|
89
89
|
fetch: :tuple_index,
|
|
90
|
-
dig: :tuple_dig
|
|
90
|
+
dig: :tuple_dig,
|
|
91
|
+
values_at: :tuple_values_at,
|
|
92
|
+
:+ => :tuple_concat,
|
|
93
|
+
compact: :tuple_compact,
|
|
94
|
+
take: :tuple_take
|
|
91
95
|
}.freeze
|
|
92
96
|
|
|
93
97
|
HASH_SHAPE_HANDLERS = {
|
|
@@ -96,19 +100,41 @@ module Rigor
|
|
|
96
100
|
count: :hash_size,
|
|
97
101
|
empty?: :hash_empty?,
|
|
98
102
|
any?: :hash_any?,
|
|
103
|
+
none?: :hash_none?,
|
|
104
|
+
one?: :hash_one?,
|
|
99
105
|
keys: :hash_keys,
|
|
100
106
|
values: :hash_values,
|
|
101
107
|
first: :hash_first,
|
|
102
108
|
flatten: :hash_flatten,
|
|
103
109
|
compact: :hash_compact,
|
|
104
110
|
to_a: :hash_to_a,
|
|
111
|
+
entries: :hash_to_a,
|
|
105
112
|
to_h: :hash_to_h,
|
|
113
|
+
to_hash: :hash_to_h,
|
|
114
|
+
deconstruct_keys: :hash_deconstruct_keys,
|
|
106
115
|
invert: :hash_invert,
|
|
107
116
|
merge: :hash_merge,
|
|
117
|
+
slice: :hash_slice,
|
|
118
|
+
except: :hash_except,
|
|
108
119
|
:[] => :hash_lookup,
|
|
109
120
|
fetch: :hash_lookup,
|
|
110
121
|
dig: :hash_dig,
|
|
111
|
-
values_at: :hash_values_at
|
|
122
|
+
values_at: :hash_values_at,
|
|
123
|
+
fetch_values: :hash_fetch_values,
|
|
124
|
+
assoc: :hash_assoc,
|
|
125
|
+
key: :hash_key,
|
|
126
|
+
has_key?: :hash_has_key?,
|
|
127
|
+
key?: :hash_has_key?,
|
|
128
|
+
member?: :hash_has_key?,
|
|
129
|
+
include?: :hash_has_key?,
|
|
130
|
+
has_value?: :hash_has_value?,
|
|
131
|
+
value?: :hash_has_value?,
|
|
132
|
+
default: :hash_default,
|
|
133
|
+
default_proc: :hash_default,
|
|
134
|
+
:< => :hash_compare,
|
|
135
|
+
:<= => :hash_compare,
|
|
136
|
+
:> => :hash_compare,
|
|
137
|
+
:>= => :hash_compare
|
|
112
138
|
}.freeze
|
|
113
139
|
|
|
114
140
|
# @return [Rigor::Type, nil] the precise element/value type, or
|
|
@@ -654,6 +680,67 @@ module Rigor
|
|
|
654
680
|
[key.value, value]
|
|
655
681
|
end
|
|
656
682
|
|
|
683
|
+
# `tuple.values_at(i1, i2, ...)` — returns a Tuple of
|
|
684
|
+
# per-index elements. Each argument must be a
|
|
685
|
+
# `Constant[Integer]`. Out-of-range indices fill with
|
|
686
|
+
# `Constant[nil]`, mirroring Ruby's runtime behaviour.
|
|
687
|
+
# Declines when any argument is non-static.
|
|
688
|
+
def tuple_values_at(tuple, _method_name, args)
|
|
689
|
+
return nil if args.empty?
|
|
690
|
+
|
|
691
|
+
values = args.map do |arg|
|
|
692
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
693
|
+
return nil unless arg.value.is_a?(Integer)
|
|
694
|
+
|
|
695
|
+
idx = normalise_index(arg.value, tuple.elements.size)
|
|
696
|
+
idx ? tuple.elements[idx] : Type::Combinator.constant_of(nil)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
Type::Combinator.tuple_of(*values)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# `tuple + other` — concatenates two Tuples. Both sides
|
|
703
|
+
# must be `Type::Tuple`. Returns a new Tuple whose
|
|
704
|
+
# elements are those of the receiver followed by those
|
|
705
|
+
# of the argument.
|
|
706
|
+
def tuple_concat(tuple, _method_name, args)
|
|
707
|
+
return nil unless args.size == 1
|
|
708
|
+
|
|
709
|
+
other = args.first
|
|
710
|
+
return nil unless other.is_a?(Type::Tuple)
|
|
711
|
+
|
|
712
|
+
Type::Combinator.tuple_of(*tuple.elements, *other.elements)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# `tuple.compact` — removes every element that is
|
|
716
|
+
# `Constant[nil]`. Folds only when every element is a
|
|
717
|
+
# `Constant` (so the nil set is decidable). Mixed-shape
|
|
718
|
+
# elements decline so the RBS tier widens.
|
|
719
|
+
def tuple_compact(tuple, _method_name, args)
|
|
720
|
+
return nil unless args.empty?
|
|
721
|
+
return nil unless tuple.elements.all?(Type::Constant)
|
|
722
|
+
|
|
723
|
+
kept = tuple.elements.reject { |e| e.is_a?(Type::Constant) && e.value.nil? }
|
|
724
|
+
Type::Combinator.tuple_of(*kept)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# `tuple.take(n)` — returns the first n elements as a
|
|
728
|
+
# new Tuple. The argument must be a `Constant[Integer]`.
|
|
729
|
+
# n <= 0 returns the empty Tuple; n >= size returns the
|
|
730
|
+
# full receiver.
|
|
731
|
+
def tuple_take(tuple, _method_name, args)
|
|
732
|
+
return nil unless args.size == 1
|
|
733
|
+
|
|
734
|
+
arg = args.first
|
|
735
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
736
|
+
return nil unless arg.value.is_a?(Integer)
|
|
737
|
+
|
|
738
|
+
n = arg.value
|
|
739
|
+
return Type::Combinator.tuple_of if n <= 0
|
|
740
|
+
|
|
741
|
+
Type::Combinator.tuple_of(*tuple.elements.take(n))
|
|
742
|
+
end
|
|
743
|
+
|
|
657
744
|
# Returns `true` / `false` if every element's truthiness
|
|
658
745
|
# agrees, nil for mixed-or-unknown shapes. `all: true`
|
|
659
746
|
# checks every element is truthy; `all: false` checks
|
|
@@ -817,6 +904,185 @@ module Rigor
|
|
|
817
904
|
|
|
818
905
|
Type::Combinator.constant_of(!shape.pairs.empty?)
|
|
819
906
|
end
|
|
907
|
+
|
|
908
|
+
# `shape.none?` (no block, no arg) — mirror of `any?`.
|
|
909
|
+
# Folds to `Constant[shape.pairs.empty?]` for closed
|
|
910
|
+
# shapes with no optional keys.
|
|
911
|
+
def hash_none?(shape, _method_name, args)
|
|
912
|
+
return nil unless args.empty?
|
|
913
|
+
return nil unless shape.closed?
|
|
914
|
+
return nil unless shape.optional_keys.empty?
|
|
915
|
+
|
|
916
|
+
Type::Combinator.constant_of(shape.pairs.empty?)
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# `shape.one?` (no block, no arg) — folds to
|
|
920
|
+
# `Constant[shape.pairs.size == 1]` for a closed shape
|
|
921
|
+
# with no optional keys.
|
|
922
|
+
def hash_one?(shape, _method_name, args)
|
|
923
|
+
return nil unless args.empty?
|
|
924
|
+
return nil unless shape.closed?
|
|
925
|
+
return nil unless shape.optional_keys.empty?
|
|
926
|
+
|
|
927
|
+
Type::Combinator.constant_of(shape.pairs.size == 1)
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# `shape.deconstruct_keys(keys)` — Ruby's `Hash#deconstruct_keys`
|
|
931
|
+
# returns the receiver itself regardless of the `keys`
|
|
932
|
+
# argument, so the precise answer is the shape unchanged.
|
|
933
|
+
def hash_deconstruct_keys(shape, _method_name, args)
|
|
934
|
+
return nil unless args.size == 1
|
|
935
|
+
|
|
936
|
+
shape
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# `shape.fetch_values(:a, :b, ...)` — like `values_at` but
|
|
940
|
+
# raises `KeyError` on a missing key. Folds to `Tuple[V…]`
|
|
941
|
+
# only when every requested key is present; a missing key
|
|
942
|
+
# declines so the RBS tier reflects the raise.
|
|
943
|
+
def hash_fetch_values(shape, _method_name, args)
|
|
944
|
+
return nil if args.empty?
|
|
945
|
+
return nil unless shape.closed?
|
|
946
|
+
return nil unless shape.optional_keys.empty?
|
|
947
|
+
|
|
948
|
+
values = []
|
|
949
|
+
args.each do |arg|
|
|
950
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
951
|
+
|
|
952
|
+
key = arg.value
|
|
953
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
954
|
+
return nil unless shape.pairs.key?(key)
|
|
955
|
+
|
|
956
|
+
values << shape.pairs[key]
|
|
957
|
+
end
|
|
958
|
+
Type::Combinator.tuple_of(*values)
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
# `shape.assoc(key)` — returns `Tuple[Constant[k], V]` for a
|
|
962
|
+
# known key, `Constant[nil]` for a missing key.
|
|
963
|
+
def hash_assoc(shape, _method_name, args)
|
|
964
|
+
return nil unless args.size == 1
|
|
965
|
+
return nil unless shape.closed?
|
|
966
|
+
return nil unless shape.optional_keys.empty?
|
|
967
|
+
|
|
968
|
+
arg = args.first
|
|
969
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
970
|
+
|
|
971
|
+
key = arg.value
|
|
972
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
973
|
+
return Type::Combinator.constant_of(nil) unless shape.pairs.key?(key)
|
|
974
|
+
|
|
975
|
+
Type::Combinator.tuple_of(Type::Combinator.constant_of(key), shape.pairs[key])
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
# `shape.key(value)` — reverse lookup. Folds when every
|
|
979
|
+
# value is a `Constant` so equality is decidable: returns
|
|
980
|
+
# `Constant[k]` for the first matching key, `Constant[nil]`
|
|
981
|
+
# when no value matches.
|
|
982
|
+
def hash_key(shape, _method_name, args)
|
|
983
|
+
return nil unless args.size == 1
|
|
984
|
+
return nil unless shape.closed?
|
|
985
|
+
return nil unless shape.optional_keys.empty?
|
|
986
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
987
|
+
|
|
988
|
+
arg = args.first
|
|
989
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
990
|
+
|
|
991
|
+
pair = shape.pairs.find { |_k, v| v.value == arg.value }
|
|
992
|
+
Type::Combinator.constant_of(pair&.first)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# `shape.has_value?(v)` / `value?(v)` — folds to
|
|
996
|
+
# `Constant[true/false]` when every value is a `Constant`
|
|
997
|
+
# (so equality is decidable) and the argument is a
|
|
998
|
+
# `Constant`.
|
|
999
|
+
def hash_has_value?(shape, _method_name, args)
|
|
1000
|
+
return nil unless args.size == 1
|
|
1001
|
+
return nil unless shape.closed?
|
|
1002
|
+
return nil unless shape.optional_keys.empty?
|
|
1003
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
1004
|
+
|
|
1005
|
+
arg = args.first
|
|
1006
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
1007
|
+
|
|
1008
|
+
found = shape.pairs.values.any? { |v| v.value == arg.value }
|
|
1009
|
+
Type::Combinator.constant_of(found)
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
# `shape.default` / `default_proc` — a literal `HashShape`
|
|
1013
|
+
# carries no default value or proc, so both fold to
|
|
1014
|
+
# `Constant[nil]`. `default` accepts an optional key
|
|
1015
|
+
# argument (still returns the default), `default_proc`
|
|
1016
|
+
# takes none — the `args.size <= 1` guard covers both.
|
|
1017
|
+
def hash_default(shape, _method_name, args)
|
|
1018
|
+
return nil unless args.size <= 1
|
|
1019
|
+
return nil unless shape.closed?
|
|
1020
|
+
|
|
1021
|
+
Type::Combinator.constant_of(nil)
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
# `shape < other` / `<=` / `>` / `>=` — Hash containment
|
|
1025
|
+
# comparison. Both sides must be closed `HashShape`s whose
|
|
1026
|
+
# values are all `Constant` (so pair equality is
|
|
1027
|
+
# decidable). `<` / `>` are proper-subset / -superset.
|
|
1028
|
+
def hash_compare(shape, method_name, args)
|
|
1029
|
+
return nil unless args.size == 1
|
|
1030
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
1031
|
+
|
|
1032
|
+
other = args.first
|
|
1033
|
+
return nil unless other.is_a?(Type::HashShape)
|
|
1034
|
+
return nil unless other.closed? && other.optional_keys.empty?
|
|
1035
|
+
|
|
1036
|
+
left = constant_pairs(shape)
|
|
1037
|
+
right = constant_pairs(other)
|
|
1038
|
+
return nil if left.nil? || right.nil?
|
|
1039
|
+
|
|
1040
|
+
Type::Combinator.constant_of(hash_containment(method_name, left, right))
|
|
1041
|
+
end
|
|
1042
|
+
|
|
1043
|
+
# Unwraps a closed shape's pairs to a plain Ruby Hash of
|
|
1044
|
+
# `key => value` for value-equality comparison. Returns nil
|
|
1045
|
+
# when any value is not a `Constant`.
|
|
1046
|
+
def constant_pairs(shape)
|
|
1047
|
+
return nil unless shape.pairs.values.all?(Type::Constant)
|
|
1048
|
+
|
|
1049
|
+
shape.pairs.transform_values(&:value)
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
def hash_containment(method_name, left, right)
|
|
1053
|
+
case method_name
|
|
1054
|
+
when :< then hash_proper_subset?(left, right)
|
|
1055
|
+
when :<= then hash_subset?(left, right)
|
|
1056
|
+
when :> then hash_proper_subset?(right, left)
|
|
1057
|
+
when :>= then hash_subset?(right, left)
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
def hash_subset?(left, right)
|
|
1062
|
+
left.all? { |k, v| right.key?(k) && right[k] == v }
|
|
1063
|
+
end
|
|
1064
|
+
|
|
1065
|
+
def hash_proper_subset?(left, right)
|
|
1066
|
+
left.size < right.size && hash_subset?(left, right)
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
# `shape.has_key?(k)` / `key?(k)` / `member?(k)` /
|
|
1070
|
+
# `include?(k)` — folds to `Constant[true/false]` when
|
|
1071
|
+
# the argument is a `Constant[Symbol|String]` and the
|
|
1072
|
+
# shape is closed with no optional keys.
|
|
1073
|
+
def hash_has_key?(shape, _method_name, args)
|
|
1074
|
+
return nil unless args.size == 1
|
|
1075
|
+
return nil unless shape.closed?
|
|
1076
|
+
return nil unless shape.optional_keys.empty?
|
|
1077
|
+
|
|
1078
|
+
arg = args.first
|
|
1079
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
1080
|
+
|
|
1081
|
+
key = arg.value
|
|
1082
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
1083
|
+
|
|
1084
|
+
Type::Combinator.constant_of(shape.pairs.key?(key))
|
|
1085
|
+
end
|
|
820
1086
|
# rubocop:enable Style/ReturnNilInPredicateMethodDefinition
|
|
821
1087
|
|
|
822
1088
|
# `shape.keys` — returns a `Tuple[Constant<k>…]` for a
|
|
@@ -1027,6 +1293,54 @@ module Rigor
|
|
|
1027
1293
|
Type::Combinator.tuple_of(*values)
|
|
1028
1294
|
end
|
|
1029
1295
|
|
|
1296
|
+
# `shape.slice(:a, :b, ...)` — returns a sub-HashShape
|
|
1297
|
+
# containing only the specified keys. All arguments must
|
|
1298
|
+
# be `Constant[Symbol|String]`. Keys not present in the
|
|
1299
|
+
# shape are silently omitted (matching Ruby's runtime
|
|
1300
|
+
# semantics — no nil padding). Declines on open shapes
|
|
1301
|
+
# or when any argument is not a static key.
|
|
1302
|
+
def hash_slice(shape, _method_name, args)
|
|
1303
|
+
return nil if args.empty?
|
|
1304
|
+
return nil unless shape.closed?
|
|
1305
|
+
return nil unless shape.optional_keys.empty?
|
|
1306
|
+
|
|
1307
|
+
requested = []
|
|
1308
|
+
args.each do |arg|
|
|
1309
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
1310
|
+
|
|
1311
|
+
key = arg.value
|
|
1312
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
1313
|
+
|
|
1314
|
+
requested << key
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
Type::Combinator.hash_shape_of(shape.pairs.slice(*requested))
|
|
1318
|
+
end
|
|
1319
|
+
|
|
1320
|
+
# `shape.except(:a, :b, ...)` — returns a sub-HashShape
|
|
1321
|
+
# with the specified keys removed. All arguments must be
|
|
1322
|
+
# `Constant[Symbol|String]`. Keys not present in the shape
|
|
1323
|
+
# are silently ignored. Declines on open shapes or when
|
|
1324
|
+
# any argument is not a static key.
|
|
1325
|
+
def hash_except(shape, _method_name, args)
|
|
1326
|
+
return nil if args.empty?
|
|
1327
|
+
return nil unless shape.closed?
|
|
1328
|
+
return nil unless shape.optional_keys.empty?
|
|
1329
|
+
|
|
1330
|
+
excluded = {}
|
|
1331
|
+
args.each do |arg|
|
|
1332
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
1333
|
+
|
|
1334
|
+
key = arg.value
|
|
1335
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
1336
|
+
|
|
1337
|
+
excluded[key] = true
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
kept = shape.pairs.reject { |k, _v| excluded.key?(k) }
|
|
1341
|
+
Type::Combinator.hash_shape_of(kept)
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1030
1344
|
# Continues a `dig` chain after the first step. Tuple and
|
|
1031
1345
|
# HashShape members re-dispatch into the catalogue;
|
|
1032
1346
|
# `Constant[nil]` short-circuits the chain (Hash#dig and
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Folds `Shellwords` module-function calls on statically known
|
|
10
|
+
# string constants.
|
|
11
|
+
#
|
|
12
|
+
# `Shellwords` is a pure, side-effect-free module whose functions
|
|
13
|
+
# are deterministic over their inputs — the same string always
|
|
14
|
+
# produces the same escaped / split / joined result. When all
|
|
15
|
+
# relevant arguments are `Constant[String]` (or `Tuple` of them
|
|
16
|
+
# for `join`), the analyzer can evaluate the call at inference
|
|
17
|
+
# time and return the concrete `Constant<T>` result.
|
|
18
|
+
#
|
|
19
|
+
# === Supported methods
|
|
20
|
+
#
|
|
21
|
+
# * `escape` / `shellescape(str)` — returns `Constant[String]`.
|
|
22
|
+
# `Shellwords.escape("")` → `"''"`, so the result is always
|
|
23
|
+
# non-empty; callers that care about the `non-empty-string`
|
|
24
|
+
# refinement will get it for free once that carrier is wired
|
|
25
|
+
# to scalar-constant returns.
|
|
26
|
+
#
|
|
27
|
+
# * `split` / `shellsplit` / `shellwords(line)` — splits the
|
|
28
|
+
# shell command line into tokens, returns
|
|
29
|
+
# `Tuple[Constant[String], …]`. Raises `ArgumentError` on
|
|
30
|
+
# unmatched quotes; the handler declines (returns `nil`) so
|
|
31
|
+
# the RBS tier widens gracefully.
|
|
32
|
+
#
|
|
33
|
+
# * `join` / `shelljoin(array)` — joins an array of tokens into
|
|
34
|
+
# a shell-safe string. Requires a `Tuple` whose every element
|
|
35
|
+
# is a `Constant[String]`; returns `Constant[String]`.
|
|
36
|
+
#
|
|
37
|
+
# === Non-constant / unsupported cases
|
|
38
|
+
#
|
|
39
|
+
# Any call where:
|
|
40
|
+
# - the receiver is not `Singleton[Shellwords]`,
|
|
41
|
+
# - the required argument is not a `Constant[String]` (or
|
|
42
|
+
# `Tuple[Constant[String]…]` for `join`),
|
|
43
|
+
# - the method is not in the supported set, or
|
|
44
|
+
# - `Shellwords.split` raises on malformed input
|
|
45
|
+
#
|
|
46
|
+
# returns `nil`, deferring to the next dispatcher tier.
|
|
47
|
+
module ShellwordsFolding
|
|
48
|
+
SHELLWORDS_ESCAPE_METHODS = Set[:escape, :shellescape].freeze
|
|
49
|
+
SHELLWORDS_SPLIT_METHODS = Set[:split, :shellsplit, :shellwords].freeze
|
|
50
|
+
SHELLWORDS_JOIN_METHODS = Set[:join, :shelljoin].freeze
|
|
51
|
+
SHELLWORDS_ALL_METHODS = (
|
|
52
|
+
SHELLWORDS_ESCAPE_METHODS | SHELLWORDS_SPLIT_METHODS | SHELLWORDS_JOIN_METHODS
|
|
53
|
+
).freeze
|
|
54
|
+
|
|
55
|
+
# Maximum number of tokens `split` may produce before we
|
|
56
|
+
# decline the fold (same rationale as STRING_ARRAY_LIFT_LIMIT
|
|
57
|
+
# in ConstantFolding — keep the result Tuple manageable).
|
|
58
|
+
SHELLWORDS_SPLIT_LIMIT = 64
|
|
59
|
+
|
|
60
|
+
private_constant :SHELLWORDS_ESCAPE_METHODS, :SHELLWORDS_SPLIT_METHODS,
|
|
61
|
+
:SHELLWORDS_JOIN_METHODS, :SHELLWORDS_ALL_METHODS,
|
|
62
|
+
:SHELLWORDS_SPLIT_LIMIT
|
|
63
|
+
|
|
64
|
+
module_function
|
|
65
|
+
|
|
66
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
67
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
68
|
+
return nil unless dispatch_target?(receiver)
|
|
69
|
+
return nil unless SHELLWORDS_ALL_METHODS.include?(method_name)
|
|
70
|
+
|
|
71
|
+
if SHELLWORDS_ESCAPE_METHODS.include?(method_name)
|
|
72
|
+
fold_escape(args)
|
|
73
|
+
elsif SHELLWORDS_SPLIT_METHODS.include?(method_name)
|
|
74
|
+
fold_split(args)
|
|
75
|
+
else
|
|
76
|
+
fold_join(args)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def dispatch_target?(receiver)
|
|
81
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Shellwords"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `Shellwords.escape(str)` / `.shellescape(str)` — one String arg.
|
|
85
|
+
def fold_escape(args)
|
|
86
|
+
return nil unless args.size == 1
|
|
87
|
+
|
|
88
|
+
arg = args.first
|
|
89
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
90
|
+
|
|
91
|
+
Type::Combinator.constant_of(Shellwords.escape(arg.value))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# `Shellwords.split(line)` / `.shellsplit` / `.shellwords` —
|
|
95
|
+
# one String arg, result lifted to Tuple[Constant[String]…].
|
|
96
|
+
def fold_split(args)
|
|
97
|
+
return nil unless args.size == 1
|
|
98
|
+
|
|
99
|
+
arg = args.first
|
|
100
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
101
|
+
|
|
102
|
+
tokens = Shellwords.split(arg.value)
|
|
103
|
+
return nil if tokens.size > SHELLWORDS_SPLIT_LIMIT
|
|
104
|
+
|
|
105
|
+
Type::Combinator.tuple_of(*tokens.map { |t| Type::Combinator.constant_of(t) })
|
|
106
|
+
rescue ArgumentError
|
|
107
|
+
# Unmatched quotes / invalid shell syntax — decline so the
|
|
108
|
+
# RBS tier returns the widened Array[String] envelope.
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# `Shellwords.join(array)` / `.shelljoin` — one Tuple arg
|
|
113
|
+
# whose every element is `Constant[String]`.
|
|
114
|
+
def fold_join(args)
|
|
115
|
+
return nil unless args.size == 1
|
|
116
|
+
|
|
117
|
+
arg = args.first
|
|
118
|
+
return nil unless arg.is_a?(Type::Tuple)
|
|
119
|
+
return nil unless arg.elements.all? { |e| e.is_a?(Type::Constant) && e.value.is_a?(String) }
|
|
120
|
+
|
|
121
|
+
Type::Combinator.constant_of(Shellwords.join(arg.elements.map(&:value)))
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|