-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
/
typed_enumerable.rb
190 lines (175 loc) 路 5.74 KB
/
typed_enumerable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# frozen_string_literal: true
# typed: true
module T::Types
# Note: All subclasses of Enumerable should add themselves to the
# `case` statement below in `describe_obj` in order to get better
# error messages.
class TypedEnumerable < Base
def initialize(type)
@inner_type = type
end
def type
@type ||= T::Utils.coerce(@inner_type)
end
def build_type
type
nil
end
def underlying_class
Enumerable
end
# overrides Base
def name
"T::Enumerable[#{type.name}]"
end
# overrides Base
def valid?(obj)
obj.is_a?(Enumerable)
end
# overrides Base
def recursively_valid?(obj)
return false unless obj.is_a?(Enumerable)
case obj
when Array
begin
it = 0
while it < obj.count
return false unless type.recursively_valid?(obj[it])
it += 1
end
true
end
when Hash
return false unless type.is_a?(FixedArray)
types = type.types
return false if types.count != 2
key_type = types[0]
value_type = types[1]
obj.each_pair do |key, val|
# Some objects (I'm looking at you Rack::Utils::HeaderHash) don't
# iterate over a [key, value] array, so we can't just use the type.recursively_valid?(v)
return false if !key_type.recursively_valid?(key) || !value_type.recursively_valid?(val)
end
true
when Enumerator::Lazy
# Enumerators can be unbounded: see `[:foo, :bar].cycle`
true
when Enumerator::Chain
# Enumerators can be unbounded: see `[:foo, :bar].cycle`
true
when Enumerator
# Enumerators can be unbounded: see `[:foo, :bar].cycle`
true
when Range
# A nil beginning or a nil end does not provide any type information. That is, nil in a range represents
# boundlessness, it does not express a type. For example `(nil...nil)` is not a T::Range[NilClass], its a range
# of unknown types (T::Range[T.untyped]).
# Similarly, `(nil...1)` is not a `T::Range[T.nilable(Integer)]`, it's a boundless range of Integer.
(obj.begin.nil? || type.recursively_valid?(obj.begin)) && (obj.end.nil? || type.recursively_valid?(obj.end))
when Set
obj.each do |item|
return false unless type.recursively_valid?(item)
end
true
else
# We don't check the enumerable since it isn't guaranteed to be
# rewindable (e.g. STDIN) and it may be expensive to enumerate
# (e.g. an enumerator that makes an HTTP request)"
true
end
end
# overrides Base
private def subtype_of_single?(other)
if other.class <= TypedEnumerable &&
underlying_class <= other.underlying_class
# Enumerables are covariant because they are read only
#
# Properly speaking, many Enumerable subtypes (e.g. Set)
# should be invariant because they are mutable and support
# both reading and writing. However, Sorbet treats *all*
# Enumerable subclasses as covariant for ease of adoption.
type.subtype_of?(other.type)
elsif other.class <= Simple
underlying_class <= other.raw_type
else
false
end
end
# overrides Base
def describe_obj(obj)
return super unless obj.is_a?(Enumerable)
type_from_instance(obj).name
end
private def type_from_instances(objs)
return objs.class unless objs.is_a?(Enumerable)
obtained_types = []
begin
objs.each do |x|
obtained_types << type_from_instance(x)
end
rescue
return T.untyped # all we can do is go with the types we have so far
end
if obtained_types.count > 1
# Multiple kinds of bad types showed up, we'll suggest a union
# type you might want.
Union.new(obtained_types)
elsif obtained_types.empty?
T.noreturn
else
# Everything was the same bad type, lets just show that
obtained_types.first
end
end
private def type_from_instance(obj)
if [true, false].include?(obj)
return T::Boolean
elsif !obj.is_a?(Enumerable)
return obj.class
end
case obj
when Array
T::Array[type_from_instances(obj)]
when Hash
inferred_key = type_from_instances(obj.keys)
inferred_val = type_from_instances(obj.values)
T::Hash[inferred_key, inferred_val]
when Range
# We can't get any information from `NilClass` in ranges (since nil is used to represent boundlessness).
typeable_objects = [obj.begin, obj.end].compact
if typeable_objects.empty?
T::Range[T.untyped]
else
T::Range[type_from_instances(typeable_objects)]
end
when Enumerator::Lazy
T::Enumerator::Lazy[type_from_instances(obj)]
when Enumerator::Chain
T::Enumerator::Chain[type_from_instances(obj)]
when Enumerator
T::Enumerator[type_from_instances(obj)]
when Set
T::Set[type_from_instances(obj)]
when IO
# Short circuit for anything IO-like (File, etc.). In these cases,
# enumerating the object is a destructive operation and might hang.
obj.class
else
# This is a specialized enumerable type, just return the class.
if T::Configuration::AT_LEAST_RUBY_2_7
Object.instance_method(:class).bind_call(obj)
else
Object.instance_method(:class).bind(obj).call # rubocop:disable Performance/BindCall
end
end
end
class Untyped < TypedEnumerable
def initialize
super(T.untyped)
end
def valid?(obj)
obj.is_a?(Enumerable)
end
end
end
end