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.
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