rdoc 6.8.1 → 6.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1026 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'prism'
4
+ require_relative 'ripper_state_lex'
5
+
6
+ # Unlike lib/rdoc/parser/ruby.rb, this file is not based on rtags and does not contain code from
7
+ # rtags.rb -
8
+ # ruby-lex.rb - ruby lexcal analyzer
9
+ # ruby-token.rb - ruby tokens
10
+
11
+ # Parse and collect document from Ruby source code.
12
+ # RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it.
13
+
14
+ class RDoc::Parser::PrismRuby < RDoc::Parser
15
+
16
+ parse_files_matching(/\.rbw?$/) if ENV['RDOC_USE_PRISM_PARSER']
17
+
18
+ attr_accessor :visibility
19
+ attr_reader :container, :singleton
20
+
21
+ def initialize(top_level, file_name, content, options, stats)
22
+ super
23
+
24
+ content = handle_tab_width(content)
25
+
26
+ @size = 0
27
+ @token_listeners = nil
28
+ content = RDoc::Encoding.remove_magic_comment content
29
+ @content = content
30
+ @markup = @options.markup
31
+ @track_visibility = :nodoc != @options.visibility
32
+ @encoding = @options.encoding
33
+
34
+ @module_nesting = [top_level]
35
+ @container = top_level
36
+ @visibility = :public
37
+ @singleton = false
38
+ end
39
+
40
+ # Dive into another container
41
+
42
+ def with_container(container, singleton: false)
43
+ old_container = @container
44
+ old_visibility = @visibility
45
+ old_singleton = @singleton
46
+ @visibility = :public
47
+ @container = container
48
+ @singleton = singleton
49
+ unless singleton
50
+ @module_nesting.push container
51
+
52
+ # Need to update module parent chain to emulate Module.nesting.
53
+ # This mechanism is inaccurate and needs to be fixed.
54
+ container.parent = old_container
55
+ end
56
+ yield container
57
+ ensure
58
+ @container = old_container
59
+ @visibility = old_visibility
60
+ @singleton = old_singleton
61
+ @module_nesting.pop unless singleton
62
+ end
63
+
64
+ # Records the location of this +container+ in the file for this parser and
65
+ # adds it to the list of classes and modules in the file.
66
+
67
+ def record_location container # :nodoc:
68
+ case container
69
+ when RDoc::ClassModule then
70
+ @top_level.add_to_classes_or_modules container
71
+ end
72
+
73
+ container.record_location @top_level
74
+ end
75
+
76
+ # Scans this Ruby file for Ruby constructs
77
+
78
+ def scan
79
+ @tokens = RDoc::Parser::RipperStateLex.parse(@content)
80
+ @lines = @content.lines
81
+ result = Prism.parse(@content)
82
+ @program_node = result.value
83
+ @line_nodes = {}
84
+ prepare_line_nodes(@program_node)
85
+ prepare_comments(result.comments)
86
+ return if @top_level.done_documenting
87
+
88
+ @first_non_meta_comment = nil
89
+ if (_line_no, start_line, rdoc_comment = @unprocessed_comments.first)
90
+ @first_non_meta_comment = rdoc_comment if start_line < @program_node.location.start_line
91
+ end
92
+
93
+ @program_node.accept(RDocVisitor.new(self, @top_level, @store))
94
+ process_comments_until(@lines.size + 1)
95
+ end
96
+
97
+ def should_document?(code_object) # :nodoc:
98
+ return true unless @track_visibility
99
+ return false if code_object.parent&.document_children == false
100
+ code_object.document_self
101
+ end
102
+
103
+ # Assign AST node to a line.
104
+ # This is used to show meta-method source code in the documentation.
105
+
106
+ def prepare_line_nodes(node) # :nodoc:
107
+ case node
108
+ when Prism::CallNode, Prism::DefNode
109
+ @line_nodes[node.location.start_line] ||= node
110
+ end
111
+ node.compact_child_nodes.each do |child|
112
+ prepare_line_nodes(child)
113
+ end
114
+ end
115
+
116
+ # Prepares comments for processing. Comments are grouped into consecutive.
117
+ # Consecutive comment is linked to the next non-blank line.
118
+ #
119
+ # Example:
120
+ # 01| class A # modifier comment 1
121
+ # 02| def foo; end # modifier comment 2
122
+ # 03|
123
+ # 04| # consecutive comment 1 start_line: 4
124
+ # 05| # consecutive comment 1 linked to line: 7
125
+ # 06|
126
+ # 07| # consecutive comment 2 start_line: 7
127
+ # 08| # consecutive comment 2 linked to line: 10
128
+ # 09|
129
+ # 10| def bar; end # consecutive comment 2 linked to this line
130
+ # 11| end
131
+
132
+ def prepare_comments(comments)
133
+ current = []
134
+ consecutive_comments = [current]
135
+ @modifier_comments = {}
136
+ comments.each do |comment|
137
+ if comment.is_a? Prism::EmbDocComment
138
+ consecutive_comments << [comment] << (current = [])
139
+ elsif comment.location.start_line_slice.match?(/\S/)
140
+ @modifier_comments[comment.location.start_line] = RDoc::Comment.new(comment.slice, @top_level, :ruby)
141
+ elsif current.empty? || current.last.location.end_line + 1 == comment.location.start_line
142
+ current << comment
143
+ else
144
+ consecutive_comments << (current = [comment])
145
+ end
146
+ end
147
+ consecutive_comments.reject!(&:empty?)
148
+
149
+ # Example: line_no = 5, start_line = 2, comment_text = "# comment_start_line\n# comment\n"
150
+ # 1| class A
151
+ # 2| # comment_start_line
152
+ # 3| # comment
153
+ # 4|
154
+ # 5| def f; end # comment linked to this line
155
+ # 6| end
156
+ @unprocessed_comments = consecutive_comments.map! do |comments|
157
+ start_line = comments.first.location.start_line
158
+ line_no = comments.last.location.end_line + (comments.last.location.end_column == 0 ? 0 : 1)
159
+ texts = comments.map do |c|
160
+ c.is_a?(Prism::EmbDocComment) ? c.slice.lines[1...-1].join : c.slice
161
+ end
162
+ text = RDoc::Encoding.change_encoding(texts.join("\n"), @encoding) if @encoding
163
+ line_no += 1 while @lines[line_no - 1]&.match?(/\A\s*$/)
164
+ comment = RDoc::Comment.new(text, @top_level, :ruby)
165
+ comment.line = start_line
166
+ [line_no, start_line, comment]
167
+ end
168
+
169
+ # The first comment is special. It defines markup for the rest of the comments.
170
+ _, first_comment_start_line, first_comment_text = @unprocessed_comments.first
171
+ if first_comment_text && @lines[0...first_comment_start_line - 1].all? { |l| l.match?(/\A\s*$/) }
172
+ comment = RDoc::Comment.new(first_comment_text.text, @top_level, :ruby)
173
+ handle_consecutive_comment_directive(@container, comment)
174
+ @markup = comment.format
175
+ end
176
+ @unprocessed_comments.each do |_, _, comment|
177
+ comment.format = @markup
178
+ end
179
+ end
180
+
181
+ # Creates an RDoc::Method on +container+ from +comment+ if there is a
182
+ # Signature section in the comment
183
+
184
+ def parse_comment_tomdoc(container, comment, line_no, start_line)
185
+ return unless signature = RDoc::TomDoc.signature(comment)
186
+
187
+ name, = signature.split %r%[ \(]%, 2
188
+
189
+ meth = RDoc::GhostMethod.new comment.text, name
190
+ record_location(meth)
191
+ meth.line = start_line
192
+ meth.call_seq = signature
193
+ return unless meth.name
194
+
195
+ meth.start_collecting_tokens
196
+ node = @line_nodes[line_no]
197
+ tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)]
198
+ tokens.each { |token| meth.token_stream << token }
199
+
200
+ container.add_method meth
201
+ comment.remove_private
202
+ comment.normalize
203
+ meth.comment = comment
204
+ @stats.add_method meth
205
+ end
206
+
207
+ def handle_modifier_directive(code_object, line_no) # :nodoc:
208
+ comment = @modifier_comments[line_no]
209
+ @preprocess.handle(comment.text, code_object) if comment
210
+ end
211
+
212
+ def handle_consecutive_comment_directive(code_object, comment) # :nodoc:
213
+ return unless comment
214
+ @preprocess.handle(comment, code_object) do |directive, param|
215
+ case directive
216
+ when 'method', 'singleton-method',
217
+ 'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then
218
+ # handled elsewhere
219
+ ''
220
+ when 'section' then
221
+ @container.set_current_section(param, comment.dup)
222
+ comment.text = ''
223
+ break
224
+ end
225
+ end
226
+ comment.remove_private
227
+ end
228
+
229
+ def call_node_name_arguments(call_node) # :nodoc:
230
+ return [] unless call_node.arguments
231
+ call_node.arguments.arguments.map do |arg|
232
+ case arg
233
+ when Prism::SymbolNode
234
+ arg.value
235
+ when Prism::StringNode
236
+ arg.unescaped
237
+ end
238
+ end || []
239
+ end
240
+
241
+ # Handles meta method comments
242
+
243
+ def handle_meta_method_comment(comment, node)
244
+ is_call_node = node.is_a?(Prism::CallNode)
245
+ singleton_method = false
246
+ visibility = @visibility
247
+ attributes = rw = line_no = method_name = nil
248
+
249
+ processed_comment = comment.dup
250
+ @preprocess.handle(processed_comment, @container) do |directive, param, line|
251
+ case directive
252
+ when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor'
253
+ attributes = [param] if param
254
+ attributes ||= call_node_name_arguments(node) if is_call_node
255
+ rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R'
256
+ ''
257
+ when 'method'
258
+ method_name = param
259
+ line_no = line
260
+ ''
261
+ when 'singleton-method'
262
+ method_name = param
263
+ line_no = line
264
+ singleton_method = true
265
+ visibility = :public
266
+ ''
267
+ when 'section' then
268
+ @container.set_current_section(param, comment.dup)
269
+ return # If the comment contains :section:, it is not a meta method comment
270
+ end
271
+ end
272
+
273
+ if attributes
274
+ attributes.each do |attr|
275
+ a = RDoc::Attr.new(@container, attr, rw, processed_comment)
276
+ a.store = @store
277
+ a.line = line_no
278
+ a.singleton = @singleton
279
+ record_location(a)
280
+ @container.add_attribute(a)
281
+ a.visibility = visibility
282
+ end
283
+ elsif line_no || node
284
+ method_name ||= call_node_name_arguments(node).first if is_call_node
285
+ meth = RDoc::AnyMethod.new(@container, method_name)
286
+ meth.singleton = @singleton || singleton_method
287
+ handle_consecutive_comment_directive(meth, comment)
288
+ comment.normalize
289
+ comment.extract_call_seq(meth)
290
+ meth.comment = comment
291
+ if node
292
+ tokens = visible_tokens_from_location(node.location)
293
+ line_no = node.location.start_line
294
+ else
295
+ tokens = [file_line_comment_token(line_no)]
296
+ end
297
+ internal_add_method(
298
+ @container,
299
+ meth,
300
+ line_no: line_no,
301
+ visibility: visibility,
302
+ singleton: @singleton || singleton_method,
303
+ params: '()',
304
+ calls_super: false,
305
+ block_params: nil,
306
+ tokens: tokens
307
+ )
308
+ end
309
+ end
310
+
311
+ def normal_comment_treat_as_ghost_method_for_now?(comment_text, line_no) # :nodoc:
312
+ # Meta method comment should start with `##` but some comments does not follow this rule.
313
+ # For now, RDoc accepts them as a meta method comment if there is no node linked to it.
314
+ !@line_nodes[line_no] && comment_text.match?(/^#\s+:(method|singleton-method|attr|attr_reader|attr_writer|attr_accessor):/)
315
+ end
316
+
317
+ def handle_standalone_consecutive_comment_directive(comment, line_no, start_line) # :nodoc:
318
+ if @markup == 'tomdoc'
319
+ parse_comment_tomdoc(@container, comment, line_no, start_line)
320
+ return
321
+ end
322
+
323
+ if comment.text =~ /\A#\#$/ && comment != @first_non_meta_comment
324
+ node = @line_nodes[line_no]
325
+ handle_meta_method_comment(comment, node)
326
+ elsif normal_comment_treat_as_ghost_method_for_now?(comment.text, line_no) && comment != @first_non_meta_comment
327
+ handle_meta_method_comment(comment, nil)
328
+ else
329
+ handle_consecutive_comment_directive(@container, comment)
330
+ end
331
+ end
332
+
333
+ # Processes consecutive comments that were not linked to any documentable code until the given line number
334
+
335
+ def process_comments_until(line_no_until)
336
+ while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
337
+ line_no, start_line, rdoc_comment = @unprocessed_comments.shift
338
+ handle_standalone_consecutive_comment_directive(rdoc_comment, line_no, start_line)
339
+ end
340
+ end
341
+
342
+ # Skips all undocumentable consecutive comments until the given line number.
343
+ # Undocumentable comments are comments written inside `def` or inside undocumentable class/module
344
+
345
+ def skip_comments_until(line_no_until)
346
+ while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
347
+ @unprocessed_comments.shift
348
+ end
349
+ end
350
+
351
+ # Returns consecutive comment linked to the given line number
352
+
353
+ def consecutive_comment(line_no)
354
+ if @unprocessed_comments.first&.first == line_no
355
+ @unprocessed_comments.shift.last
356
+ end
357
+ end
358
+
359
+ def slice_tokens(start_pos, end_pos) # :nodoc:
360
+ start_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> start_pos) >= 0 }
361
+ end_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> end_pos) >= 0 }
362
+ tokens = @tokens[start_index...end_index]
363
+ tokens.pop if tokens.last&.kind == :on_nl
364
+ tokens
365
+ end
366
+
367
+ def file_line_comment_token(line_no) # :nodoc:
368
+ position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no - 1, 0, :on_comment)
369
+ position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
370
+ position_comment
371
+ end
372
+
373
+ # Returns tokens from the given location
374
+
375
+ def visible_tokens_from_location(location)
376
+ position_comment = file_line_comment_token(location.start_line)
377
+ newline_token = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
378
+ indent_token = RDoc::Parser::RipperStateLex::Token.new(location.start_line, 0, :on_sp, ' ' * location.start_character_column)
379
+ tokens = slice_tokens(
380
+ [location.start_line, location.start_character_column],
381
+ [location.end_line, location.end_character_column]
382
+ )
383
+ [position_comment, newline_token, indent_token, *tokens]
384
+ end
385
+
386
+ # Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
387
+
388
+ def change_method_visibility(names, visibility, singleton: @singleton)
389
+ new_methods = []
390
+ @container.methods_matching(names, singleton) do |m|
391
+ if m.parent != @container
392
+ m = m.dup
393
+ record_location(m)
394
+ new_methods << m
395
+ else
396
+ m.visibility = visibility
397
+ end
398
+ end
399
+ new_methods.each do |method|
400
+ case method
401
+ when RDoc::AnyMethod then
402
+ @container.add_method(method)
403
+ when RDoc::Attr then
404
+ @container.add_attribute(method)
405
+ end
406
+ method.visibility = visibility
407
+ end
408
+ end
409
+
410
+ # Handles `module_function :foo, :bar`
411
+
412
+ def change_method_to_module_function(names)
413
+ @container.set_visibility_for(names, :private, false)
414
+ new_methods = []
415
+ @container.methods_matching(names) do |m|
416
+ s_m = m.dup
417
+ record_location(s_m)
418
+ s_m.singleton = true
419
+ new_methods << s_m
420
+ end
421
+ new_methods.each do |method|
422
+ case method
423
+ when RDoc::AnyMethod then
424
+ @container.add_method(method)
425
+ when RDoc::Attr then
426
+ @container.add_attribute(method)
427
+ end
428
+ method.visibility = :public
429
+ end
430
+ end
431
+
432
+ # Handles `alias foo bar` and `alias_method :foo, :bar`
433
+
434
+ def add_alias_method(old_name, new_name, line_no)
435
+ comment = consecutive_comment(line_no)
436
+ handle_consecutive_comment_directive(@container, comment)
437
+ visibility = @container.find_method(old_name, @singleton)&.visibility || :public
438
+ a = RDoc::Alias.new(nil, old_name, new_name, comment, @singleton)
439
+ a.comment = comment
440
+ handle_modifier_directive(a, line_no)
441
+ a.store = @store
442
+ a.line = line_no
443
+ record_location(a)
444
+ if should_document?(a)
445
+ @container.add_alias(a)
446
+ @container.find_method(new_name, @singleton)&.visibility = visibility
447
+ end
448
+ end
449
+
450
+ # Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
451
+
452
+ def add_attributes(names, rw, line_no)
453
+ comment = consecutive_comment(line_no)
454
+ handle_consecutive_comment_directive(@container, comment)
455
+ return unless @container.document_children
456
+
457
+ names.each do |symbol|
458
+ a = RDoc::Attr.new(nil, symbol.to_s, rw, comment)
459
+ a.store = @store
460
+ a.line = line_no
461
+ a.singleton = @singleton
462
+ record_location(a)
463
+ handle_modifier_directive(a, line_no)
464
+ @container.add_attribute(a) if should_document?(a)
465
+ a.visibility = visibility # should set after adding to container
466
+ end
467
+ end
468
+
469
+ def add_includes_extends(names, rdoc_class, line_no) # :nodoc:
470
+ comment = consecutive_comment(line_no)
471
+ handle_consecutive_comment_directive(@container, comment)
472
+ names.each do |name|
473
+ ie = @container.add(rdoc_class, name, '')
474
+ ie.store = @store
475
+ ie.line = line_no
476
+ ie.comment = comment
477
+ record_location(ie)
478
+ end
479
+ end
480
+
481
+ # Handle `include Foo, Bar`
482
+
483
+ def add_includes(names, line_no) # :nodoc:
484
+ add_includes_extends(names, RDoc::Include, line_no)
485
+ end
486
+
487
+ # Handle `extend Foo, Bar`
488
+
489
+ def add_extends(names, line_no) # :nodoc:
490
+ add_includes_extends(names, RDoc::Extend, line_no)
491
+ end
492
+
493
+ # Adds a method defined by `def` syntax
494
+
495
+ def add_method(name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, end_line:)
496
+ receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
497
+ meth = RDoc::AnyMethod.new(nil, name)
498
+ if (comment = consecutive_comment(start_line))
499
+ handle_consecutive_comment_directive(@container, comment)
500
+ handle_consecutive_comment_directive(meth, comment)
501
+
502
+ comment.normalize
503
+ comment.extract_call_seq(meth)
504
+ meth.comment = comment
505
+ end
506
+ handle_modifier_directive(meth, start_line)
507
+ handle_modifier_directive(meth, end_line)
508
+ return unless should_document?(meth)
509
+
510
+
511
+ if meth.name == 'initialize' && !singleton
512
+ if meth.dont_rename_initialize
513
+ visibility = :protected
514
+ else
515
+ meth.name = 'new'
516
+ singleton = true
517
+ visibility = :public
518
+ end
519
+ end
520
+
521
+ internal_add_method(
522
+ receiver,
523
+ meth,
524
+ line_no: start_line,
525
+ visibility: visibility,
526
+ singleton: singleton,
527
+ params: params,
528
+ calls_super: calls_super,
529
+ block_params: block_params,
530
+ tokens: tokens
531
+ )
532
+ end
533
+
534
+ private def internal_add_method(container, meth, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc:
535
+ meth.name ||= meth.call_seq[/\A[^()\s]+/] if meth.call_seq
536
+ meth.name ||= 'unknown'
537
+ meth.store = @store
538
+ meth.line = line_no
539
+ meth.singleton = singleton
540
+ container.add_method(meth) # should add after setting singleton and before setting visibility
541
+ meth.visibility = visibility
542
+ meth.params ||= params
543
+ meth.calls_super = calls_super
544
+ meth.block_params ||= block_params if block_params
545
+ record_location(meth)
546
+ meth.start_collecting_tokens
547
+ tokens.each do |token|
548
+ meth.token_stream << token
549
+ end
550
+ end
551
+
552
+ # Find or create module or class from a given module name.
553
+ # If module or class does not exist, creates a module or a class according to `create_mode` argument.
554
+
555
+ def find_or_create_module_path(module_name, create_mode)
556
+ root_name, *path, name = module_name.split('::')
557
+ add_module = ->(mod, name, mode) {
558
+ case mode
559
+ when :class
560
+ mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store }
561
+ when :module
562
+ mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store }
563
+ end
564
+ }
565
+ if root_name.empty?
566
+ mod = @top_level
567
+ else
568
+ @module_nesting.reverse_each do |nesting|
569
+ mod = nesting.find_module_named(root_name)
570
+ break if mod
571
+ end
572
+ return mod || add_module.call(@top_level, root_name, create_mode) unless name
573
+ mod ||= add_module.call(@top_level, root_name, :module)
574
+ end
575
+ path.each do |name|
576
+ mod = mod.find_module_named(name) || add_module.call(mod, name, :module)
577
+ end
578
+ mod.find_module_named(name) || add_module.call(mod, name, create_mode)
579
+ end
580
+
581
+ # Resolves constant path to a full path by searching module nesting
582
+
583
+ def resolve_constant_path(constant_path)
584
+ owner_name, path = constant_path.split('::', 2)
585
+ return constant_path if owner_name.empty? # ::Foo, ::Foo::Bar
586
+ mod = nil
587
+ @module_nesting.reverse_each do |nesting|
588
+ mod = nesting.find_module_named(owner_name)
589
+ break if mod
590
+ end
591
+ mod ||= @top_level.find_module_named(owner_name)
592
+ [mod.full_name, path].compact.join('::') if mod
593
+ end
594
+
595
+ # Returns a pair of owner module and constant name from a given constant path.
596
+ # Creates owner module if it does not exist.
597
+
598
+ def find_or_create_constant_owner_name(constant_path)
599
+ const_path, colon, name = constant_path.rpartition('::')
600
+ if colon.empty? # class Foo
601
+ [@container, name]
602
+ elsif const_path.empty? # class ::Foo
603
+ [@top_level, name]
604
+ else # `class Foo::Bar` or `class ::Foo::Bar`
605
+ [find_or_create_module_path(const_path, :module), name]
606
+ end
607
+ end
608
+
609
+ # Adds a constant
610
+
611
+ def add_constant(constant_name, rhs_name, start_line, end_line)
612
+ comment = consecutive_comment(start_line)
613
+ handle_consecutive_comment_directive(@container, comment)
614
+ owner, name = find_or_create_constant_owner_name(constant_name)
615
+ constant = RDoc::Constant.new(name, rhs_name, comment)
616
+ constant.store = @store
617
+ constant.line = start_line
618
+ record_location(constant)
619
+ handle_modifier_directive(constant, start_line)
620
+ handle_modifier_directive(constant, end_line)
621
+ owner.add_constant(constant)
622
+ mod =
623
+ if rhs_name =~ /^::/
624
+ @store.find_class_or_module(rhs_name)
625
+ else
626
+ @container.find_module_named(rhs_name)
627
+ end
628
+ if mod && constant.document_self
629
+ a = @container.add_module_alias(mod, rhs_name, constant, @top_level)
630
+ a.store = @store
631
+ a.line = start_line
632
+ record_location(a)
633
+ end
634
+ end
635
+
636
+ # Adds module or class
637
+
638
+ def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil)
639
+ comment = consecutive_comment(start_line)
640
+ handle_consecutive_comment_directive(@container, comment)
641
+ return unless @container.document_children
642
+
643
+ owner, name = find_or_create_constant_owner_name(module_name)
644
+ if is_class
645
+ mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')
646
+
647
+ # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information.
648
+ # We need to fix it when RDoc::NormalClass resolved to a wrong constant name
649
+ if superclass_name
650
+ superclass_full_path = resolve_constant_path(superclass_name)
651
+ superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path
652
+ superclass_full_path ||= superclass_name
653
+ if superclass
654
+ mod.superclass = superclass
655
+ elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path
656
+ mod.superclass = superclass_full_path
657
+ end
658
+ end
659
+ else
660
+ mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name)
661
+ end
662
+
663
+ mod.store = @store
664
+ mod.line = start_line
665
+ record_location(mod)
666
+ handle_modifier_directive(mod, start_line)
667
+ handle_modifier_directive(mod, end_line)
668
+ mod.add_comment(comment, @top_level) if comment
669
+ mod
670
+ end
671
+
672
+ class RDocVisitor < Prism::Visitor # :nodoc:
673
+ def initialize(scanner, top_level, store)
674
+ @scanner = scanner
675
+ @top_level = top_level
676
+ @store = store
677
+ end
678
+
679
+ def visit_call_node(node)
680
+ @scanner.process_comments_until(node.location.start_line - 1)
681
+ if node.receiver.nil?
682
+ case node.name
683
+ when :attr
684
+ _visit_call_attr_reader_writer_accessor(node, 'R')
685
+ when :attr_reader
686
+ _visit_call_attr_reader_writer_accessor(node, 'R')
687
+ when :attr_writer
688
+ _visit_call_attr_reader_writer_accessor(node, 'W')
689
+ when :attr_accessor
690
+ _visit_call_attr_reader_writer_accessor(node, 'RW')
691
+ when :include
692
+ _visit_call_include(node)
693
+ when :extend
694
+ _visit_call_extend(node)
695
+ when :public
696
+ _visit_call_public_private_protected(node, :public) { super }
697
+ when :private
698
+ _visit_call_public_private_protected(node, :private) { super }
699
+ when :protected
700
+ _visit_call_public_private_protected(node, :protected) { super }
701
+ when :private_constant
702
+ _visit_call_private_constant(node)
703
+ when :public_constant
704
+ _visit_call_public_constant(node)
705
+ when :require
706
+ _visit_call_require(node)
707
+ when :alias_method
708
+ _visit_call_alias_method(node)
709
+ when :module_function
710
+ _visit_call_module_function(node) { super }
711
+ when :public_class_method
712
+ _visit_call_public_private_class_method(node, :public) { super }
713
+ when :private_class_method
714
+ _visit_call_public_private_class_method(node, :private) { super }
715
+ else
716
+ super
717
+ end
718
+ else
719
+ super
720
+ end
721
+ end
722
+
723
+ def visit_alias_method_node(node)
724
+ @scanner.process_comments_until(node.location.start_line - 1)
725
+ return unless node.old_name.is_a?(Prism::SymbolNode) && node.new_name.is_a?(Prism::SymbolNode)
726
+ @scanner.add_alias_method(node.old_name.value.to_s, node.new_name.value.to_s, node.location.start_line)
727
+ end
728
+
729
+ def visit_module_node(node)
730
+ @scanner.process_comments_until(node.location.start_line - 1)
731
+ module_name = constant_path_string(node.constant_path)
732
+ mod = @scanner.add_module_or_class(module_name, node.location.start_line, node.location.end_line) if module_name
733
+ if mod
734
+ @scanner.with_container(mod) do
735
+ super
736
+ @scanner.process_comments_until(node.location.end_line)
737
+ end
738
+ else
739
+ @scanner.skip_comments_until(node.location.end_line)
740
+ end
741
+ end
742
+
743
+ def visit_class_node(node)
744
+ @scanner.process_comments_until(node.location.start_line - 1)
745
+ superclass_name = constant_path_string(node.superclass) if node.superclass
746
+ class_name = constant_path_string(node.constant_path)
747
+ klass = @scanner.add_module_or_class(class_name, node.location.start_line, node.location.end_line, is_class: true, superclass_name: superclass_name) if class_name
748
+ if klass
749
+ @scanner.with_container(klass) do
750
+ super
751
+ @scanner.process_comments_until(node.location.end_line)
752
+ end
753
+ else
754
+ @scanner.skip_comments_until(node.location.end_line)
755
+ end
756
+ end
757
+
758
+ def visit_singleton_class_node(node)
759
+ @scanner.process_comments_until(node.location.start_line - 1)
760
+
761
+ expression = node.expression
762
+ expression = expression.body.body.first if expression.is_a?(Prism::ParenthesesNode) && expression.body&.body&.size == 1
763
+
764
+ case expression
765
+ when Prism::ConstantWriteNode
766
+ # Accept `class << (NameErrorCheckers = Object.new)` as a module which is not actually a module
767
+ mod = @scanner.container.add_module(RDoc::NormalModule, expression.name.to_s)
768
+ when Prism::ConstantPathNode, Prism::ConstantReadNode
769
+ expression_name = constant_path_string(expression)
770
+ # If a constant_path does not exist, RDoc creates a module
771
+ mod = @scanner.find_or_create_module_path(expression_name, :module) if expression_name
772
+ when Prism::SelfNode
773
+ mod = @scanner.container if @scanner.container != @top_level
774
+ end
775
+ if mod
776
+ @scanner.with_container(mod, singleton: true) do
777
+ super
778
+ @scanner.process_comments_until(node.location.end_line)
779
+ end
780
+ else
781
+ @scanner.skip_comments_until(node.location.end_line)
782
+ end
783
+ end
784
+
785
+ def visit_def_node(node)
786
+ start_line = node.location.start_line
787
+ end_line = node.location.end_line
788
+ @scanner.process_comments_until(start_line - 1)
789
+
790
+ case node.receiver
791
+ when Prism::NilNode, Prism::TrueNode, Prism::FalseNode
792
+ visibility = :public
793
+ singleton = false
794
+ receiver_name =
795
+ case node.receiver
796
+ when Prism::NilNode
797
+ 'NilClass'
798
+ when Prism::TrueNode
799
+ 'TrueClass'
800
+ when Prism::FalseNode
801
+ 'FalseClass'
802
+ end
803
+ receiver_fallback_type = :class
804
+ when Prism::SelfNode
805
+ # singleton method of a singleton class is not documentable
806
+ return if @scanner.singleton
807
+ visibility = :public
808
+ singleton = true
809
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
810
+ visibility = :public
811
+ singleton = true
812
+ receiver_name = constant_path_string(node.receiver)
813
+ receiver_fallback_type = :module
814
+ return unless receiver_name
815
+ when nil
816
+ visibility = @scanner.visibility
817
+ singleton = @scanner.singleton
818
+ else
819
+ # `def (unknown expression).method_name` is not documentable
820
+ return
821
+ end
822
+ name = node.name.to_s
823
+ params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node)
824
+ tokens = @scanner.visible_tokens_from_location(node.location)
825
+
826
+ @scanner.add_method(
827
+ name,
828
+ receiver_name: receiver_name,
829
+ receiver_fallback_type: receiver_fallback_type,
830
+ visibility: visibility,
831
+ singleton: singleton,
832
+ params: params,
833
+ block_params: block_params,
834
+ calls_super: calls_super,
835
+ tokens: tokens,
836
+ start_line: start_line,
837
+ end_line: end_line
838
+ )
839
+ ensure
840
+ @scanner.skip_comments_until(end_line)
841
+ end
842
+
843
+ def visit_constant_path_write_node(node)
844
+ @scanner.process_comments_until(node.location.start_line - 1)
845
+ path = constant_path_string(node.target)
846
+ return unless path
847
+
848
+ @scanner.add_constant(
849
+ path,
850
+ constant_path_string(node.value) || node.value.slice,
851
+ node.location.start_line,
852
+ node.location.end_line
853
+ )
854
+ @scanner.skip_comments_until(node.location.end_line)
855
+ # Do not traverse rhs not to document `A::B = Struct.new{def undocumentable_method; end}`
856
+ end
857
+
858
+ def visit_constant_write_node(node)
859
+ @scanner.process_comments_until(node.location.start_line - 1)
860
+ @scanner.add_constant(
861
+ node.name.to_s,
862
+ constant_path_string(node.value) || node.value.slice,
863
+ node.location.start_line,
864
+ node.location.end_line
865
+ )
866
+ @scanner.skip_comments_until(node.location.end_line)
867
+ # Do not traverse rhs not to document `A = Struct.new{def undocumentable_method; end}`
868
+ end
869
+
870
+ private
871
+
872
+ def constant_arguments_names(call_node)
873
+ return unless call_node.arguments
874
+ names = call_node.arguments.arguments.map { |arg| constant_path_string(arg) }
875
+ names.all? ? names : nil
876
+ end
877
+
878
+ def symbol_arguments(call_node)
879
+ arguments_node = call_node.arguments
880
+ return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)}
881
+ arguments_node.arguments.map { |arg| arg.value.to_sym }
882
+ end
883
+
884
+ def visibility_method_arguments(call_node, singleton:)
885
+ arguments_node = call_node.arguments
886
+ return unless arguments_node
887
+ symbols = symbol_arguments(call_node)
888
+ if symbols
889
+ # module_function :foo, :bar
890
+ return symbols.map(&:to_s)
891
+ else
892
+ return unless arguments_node.arguments.size == 1
893
+ arg = arguments_node.arguments.first
894
+ return unless arg.is_a?(Prism::DefNode)
895
+
896
+ if singleton
897
+ # `private_class_method def foo; end` `private_class_method def not_self.foo; end` should be ignored
898
+ return unless arg.receiver.is_a?(Prism::SelfNode)
899
+ else
900
+ # `module_function def something.foo` should be ignored
901
+ return if arg.receiver
902
+ end
903
+ # `module_function def foo; end` or `private_class_method def self.foo; end`
904
+ [arg.name.to_s]
905
+ end
906
+ end
907
+
908
+ def constant_path_string(node)
909
+ case node
910
+ when Prism::ConstantReadNode
911
+ node.name.to_s
912
+ when Prism::ConstantPathNode
913
+ parent_name = node.parent ? constant_path_string(node.parent) : ''
914
+ "#{parent_name}::#{node.name}" if parent_name
915
+ end
916
+ end
917
+
918
+ def _visit_call_require(call_node)
919
+ return unless call_node.arguments&.arguments&.size == 1
920
+ arg = call_node.arguments.arguments.first
921
+ return unless arg.is_a?(Prism::StringNode)
922
+ @scanner.container.add_require(RDoc::Require.new(arg.unescaped, nil))
923
+ end
924
+
925
+ def _visit_call_module_function(call_node)
926
+ yield
927
+ return if @scanner.singleton
928
+ names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s)
929
+ @scanner.change_method_to_module_function(names) if names
930
+ end
931
+
932
+ def _visit_call_public_private_class_method(call_node, visibility)
933
+ yield
934
+ return if @scanner.singleton
935
+ names = visibility_method_arguments(call_node, singleton: true)
936
+ @scanner.change_method_visibility(names, visibility, singleton: true) if names
937
+ end
938
+
939
+ def _visit_call_public_private_protected(call_node, visibility)
940
+ arguments_node = call_node.arguments
941
+ if arguments_node.nil? # `public` `private`
942
+ @scanner.visibility = visibility
943
+ else # `public :foo, :bar`, `private def foo; end`
944
+ yield
945
+ names = visibility_method_arguments(call_node, singleton: @scanner.singleton)
946
+ @scanner.change_method_visibility(names, visibility) if names
947
+ end
948
+ end
949
+
950
+ def _visit_call_alias_method(call_node)
951
+ new_name, old_name, *rest = symbol_arguments(call_node)
952
+ return unless old_name && new_name && rest.empty?
953
+ @scanner.add_alias_method(old_name.to_s, new_name.to_s, call_node.location.start_line)
954
+ end
955
+
956
+ def _visit_call_include(call_node)
957
+ names = constant_arguments_names(call_node)
958
+ line_no = call_node.location.start_line
959
+ return unless names
960
+
961
+ if @scanner.singleton
962
+ @scanner.add_extends(names, line_no)
963
+ else
964
+ @scanner.add_includes(names, line_no)
965
+ end
966
+ end
967
+
968
+ def _visit_call_extend(call_node)
969
+ names = constant_arguments_names(call_node)
970
+ @scanner.add_extends(names, call_node.location.start_line) if names && !@scanner.singleton
971
+ end
972
+
973
+ def _visit_call_public_constant(call_node)
974
+ return if @scanner.singleton
975
+ names = symbol_arguments(call_node)
976
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :public) if names
977
+ end
978
+
979
+ def _visit_call_private_constant(call_node)
980
+ return if @scanner.singleton
981
+ names = symbol_arguments(call_node)
982
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :private) if names
983
+ end
984
+
985
+ def _visit_call_attr_reader_writer_accessor(call_node, rw)
986
+ names = symbol_arguments(call_node)
987
+ @scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names
988
+ end
989
+ class MethodSignatureVisitor < Prism::Visitor # :nodoc:
990
+ class << self
991
+ def scan_signature(def_node)
992
+ visitor = new
993
+ def_node.body&.accept(visitor)
994
+ params = "(#{def_node.parameters&.slice})"
995
+ block_params = visitor.yields.first
996
+ [params, block_params, visitor.calls_super]
997
+ end
998
+ end
999
+
1000
+ attr_reader :params, :yields, :calls_super
1001
+
1002
+ def initialize
1003
+ @params = nil
1004
+ @calls_super = false
1005
+ @yields = []
1006
+ end
1007
+
1008
+ def visit_def_node(node)
1009
+ # stop traverse inside nested def
1010
+ end
1011
+
1012
+ def visit_yield_node(node)
1013
+ @yields << (node.arguments&.slice || '')
1014
+ end
1015
+
1016
+ def visit_super_node(node)
1017
+ @calls_super = true
1018
+ super
1019
+ end
1020
+
1021
+ def visit_forwarding_super_node(node)
1022
+ @calls_super = true
1023
+ end
1024
+ end
1025
+ end
1026
+ end