rdoc 6.8.1 → 6.9.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.
@@ -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