rigortype 0.1.8 → 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 +20 -0
- 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.rb +62 -4
- 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 +165 -6
- 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/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 +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/statement_evaluator.rb +3 -1
- 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/triage/catalogue.rb +2 -2
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- metadata +11 -1
|
@@ -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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds the zone-pinned `Time` class constructors on statically
|
|
9
|
+
# known arguments.
|
|
10
|
+
#
|
|
11
|
+
# Only `Time.utc` / `Time.gm` are folded. They pin the result
|
|
12
|
+
# to UTC, so the literal is machine-independent — every reader
|
|
13
|
+
# (`year`, `hour`, `utc_offset`, `strftime`, …) yields the same
|
|
14
|
+
# answer on any analysis host.
|
|
15
|
+
#
|
|
16
|
+
# `Time.now` (non-deterministic), `Time.at` / `Time.local` /
|
|
17
|
+
# `Time.mktime` / `Time.new` (local-zone — the result's wall
|
|
18
|
+
# clock and `utc_offset` depend on the analysis machine's
|
|
19
|
+
# timezone) are deliberately NOT folded; they keep their
|
|
20
|
+
# `Nominal[Time]` RBS answer. For the same reason `Time#getlocal`
|
|
21
|
+
# is blocklisted in `TIME_CATALOG` so a folded `Constant[Time]`
|
|
22
|
+
# cannot produce a machine-dependent local-zone copy.
|
|
23
|
+
module TimeFolding
|
|
24
|
+
TIME_UTC_METHODS = Set[:utc, :gm].freeze
|
|
25
|
+
private_constant :TIME_UTC_METHODS
|
|
26
|
+
|
|
27
|
+
# `Time.utc` accepts the 1–7 positional (year-first) form and
|
|
28
|
+
# a 10-arg (sec-first) form; cap the arity so a malformed
|
|
29
|
+
# call declines cheaply rather than reaching the constructor.
|
|
30
|
+
MAX_TIME_ARITY = 10
|
|
31
|
+
private_constant :MAX_TIME_ARITY
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
36
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
37
|
+
return nil unless dispatch_target?(receiver)
|
|
38
|
+
return nil unless TIME_UTC_METHODS.include?(method_name)
|
|
39
|
+
return nil unless args.size.between?(1, MAX_TIME_ARITY)
|
|
40
|
+
return nil unless args.all?(Type::Constant)
|
|
41
|
+
|
|
42
|
+
values = args.map(&:value)
|
|
43
|
+
return nil unless values.all? { |v| v.is_a?(Integer) || v.is_a?(String) }
|
|
44
|
+
|
|
45
|
+
Type::Combinator.constant_of(Time.utc(*values))
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dispatch_target?(receiver)
|
|
51
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Time"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "../../type"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module Inference
|
|
8
|
+
module MethodDispatcher
|
|
9
|
+
# Folds `URI` module-function calls on statically known
|
|
10
|
+
# string constants.
|
|
11
|
+
#
|
|
12
|
+
# `URI.encode_www_form_component` / `decode_www_form_component`
|
|
13
|
+
# and the newer `encode_uri_component` / `decode_uri_component`
|
|
14
|
+
# are pure, deterministic functions over their string inputs.
|
|
15
|
+
# When the argument is a `Constant[String]`, the analyzer can
|
|
16
|
+
# evaluate the call at inference time and return the concrete
|
|
17
|
+
# `Constant[String]` result.
|
|
18
|
+
#
|
|
19
|
+
# === Supported methods
|
|
20
|
+
#
|
|
21
|
+
# * `encode_www_form_component(str)` / `decode_www_form_component(str)` —
|
|
22
|
+
# RFC 3986 percent-encode / decode. Returns `Constant[String]`.
|
|
23
|
+
# * `encode_uri_component(str)` / `decode_uri_component(str)` —
|
|
24
|
+
# Same encoding but may preserve additional reserved chars
|
|
25
|
+
# (Ruby 3.2+). Returns `Constant[String]`.
|
|
26
|
+
#
|
|
27
|
+
# === Non-constant / unsupported cases
|
|
28
|
+
#
|
|
29
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
30
|
+
# - the receiver is not `Singleton[URI]`,
|
|
31
|
+
# - the first argument is not a `Constant[String]`,
|
|
32
|
+
# - the method is not in the supported set.
|
|
33
|
+
module URIFolding
|
|
34
|
+
URI_COMPONENT_METHODS = Set[
|
|
35
|
+
:encode_www_form_component, :decode_www_form_component,
|
|
36
|
+
:encode_uri_component, :decode_uri_component
|
|
37
|
+
].freeze
|
|
38
|
+
private_constant :URI_COMPONENT_METHODS
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
43
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
44
|
+
return nil unless dispatch_target?(receiver)
|
|
45
|
+
return nil unless URI_COMPONENT_METHODS.include?(method_name)
|
|
46
|
+
|
|
47
|
+
fold_uri_call(method_name, args)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def dispatch_target?(receiver)
|
|
51
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "URI"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def fold_uri_call(method_name, args)
|
|
55
|
+
return nil unless args.size == 1
|
|
56
|
+
|
|
57
|
+
arg = args.first
|
|
58
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
59
|
+
|
|
60
|
+
Type::Combinator.constant_of(URI.public_send(method_name, arg.value))
|
|
61
|
+
rescue StandardError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|