yard2steep 0.1.0
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 +7 -0
- data/.gitignore +52 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +68 -0
- data/LICENSE.txt +21 -0
- data/README.md +86 -0
- data/Rakefile +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/steep-check +8 -0
- data/bin/steep-gen +16 -0
- data/bin/steep-gen-check +7 -0
- data/example/sample1/lib/example1.rb +107 -0
- data/example/sample1/sig/example1.rbi +21 -0
- data/example/sample2/lib/2.rb +68 -0
- data/example/sample2/sig/2.rbi +25 -0
- data/exe/yard2steep +6 -0
- data/lib/yard2steep/ast/class_node.rb +95 -0
- data/lib/yard2steep/ast/constant_node.rb +21 -0
- data/lib/yard2steep/ast/i_var_node.rb +15 -0
- data/lib/yard2steep/ast/method_node.rb +21 -0
- data/lib/yard2steep/ast/p_node.rb +23 -0
- data/lib/yard2steep/ast/p_type_node.rb +26 -0
- data/lib/yard2steep/ast.rb +11 -0
- data/lib/yard2steep/cli/option.rb +27 -0
- data/lib/yard2steep/cli.rb +70 -0
- data/lib/yard2steep/engine.rb +23 -0
- data/lib/yard2steep/gen.rb +162 -0
- data/lib/yard2steep/parser.rb +584 -0
- data/lib/yard2steep/util.rb +11 -0
- data/lib/yard2steep/version.rb +3 -0
- data/lib/yard2steep.rb +9 -0
- data/sig/steep-scaffold/td.rbi +174 -0
- data/sig/yard2steep/yard2steep/ast/class_node.rbi +26 -0
- data/sig/yard2steep/yard2steep/ast/constant_node.rbi +8 -0
- data/sig/yard2steep/yard2steep/ast/i_var_node.rbi +5 -0
- data/sig/yard2steep/yard2steep/ast/method_node.rbi +9 -0
- data/sig/yard2steep/yard2steep/ast/p_node.rbi +8 -0
- data/sig/yard2steep/yard2steep/ast/p_type_node.rbi +10 -0
- data/sig/yard2steep/yard2steep/ast.rbi +0 -0
- data/sig/yard2steep/yard2steep/cli/option.rbi +12 -0
- data/sig/yard2steep/yard2steep/cli.rbi +9 -0
- data/sig/yard2steep/yard2steep/engine.rbi +3 -0
- data/sig/yard2steep/yard2steep/gen.rbi +15 -0
- data/sig/yard2steep/yard2steep/parser.rbi +52 -0
- data/sig/yard2steep/yard2steep/util.rbi +3 -0
- data/sig/yard2steep/yard2steep/version.rbi +1 -0
- data/sig/yard2steep/yard2steep.rbi +0 -0
- data/yard2steep.gemspec +29 -0
- metadata +164 -0
@@ -0,0 +1,584 @@
|
|
1
|
+
require 'yard2steep/ast'
|
2
|
+
|
3
|
+
module Yard2steep
|
4
|
+
class Parser
|
5
|
+
S_RE = /[\s\t]*/
|
6
|
+
S_P_RE = /[\s\t]+/
|
7
|
+
PRE_RE = /^#{S_RE}/
|
8
|
+
POST_RE = /#{S_RE}$/
|
9
|
+
|
10
|
+
CLASS_RE = /
|
11
|
+
#{PRE_RE}
|
12
|
+
(class)
|
13
|
+
#{S_P_RE}
|
14
|
+
(\w+)
|
15
|
+
(?:
|
16
|
+
#{S_P_RE}
|
17
|
+
<
|
18
|
+
#{S_P_RE}
|
19
|
+
\w+
|
20
|
+
)?
|
21
|
+
#{POST_RE}
|
22
|
+
/x
|
23
|
+
MODULE_RE = /
|
24
|
+
#{PRE_RE}
|
25
|
+
(module)
|
26
|
+
#{S_P_RE}
|
27
|
+
(\w+)
|
28
|
+
#{POST_RE}
|
29
|
+
/x
|
30
|
+
S_CLASS_RE = /#{PRE_RE}class#{S_P_RE}<<#{S_P_RE}\w+#{POST_RE}/
|
31
|
+
END_RE = /#{PRE_RE}end#{POST_RE}/
|
32
|
+
|
33
|
+
CONSTANT_ASSIGN_RE = /
|
34
|
+
#{PRE_RE}
|
35
|
+
(
|
36
|
+
[A-Z\d_]+
|
37
|
+
)
|
38
|
+
#{S_RE}
|
39
|
+
=
|
40
|
+
.*
|
41
|
+
#{POST_RE}
|
42
|
+
/x
|
43
|
+
|
44
|
+
# TODO(south37) `POSTFIX_IF_RE` is wrong. Fix it.
|
45
|
+
POSTFIX_IF_RE = /
|
46
|
+
#{PRE_RE}
|
47
|
+
(?:return|break|next|p|print|raise)
|
48
|
+
#{S_P_RE}
|
49
|
+
.*
|
50
|
+
(?:if|unless)
|
51
|
+
#{S_P_RE}
|
52
|
+
.*
|
53
|
+
$
|
54
|
+
/x
|
55
|
+
|
56
|
+
BEGIN_END_RE = /
|
57
|
+
#{S_P_RE}
|
58
|
+
(if|unless|do|while|until|case|for|begin)
|
59
|
+
(?:#{S_P_RE}.*)?
|
60
|
+
$/x
|
61
|
+
|
62
|
+
COMMENT_RE = /#{PRE_RE}#/
|
63
|
+
TYPE_WITH_PAREN_RE = /\[([^\]]*)\]/
|
64
|
+
|
65
|
+
PARAM_RE = /
|
66
|
+
#{COMMENT_RE}
|
67
|
+
#{S_P_RE}
|
68
|
+
@param
|
69
|
+
#{S_P_RE}
|
70
|
+
#{TYPE_WITH_PAREN_RE}
|
71
|
+
#{S_P_RE}
|
72
|
+
(\w+)
|
73
|
+
/x
|
74
|
+
RETURN_RE = /
|
75
|
+
#{COMMENT_RE}
|
76
|
+
#{S_P_RE}
|
77
|
+
@return
|
78
|
+
#{S_P_RE}
|
79
|
+
#{TYPE_WITH_PAREN_RE}
|
80
|
+
/x
|
81
|
+
|
82
|
+
PAREN_RE = /
|
83
|
+
\(
|
84
|
+
([^)]*)
|
85
|
+
\)
|
86
|
+
/x
|
87
|
+
|
88
|
+
# NOTE: This implementation should be fixed.
|
89
|
+
ARGS_RE = /
|
90
|
+
#{S_RE}
|
91
|
+
\(
|
92
|
+
[^)]*
|
93
|
+
\)
|
94
|
+
|
|
95
|
+
#{S_P_RE}
|
96
|
+
.*
|
97
|
+
/x
|
98
|
+
|
99
|
+
METHOD_RE = /
|
100
|
+
#{PRE_RE}
|
101
|
+
def
|
102
|
+
#{S_P_RE}
|
103
|
+
(
|
104
|
+
(?:\w+\.)?
|
105
|
+
\w+
|
106
|
+
(?:\!|\?)?
|
107
|
+
)
|
108
|
+
#{S_RE}
|
109
|
+
(
|
110
|
+
(?:#{ARGS_RE})
|
111
|
+
?
|
112
|
+
)
|
113
|
+
#{POST_RE}
|
114
|
+
/x
|
115
|
+
|
116
|
+
# TODO(south37) Support attr_writer, attr_accessor
|
117
|
+
ATTR_RE = /
|
118
|
+
#{PRE_RE}
|
119
|
+
attr_reader
|
120
|
+
#{S_P_RE}
|
121
|
+
(:\w+.*)
|
122
|
+
#{POST_RE}
|
123
|
+
/x
|
124
|
+
|
125
|
+
STATES = {
|
126
|
+
class: "STATES.class",
|
127
|
+
s_class: "STATES.s_class", # singleton class
|
128
|
+
method: "STATES.method",
|
129
|
+
}
|
130
|
+
|
131
|
+
ANY_TYPE = 'any'
|
132
|
+
ANY_BLOCK_TYPE = '{ (any) -> any }'
|
133
|
+
|
134
|
+
def initialize
|
135
|
+
# Debug flag
|
136
|
+
@debug = false
|
137
|
+
|
138
|
+
# NOTE: set at parse
|
139
|
+
@file = nil
|
140
|
+
|
141
|
+
main = AST::ClassNode.create_main
|
142
|
+
@ast = main
|
143
|
+
|
144
|
+
# Stack of parser state
|
145
|
+
@stack = [STATES[:class]]
|
146
|
+
# Parser state. Being last one of STATES in @stack.
|
147
|
+
@state = STATES[:class]
|
148
|
+
|
149
|
+
# NOTE: reset class context
|
150
|
+
@current_class = main
|
151
|
+
|
152
|
+
reset_method_context!
|
153
|
+
end
|
154
|
+
|
155
|
+
# @param [String] file
|
156
|
+
# @param [String] text
|
157
|
+
# @param [bool] debug
|
158
|
+
# @return [AST::ClassNode]
|
159
|
+
def parse(file, text, debug: false)
|
160
|
+
@debug = debug
|
161
|
+
|
162
|
+
debug_print!("Start parsing: #{file}")
|
163
|
+
|
164
|
+
@file = file
|
165
|
+
text.split(/\n|;/).each do |l|
|
166
|
+
parse_line(l)
|
167
|
+
end
|
168
|
+
|
169
|
+
@ast
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# @return [void]
|
175
|
+
def reset_method_context!
|
176
|
+
# Current method context. Flushed when method definition is parsed.
|
177
|
+
@p_types = {}
|
178
|
+
@r_type = nil
|
179
|
+
@m_name = nil
|
180
|
+
end
|
181
|
+
|
182
|
+
# NOTE: Current implementation override `@p_type`, `@r_type` if it appears
|
183
|
+
# multiple times before method definition.
|
184
|
+
#
|
185
|
+
# @param [String] l
|
186
|
+
# @return [void]
|
187
|
+
def parse_line(l)
|
188
|
+
# At first, try parsing comment
|
189
|
+
return if try_parse_comment(l)
|
190
|
+
|
191
|
+
return if try_parse_end(l)
|
192
|
+
return if try_parse_postfix_if(l)
|
193
|
+
return if try_parse_begin_end(l)
|
194
|
+
|
195
|
+
case @state
|
196
|
+
when STATES[:class]
|
197
|
+
return if try_parse_constant(l)
|
198
|
+
return if try_parse_class(l)
|
199
|
+
return if try_parse_singleton_class(l)
|
200
|
+
return if try_parse_method(l)
|
201
|
+
return if try_parse_attr(l)
|
202
|
+
when STATES[:s_class]
|
203
|
+
return if try_parse_method_with_no_action(l)
|
204
|
+
when STATES[:method]
|
205
|
+
# Do nothing
|
206
|
+
else
|
207
|
+
raise "invalid state: #{@state}"
|
208
|
+
end
|
209
|
+
|
210
|
+
# NOTE: Reach here when other case
|
211
|
+
end
|
212
|
+
|
213
|
+
# @param [String] l
|
214
|
+
# @return [bool]
|
215
|
+
def try_parse_comment(l)
|
216
|
+
return false unless l.match?(COMMENT_RE)
|
217
|
+
|
218
|
+
try_parse_param_or_return(l)
|
219
|
+
|
220
|
+
true
|
221
|
+
end
|
222
|
+
|
223
|
+
# @param [String] l
|
224
|
+
# @return [bool]
|
225
|
+
def try_parse_param_or_return(l)
|
226
|
+
if @state == STATES[:class]
|
227
|
+
return if try_parse_param(l)
|
228
|
+
return if try_parse_return(l)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# @param [String] l
|
233
|
+
# @return [bool]
|
234
|
+
def try_parse_end(l)
|
235
|
+
return false unless l.match?(END_RE)
|
236
|
+
|
237
|
+
# NOTE: Print before pop state, so offset is -2
|
238
|
+
debug_print!("end", offset: -2)
|
239
|
+
|
240
|
+
if stack_is_empty?
|
241
|
+
raise "Invalid end: #{@file}"
|
242
|
+
end
|
243
|
+
|
244
|
+
pop_state!
|
245
|
+
|
246
|
+
true
|
247
|
+
end
|
248
|
+
|
249
|
+
# NOTE: This implementation is wrong. Used only for skipping postfix if.
|
250
|
+
#
|
251
|
+
# @param [String] l
|
252
|
+
# @return [bool]
|
253
|
+
def try_parse_postfix_if(l)
|
254
|
+
l.match?(POSTFIX_IF_RE)
|
255
|
+
end
|
256
|
+
|
257
|
+
# @param [String] l
|
258
|
+
# @return [bool]
|
259
|
+
def try_parse_begin_end(l)
|
260
|
+
m = l.match(BEGIN_END_RE)
|
261
|
+
return false unless m
|
262
|
+
|
263
|
+
debug_print!(m[1])
|
264
|
+
|
265
|
+
push_state!(m[1])
|
266
|
+
|
267
|
+
true
|
268
|
+
end
|
269
|
+
|
270
|
+
# @param [String] l
|
271
|
+
# @return [bool]
|
272
|
+
def try_parse_class(l)
|
273
|
+
m = (l.match(MODULE_RE) || l.match(CLASS_RE))
|
274
|
+
return false unless m
|
275
|
+
|
276
|
+
debug_print!("#{m[1]} #{m[2]}")
|
277
|
+
|
278
|
+
# NOTE: If class definition is found before method definition, yard
|
279
|
+
# annotation is ignored.
|
280
|
+
reset_method_context!
|
281
|
+
|
282
|
+
c = AST::ClassNode.new(
|
283
|
+
kind: m[1],
|
284
|
+
c_name: m[2],
|
285
|
+
parent: @current_class,
|
286
|
+
)
|
287
|
+
@current_class.append_child(c)
|
288
|
+
@current_class = c
|
289
|
+
|
290
|
+
push_state!(STATES[:class])
|
291
|
+
|
292
|
+
true
|
293
|
+
end
|
294
|
+
|
295
|
+
# @param [String] l
|
296
|
+
# @return [bool]
|
297
|
+
def try_parse_constant(l)
|
298
|
+
m = l.match(CONSTANT_ASSIGN_RE)
|
299
|
+
return false unless m
|
300
|
+
|
301
|
+
c = AST::ConstantNode.new(
|
302
|
+
name: m[1],
|
303
|
+
klass: @current_class,
|
304
|
+
)
|
305
|
+
@current_class.append_constant(c)
|
306
|
+
true
|
307
|
+
end
|
308
|
+
|
309
|
+
# @param [String] l
|
310
|
+
# @return [bool]
|
311
|
+
def try_parse_singleton_class(l)
|
312
|
+
m = l.match(S_CLASS_RE)
|
313
|
+
return false unless m
|
314
|
+
|
315
|
+
debug_print!("class <<")
|
316
|
+
|
317
|
+
push_state!(STATES[:s_class])
|
318
|
+
|
319
|
+
true
|
320
|
+
end
|
321
|
+
|
322
|
+
# @param [String] l
|
323
|
+
# @return [bool]
|
324
|
+
def try_parse_param(l)
|
325
|
+
m = l.match(PARAM_RE)
|
326
|
+
return false unless m
|
327
|
+
|
328
|
+
p = AST::PTypeNode.new(
|
329
|
+
p_type: normalize_type(m[1]),
|
330
|
+
p_name: m[2],
|
331
|
+
kind: AST::PTypeNode::KIND[:normal],
|
332
|
+
)
|
333
|
+
@p_types[p.p_name] = p
|
334
|
+
|
335
|
+
true
|
336
|
+
end
|
337
|
+
|
338
|
+
# @param [String] l
|
339
|
+
# @return [bool]
|
340
|
+
def try_parse_return(l)
|
341
|
+
m = l.match(RETURN_RE)
|
342
|
+
return false unless m
|
343
|
+
|
344
|
+
@r_type = normalize_type(m[1])
|
345
|
+
|
346
|
+
true
|
347
|
+
end
|
348
|
+
|
349
|
+
# @param [String] l
|
350
|
+
# @return [bool]
|
351
|
+
def try_parse_method(l)
|
352
|
+
m = l.match(METHOD_RE)
|
353
|
+
return false unless m
|
354
|
+
|
355
|
+
debug_print!("def #{m[1]}")
|
356
|
+
|
357
|
+
Util.assert! { m[1].is_a?(String) && m[2].is_a?(String) }
|
358
|
+
|
359
|
+
@m_name = m[1]
|
360
|
+
p_list = parse_method_params(m[2].strip)
|
361
|
+
|
362
|
+
m_node = AST::MethodNode.new(
|
363
|
+
p_list: p_list,
|
364
|
+
r_type: (@r_type || ANY_TYPE),
|
365
|
+
m_name: @m_name,
|
366
|
+
)
|
367
|
+
@current_class.append_m(m_node)
|
368
|
+
reset_method_context!
|
369
|
+
|
370
|
+
push_state!(STATES[:method])
|
371
|
+
|
372
|
+
true
|
373
|
+
end
|
374
|
+
|
375
|
+
# @param [String] l
|
376
|
+
# @return [bool]
|
377
|
+
def try_parse_method_with_no_action(l)
|
378
|
+
m = l.match(METHOD_RE)
|
379
|
+
return false unless m
|
380
|
+
|
381
|
+
debug_print!("def #{m[1]}")
|
382
|
+
|
383
|
+
# Do no action
|
384
|
+
|
385
|
+
push_state!(STATES[:method])
|
386
|
+
|
387
|
+
true
|
388
|
+
end
|
389
|
+
|
390
|
+
# @param [String] params_s
|
391
|
+
# @return [Array<AST::PNode>]
|
392
|
+
def parse_method_params(params_s)
|
393
|
+
Util.assert! { params_s.is_a?(String) }
|
394
|
+
|
395
|
+
# NOTE: Remove parenthesis
|
396
|
+
if (m = params_s.match(PAREN_RE))
|
397
|
+
params_s = m[1]
|
398
|
+
end
|
399
|
+
|
400
|
+
if params_s == ''
|
401
|
+
if @p_types.size > 0
|
402
|
+
print "warn: #{@m_name} has no args, but annotated as #{@p_types}"
|
403
|
+
end
|
404
|
+
return []
|
405
|
+
end
|
406
|
+
|
407
|
+
params_s.split(',').map { |s| s.strip }.map do |p|
|
408
|
+
if p.include?(':')
|
409
|
+
name, default_value = p.split(':')
|
410
|
+
if default_value
|
411
|
+
AST::PNode.new(
|
412
|
+
type_node: type_node(name),
|
413
|
+
style: AST::PNode::STYLE[:keyword_with_default],
|
414
|
+
)
|
415
|
+
else
|
416
|
+
AST::PNode.new(
|
417
|
+
type_node: type_node(name),
|
418
|
+
style: AST::PNode::STYLE[:keyword],
|
419
|
+
)
|
420
|
+
end
|
421
|
+
else
|
422
|
+
AST::PNode.new(
|
423
|
+
type_node: type_node(p),
|
424
|
+
style: AST::PNode::STYLE[:normal],
|
425
|
+
)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# @param [String] l
|
431
|
+
# @return [bool]
|
432
|
+
def try_parse_attr(l)
|
433
|
+
m = l.match(ATTR_RE)
|
434
|
+
return false unless m
|
435
|
+
|
436
|
+
ivars = m[1].split(",").map { |s| s.strip.gsub(/^:/, '') }
|
437
|
+
ivars.each do |ivarname|
|
438
|
+
@current_class.append_ivar(
|
439
|
+
AST::IVarNode.new(
|
440
|
+
name: ivarname
|
441
|
+
)
|
442
|
+
)
|
443
|
+
|
444
|
+
# NOTE: Attr reader should add getter method
|
445
|
+
@current_class.append_m(
|
446
|
+
AST::MethodNode.new(
|
447
|
+
p_list: [],
|
448
|
+
r_type: ANY_TYPE,
|
449
|
+
m_name: ivarname,
|
450
|
+
)
|
451
|
+
)
|
452
|
+
end
|
453
|
+
|
454
|
+
true
|
455
|
+
end
|
456
|
+
|
457
|
+
# @param [String] state
|
458
|
+
# @return [void]
|
459
|
+
def push_state!(state)
|
460
|
+
if STATES.values.include?(state)
|
461
|
+
@state = state
|
462
|
+
end
|
463
|
+
@stack.push(state)
|
464
|
+
end
|
465
|
+
|
466
|
+
# @return [void]
|
467
|
+
def pop_state!
|
468
|
+
state = @stack.pop
|
469
|
+
if STATES.values.include?(state)
|
470
|
+
# Restore prev class
|
471
|
+
if state == STATES[:class]
|
472
|
+
@current_class = @current_class.parent
|
473
|
+
end
|
474
|
+
|
475
|
+
# Restore prev state
|
476
|
+
@state = @stack.select { |s| STATES.values.include?(s) }.last
|
477
|
+
Util.assert! { !@state.nil? }
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
# @return [bool]
|
482
|
+
def stack_is_empty?
|
483
|
+
@stack.size <= 1
|
484
|
+
end
|
485
|
+
|
486
|
+
##
|
487
|
+
# Helper
|
488
|
+
|
489
|
+
# @param [String] p
|
490
|
+
# @return [AST::PTypeNode]
|
491
|
+
def type_node(p)
|
492
|
+
if @p_types[p]
|
493
|
+
@p_types[p]
|
494
|
+
else
|
495
|
+
# NOTE: `&` represents block variable
|
496
|
+
if p[0] == '&'
|
497
|
+
AST::PTypeNode.new(
|
498
|
+
p_type: ANY_BLOCK_TYPE,
|
499
|
+
p_name: p[1..-1],
|
500
|
+
kind: AST::PTypeNode::KIND[:block],
|
501
|
+
)
|
502
|
+
else
|
503
|
+
AST::PTypeNode.new(
|
504
|
+
p_type: ANY_TYPE,
|
505
|
+
p_name: p,
|
506
|
+
kind: AST::PTypeNode::KIND[:normal],
|
507
|
+
)
|
508
|
+
end
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
# @param [String] message
|
513
|
+
# @param [Integer] offset
|
514
|
+
# @return [void]
|
515
|
+
def debug_print!(message, offset: 0)
|
516
|
+
print "#{' ' * (@stack.size * 2 + offset)}#{message}\n" if @debug
|
517
|
+
end
|
518
|
+
|
519
|
+
ARRAY_TYPE_RE = /^
|
520
|
+
Array
|
521
|
+
#{S_RE}
|
522
|
+
<
|
523
|
+
([^>]+)
|
524
|
+
>
|
525
|
+
#{S_RE}
|
526
|
+
$/x
|
527
|
+
FIXED_ARRAY_TYPE_RE = /^
|
528
|
+
Array
|
529
|
+
#{S_RE}
|
530
|
+
\(
|
531
|
+
([^)]+)
|
532
|
+
\)
|
533
|
+
#{S_RE}
|
534
|
+
$/x
|
535
|
+
HASH_TYPE_RE = /^
|
536
|
+
Hash
|
537
|
+
#{S_RE}
|
538
|
+
\{
|
539
|
+
#{S_RE}
|
540
|
+
([^=]+)
|
541
|
+
#{S_RE}
|
542
|
+
=>
|
543
|
+
#{S_RE}
|
544
|
+
([^}]+)
|
545
|
+
#{S_RE}
|
546
|
+
\}
|
547
|
+
#{S_RE}
|
548
|
+
$/x
|
549
|
+
|
550
|
+
# NOTE: normalize type to steep representation
|
551
|
+
#
|
552
|
+
# @param [String] type
|
553
|
+
# @return [String]
|
554
|
+
def normalize_type(type)
|
555
|
+
if type[0..4] == 'Array'.freeze
|
556
|
+
if type == 'Array'.freeze
|
557
|
+
'Array<any>'.freeze
|
558
|
+
elsif (m = ARRAY_TYPE_RE.match(type))
|
559
|
+
"Array<#{normalize_multi_type(m[1])}>"
|
560
|
+
elsif (m = FIXED_ARRAY_TYPE_RE.match(type))
|
561
|
+
"Array<#{normalize_multi_type(m[1])}>"
|
562
|
+
else
|
563
|
+
raise "invalid Array type: #{type}"
|
564
|
+
end
|
565
|
+
elsif type[0..3] == 'Hash'.freeze
|
566
|
+
if type == 'Hash'.freeze
|
567
|
+
'Hash<any, any>'.freeze
|
568
|
+
elsif (m = HASH_TYPE_RE.match(type))
|
569
|
+
"Hash<#{normalize_multi_type(m[1])}, #{normalize_multi_type(m[2])}>"
|
570
|
+
else
|
571
|
+
raise "invalid Hash type: #{type}"
|
572
|
+
end
|
573
|
+
else
|
574
|
+
normalize_multi_type(type)
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
# @param [String] type_s
|
579
|
+
# @return [String]
|
580
|
+
def normalize_multi_type(type_s)
|
581
|
+
type_s.split(',').map { |s| s.strip }.uniq.join(' | ')
|
582
|
+
end
|
583
|
+
end
|
584
|
+
end
|