yard2steep 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +52 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +68 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +86 -0
  8. data/Rakefile +2 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/bin/steep-check +8 -0
  12. data/bin/steep-gen +16 -0
  13. data/bin/steep-gen-check +7 -0
  14. data/example/sample1/lib/example1.rb +107 -0
  15. data/example/sample1/sig/example1.rbi +21 -0
  16. data/example/sample2/lib/2.rb +68 -0
  17. data/example/sample2/sig/2.rbi +25 -0
  18. data/exe/yard2steep +6 -0
  19. data/lib/yard2steep/ast/class_node.rb +95 -0
  20. data/lib/yard2steep/ast/constant_node.rb +21 -0
  21. data/lib/yard2steep/ast/i_var_node.rb +15 -0
  22. data/lib/yard2steep/ast/method_node.rb +21 -0
  23. data/lib/yard2steep/ast/p_node.rb +23 -0
  24. data/lib/yard2steep/ast/p_type_node.rb +26 -0
  25. data/lib/yard2steep/ast.rb +11 -0
  26. data/lib/yard2steep/cli/option.rb +27 -0
  27. data/lib/yard2steep/cli.rb +70 -0
  28. data/lib/yard2steep/engine.rb +23 -0
  29. data/lib/yard2steep/gen.rb +162 -0
  30. data/lib/yard2steep/parser.rb +584 -0
  31. data/lib/yard2steep/util.rb +11 -0
  32. data/lib/yard2steep/version.rb +3 -0
  33. data/lib/yard2steep.rb +9 -0
  34. data/sig/steep-scaffold/td.rbi +174 -0
  35. data/sig/yard2steep/yard2steep/ast/class_node.rbi +26 -0
  36. data/sig/yard2steep/yard2steep/ast/constant_node.rbi +8 -0
  37. data/sig/yard2steep/yard2steep/ast/i_var_node.rbi +5 -0
  38. data/sig/yard2steep/yard2steep/ast/method_node.rbi +9 -0
  39. data/sig/yard2steep/yard2steep/ast/p_node.rbi +8 -0
  40. data/sig/yard2steep/yard2steep/ast/p_type_node.rbi +10 -0
  41. data/sig/yard2steep/yard2steep/ast.rbi +0 -0
  42. data/sig/yard2steep/yard2steep/cli/option.rbi +12 -0
  43. data/sig/yard2steep/yard2steep/cli.rbi +9 -0
  44. data/sig/yard2steep/yard2steep/engine.rbi +3 -0
  45. data/sig/yard2steep/yard2steep/gen.rbi +15 -0
  46. data/sig/yard2steep/yard2steep/parser.rbi +52 -0
  47. data/sig/yard2steep/yard2steep/util.rbi +3 -0
  48. data/sig/yard2steep/yard2steep/version.rbi +1 -0
  49. data/sig/yard2steep/yard2steep.rbi +0 -0
  50. data/yard2steep.gemspec +29 -0
  51. 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
@@ -0,0 +1,11 @@
1
+ module Yard2steep
2
+ module Util
3
+ class AssertError < RuntimeError; end
4
+
5
+ # @param [{ () -> any }]
6
+ # @return [void]
7
+ def self.assert!(&block)
8
+ raise AssertError.new("Assertion failed!") if !block.call
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Yard2steep
2
+ VERSION = "0.1.0"
3
+ end
data/lib/yard2steep.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'yard2steep/util'
2
+ require 'yard2steep/parser'
3
+ require 'yard2steep/gen'
4
+ require 'yard2steep/engine'
5
+ require 'yard2steep/cli'
6
+ require "yard2steep/version"
7
+
8
+ module Yard2steep
9
+ end