forked from castwide/solargraph
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomplex_type.rb
397 lines (347 loc) · 11.5 KB
/
complex_type.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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# frozen_string_literal: true
module Solargraph
# A container for type data based on YARD type tags.
#
class ComplexType
GENERIC_TAG_NAME = 'generic'.freeze
# @!parse
# include TypeMethods
autoload :TypeMethods, 'solargraph/complex_type/type_methods'
autoload :UniqueType, 'solargraph/complex_type/unique_type'
# @param types [Array<UniqueType, ComplexType>]
def initialize types = [UniqueType::UNDEFINED]
# @todo @items here should not need an annotation
# @type [Array<UniqueType>]
@items = types.flat_map(&:items).uniq(&:to_s)
end
def eql?(other)
self.class == other.class &&
@items == other.items
end
def ==(other)
self.eql?(other)
end
def hash
[self.class, @items].hash
end
# @param api_map [ApiMap]
# @param context [String]
# @return [ComplexType]
def qualify api_map, context = ''
red = reduce_object
types = red.items.map do |t|
next t if ['nil', 'void', 'undefined'].include?(t.name)
next t if ['::Boolean'].include?(t.rooted_name)
t.qualify api_map, context
end
ComplexType.new(types).reduce_object
end
# @param generics_to_resolve [Enumerable<String>]]
# @param context_type [UniqueType, nil]
# @param resolved_generic_values [Hash{String => ComplexType}] Added to as types are encountered or resolved
# @return [self]
def resolve_generics_from_context generics_to_resolve, context_type, resolved_generic_values: {}
return self unless generic?
ComplexType.new(@items.map { |i| i.resolve_generics_from_context(generics_to_resolve, context_type, resolved_generic_values: resolved_generic_values) })
end
# @return [UniqueType]
def first
@items.first
end
# @return [String]
def to_rbs
((@items.length > 1 ? '(' : '') +
@items.map(&:to_rbs).join(' | ') +
(@items.length > 1 ? ')' : ''))
end
# @param dst [ComplexType]
# @return [ComplexType]
def self_to_type dst
object_type_dst = dst.reduce_class_type
transform do |t|
next t if t.name != 'self'
object_type_dst
end
end
# @yieldparam [UniqueType]
# @return [Array]
def map &block
@items.map &block
end
# @yieldparam [UniqueType]
# @return [Enumerable<UniqueType>]
def each &block
@items.each &block
end
# @yieldparam [UniqueType]
# @return [void]
# @overload each_unique_type()
# @return [Enumerator<UniqueType>]
def each_unique_type &block
return enum_for(__method__) unless block_given?
@items.each do |item|
item.each_unique_type &block
end
end
# @return [Integer]
def length
@items.length
end
# @return [Array<UniqueType>]
def to_a
@items
end
def tags
@items.map(&:tag).join(', ')
end
# @param index [Integer]
# @return [UniqueType]
def [](index)
@items[index]
end
# @return [Array<UniqueType>]
def select &block
@items.select &block
end
# @return [String]
def namespace
# cache this attr for high frequency call
@namespace ||= method_missing(:namespace).to_s
end
# @return [Array<String>]
def namespaces
@items.map(&:namespace)
end
# @param name [Symbol]
# @return [Object, nil]
def method_missing name, *args, &block
return if @items.first.nil?
return @items.first.send(name, *args, &block) if respond_to_missing?(name)
super
end
# @param name [Symbol]
# @param include_private [Boolean]
def respond_to_missing?(name, include_private = false)
TypeMethods.public_instance_methods.include?(name) || super
end
def to_s
map(&:tag).join(', ')
end
def rooted_tags
map(&:rooted_tag).join(', ')
end
# @yieldparam [UniqueType]
def all? &block
@items.all? &block
end
# @yieldparam [UniqueType]
# @yieldreturn [Boolean]
# @return [Boolean]
def any? &block
@items.compact.any? &block
end
def selfy?
@items.any?(&:selfy?)
end
def generic?
any?(&:generic?)
end
# @param new_name [String, nil]
# @yieldparam t [UniqueType]
# @yieldreturn [UniqueType]
# @return [ComplexType]
def transform(new_name = nil, &transform_type)
raise "Please remove leading :: and set rooted with recreate() instead - #{new_name}" if new_name&.start_with?('::')
ComplexType.new(map { |ut| ut.transform(new_name, &transform_type) })
end
# @return [self]
def force_rooted
transform do |t|
t.recreate(make_rooted: true)
end
end
# @param definitions [Pin::Namespace, Pin::Method]
# @param context_type [ComplexType]
# @return [ComplexType]
def resolve_generics definitions, context_type
result = @items.map { |i| i.resolve_generics(definitions, context_type) }
ComplexType.new(result)
end
def nullable?
@items.any?(&:nil_type?)
end
# @return [Array<ComplexType>]
def all_params
@items.first.all_params || []
end
# @return [ComplexType]
def reduce_class_type
new_items = items.flat_map do |type|
next type unless ['Module', 'Class'].include?(type.name)
type.all_params
end
ComplexType.new(new_items)
end
# every type and subtype in this union have been resolved to be
# fully qualified
def all_rooted?
all?(&:all_rooted?)
end
# every top-level type has resolved to be fully qualified; see
# #all_rooted? to check their subtypes as well
def rooted?
all?(&:rooted?)
end
attr_reader :items
def rooted?
@items.all?(&:rooted?)
end
protected
# @return [ComplexType]
def reduce_object
new_items = items.flat_map do |ut|
next [ut] if ut.name != 'Object' || ut.subtypes.empty?
ut.subtypes
end
ComplexType.new(new_items)
end
def bottom?
@items.all?(&:bot?)
end
class << self
# Parse type strings into a ComplexType.
#
# @example
# ComplexType.parse 'String', 'Foo', 'nil' #=> [String, Foo, nil]
#
# @note
# The `partial` parameter is used to indicate that the method is
# receiving a string that will be used inside another ComplexType.
# It returns arrays of ComplexTypes instead of a single cohesive one.
# Consumers should not need to use this parameter; it should only be
# used internally.
#
# @param strings [Array<String>] The type definitions to parse
# @return [ComplexType]
# # @overload parse(*strings, partial: false)
# # @todo Need ability to use a literal true as a type below
# # @param partial [Boolean] True if the string is part of a another type
# # @return [Array<UniqueType>]
# @sg-ignore
# @todo To be able to select the right signature above,
# Chain::Call needs to know the decl type (:arg, :optarg,
# :kwarg, etc) of the arguments given, instead of just having
# an array of Chains as the arguments.
def parse *strings, partial: false
# @type [Hash{Array<String> => ComplexType}]
@cache ||= {}
unless partial
cached = @cache[strings]
return cached unless cached.nil?
end
types = []
key_types = nil
strings.each do |type_string|
point_stack = 0
curly_stack = 0
paren_stack = 0
base = String.new
subtype_string = String.new
type_string&.each_char do |char|
if char == '='
#raise ComplexTypeError, "Invalid = in type #{type_string}" unless curly_stack > 0
elsif char == '<'
point_stack += 1
elsif char == '>'
if subtype_string.end_with?('=') && curly_stack > 0
subtype_string += char
elsif base.end_with?('=')
raise ComplexTypeError, "Invalid hash thing" unless key_types.nil?
# types.push ComplexType.new([UniqueType.new(base[0..-2].strip)])
types.push UniqueType.parse(base[0..-2].strip, subtype_string)
# @todo this should either expand key_type's type
# automatically or complain about not being
# compatible with key_type's type in type checking
key_types = types
types = []
base.clear
subtype_string.clear
next
else
raise ComplexTypeError, "Invalid close in type #{type_string}" if point_stack == 0
point_stack -= 1
subtype_string += char
end
next
elsif char == '{'
curly_stack += 1
elsif char == '}'
curly_stack -= 1
subtype_string += char
raise ComplexTypeError, "Invalid close in type #{type_string}" if curly_stack < 0
next
elsif char == '('
paren_stack += 1
elsif char == ')'
paren_stack -= 1
subtype_string += char
raise ComplexTypeError, "Invalid close in type #{type_string}" if paren_stack < 0
next
elsif char == ',' && point_stack == 0 && curly_stack == 0 && paren_stack == 0
# types.push ComplexType.new([UniqueType.new(base.strip, subtype_string.strip)])
types.push UniqueType.parse(base.strip, subtype_string.strip)
base.clear
subtype_string.clear
next
end
if point_stack == 0 && curly_stack == 0 && paren_stack == 0
base.concat char
else
subtype_string.concat char
end
end
raise ComplexTypeError, "Unclosed subtype in #{type_string}" if point_stack != 0 || curly_stack != 0 || paren_stack != 0
# types.push ComplexType.new([UniqueType.new(base, subtype_string)])
types.push UniqueType.parse(base.strip, subtype_string.strip)
end
unless key_types.nil?
raise ComplexTypeError, "Invalid use of key/value parameters" unless partial
return key_types if types.empty?
return [key_types, types]
end
result = partial ? types : ComplexType.new(types)
@cache[strings] = result unless partial
result
end
# @param strings [Array<String>]
# @return [ComplexType]
def try_parse *strings
parse *strings
rescue ComplexTypeError => e
Solargraph.logger.info "Error parsing complex type `#{strings.join(', ')}`: #{e.message}"
ComplexType::UNDEFINED
end
end
VOID = ComplexType.parse('void')
UNDEFINED = ComplexType.parse('undefined')
SYMBOL = ComplexType.parse('::Symbol')
ROOT = ComplexType.parse('::Class<>')
NIL = ComplexType.parse('nil')
SELF = ComplexType.parse('self')
BOOLEAN = ComplexType.parse('::Boolean')
BOT = ComplexType.parse('bot')
private
# @todo This is a quick and dirty hack that forces `self` keywords
# to reference an instance of their class and never the class itself.
# This behavior may change depending on which result is expected
# from YARD conventions. See https://github.com/lsegal/yard/issues/1257
# @param dst [String]
# @return [String]
def reduce_class dst
while dst =~ /^(Class|Module)\<(.*?)\>$/
dst = dst.sub(/^(Class|Module)\</, '').sub(/\>$/, '')
end
dst
end
end
end