rdoc 6.7.0 → 6.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/ExampleMarkdown.md +2 -0
  3. data/ExampleRDoc.rdoc +2 -0
  4. data/History.rdoc +64 -62
  5. data/LICENSE.rdoc +2 -0
  6. data/README.rdoc +13 -0
  7. data/RI.md +842 -0
  8. data/TODO.rdoc +8 -7
  9. data/lib/rdoc/{alias.rb → code_object/alias.rb} +1 -1
  10. data/lib/rdoc/{class_module.rb → code_object/class_module.rb} +68 -1
  11. data/lib/rdoc/{method_attr.rb → code_object/method_attr.rb} +17 -5
  12. data/lib/rdoc/{top_level.rb → code_object/top_level.rb} +4 -4
  13. data/lib/rdoc/code_object.rb +6 -0
  14. data/lib/rdoc/generator/darkfish.rb +45 -3
  15. data/lib/rdoc/generator/pot/message_extractor.rb +1 -1
  16. data/lib/rdoc/generator/pot/po_entry.rb +1 -1
  17. data/lib/rdoc/generator/template/darkfish/_head.rhtml +23 -0
  18. data/lib/rdoc/generator/template/darkfish/_sidebar_classes.rhtml +1 -0
  19. data/lib/rdoc/generator/template/darkfish/_sidebar_methods.rhtml +20 -11
  20. data/lib/rdoc/generator/template/darkfish/_sidebar_parent.rhtml +3 -8
  21. data/lib/rdoc/generator/template/darkfish/_sidebar_toggle.rhtml +3 -0
  22. data/lib/rdoc/generator/template/darkfish/class.rhtml +69 -43
  23. data/lib/rdoc/generator/template/darkfish/css/rdoc.css +380 -399
  24. data/lib/rdoc/generator/template/darkfish/index.rhtml +7 -6
  25. data/lib/rdoc/generator/template/darkfish/js/darkfish.js +24 -1
  26. data/lib/rdoc/generator/template/darkfish/page.rhtml +5 -5
  27. data/lib/rdoc/generator/template/darkfish/servlet_not_found.rhtml +10 -8
  28. data/lib/rdoc/generator/template/darkfish/servlet_root.rhtml +5 -2
  29. data/lib/rdoc/generator/template/darkfish/table_of_contents.rhtml +11 -0
  30. data/lib/rdoc/markdown.kpeg +1 -1
  31. data/lib/rdoc/markdown.rb +21 -11
  32. data/lib/rdoc/markup/attribute_manager.rb +2 -2
  33. data/lib/rdoc/markup/formatter.rb +19 -12
  34. data/lib/rdoc/markup/pre_process.rb +26 -6
  35. data/lib/rdoc/markup/to_bs.rb +1 -1
  36. data/lib/rdoc/markup/to_html.rb +1 -1
  37. data/lib/rdoc/markup/to_html_crossref.rb +63 -12
  38. data/lib/rdoc/markup/to_rdoc.rb +5 -5
  39. data/lib/rdoc/markup.rb +18 -13
  40. data/lib/rdoc/options.rb +78 -12
  41. data/lib/rdoc/parser/c.rb +25 -1
  42. data/lib/rdoc/parser/changelog.rb +2 -2
  43. data/lib/rdoc/parser/prism_ruby.rb +1028 -0
  44. data/lib/rdoc/parser/ripper_state_lex.rb +7 -305
  45. data/lib/rdoc/parser/ruby.rb +15 -6
  46. data/lib/rdoc/parser.rb +2 -1
  47. data/lib/rdoc/rd/block_parser.rb +3 -3
  48. data/lib/rdoc/rd/inline_parser.rb +3 -3
  49. data/lib/rdoc/rdoc.rb +6 -3
  50. data/lib/rdoc/ri/driver.rb +58 -14
  51. data/lib/rdoc/rubygems_hook.rb +90 -8
  52. data/lib/rdoc/store.rb +12 -0
  53. data/lib/rdoc/task.rb +2 -3
  54. data/lib/rdoc/tom_doc.rb +1 -7
  55. data/lib/rdoc/version.rb +1 -1
  56. data/lib/rdoc.rb +22 -24
  57. data/lib/rubygems_plugin.rb +23 -0
  58. metadata +27 -26
  59. data/RI.rdoc +0 -57
  60. data/lib/rdoc/generator/template/darkfish/.document +0 -0
  61. data/lib/rdoc/generator/template/json_index/.document +0 -1
  62. /data/lib/rdoc/{anon_class.rb → code_object/anon_class.rb} +0 -0
  63. /data/lib/rdoc/{any_method.rb → code_object/any_method.rb} +0 -0
  64. /data/lib/rdoc/{attr.rb → code_object/attr.rb} +0 -0
  65. /data/lib/rdoc/{constant.rb → code_object/constant.rb} +0 -0
  66. /data/lib/rdoc/{context → code_object/context}/section.rb +0 -0
  67. /data/lib/rdoc/{context.rb → code_object/context.rb} +0 -0
  68. /data/lib/rdoc/{extend.rb → code_object/extend.rb} +0 -0
  69. /data/lib/rdoc/{ghost_method.rb → code_object/ghost_method.rb} +0 -0
  70. /data/lib/rdoc/{include.rb → code_object/include.rb} +0 -0
  71. /data/lib/rdoc/{meta_method.rb → code_object/meta_method.rb} +0 -0
  72. /data/lib/rdoc/{mixin.rb → code_object/mixin.rb} +0 -0
  73. /data/lib/rdoc/{normal_class.rb → code_object/normal_class.rb} +0 -0
  74. /data/lib/rdoc/{normal_module.rb → code_object/normal_module.rb} +0 -0
  75. /data/lib/rdoc/{require.rb → code_object/require.rb} +0 -0
  76. /data/lib/rdoc/{single_class.rb → code_object/single_class.rb} +0 -0
@@ -0,0 +1,1028 @@
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
+ # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information.
646
+ # We need to fix it when RDoc::NormalClass resolved to a wrong constant name
647
+ if superclass_name
648
+ superclass_full_path = resolve_constant_path(superclass_name)
649
+ superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path
650
+ superclass_full_path ||= superclass_name
651
+ end
652
+ # add_class should be done after resolving superclass
653
+ mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')
654
+ if superclass_name
655
+ if superclass
656
+ mod.superclass = superclass
657
+ elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path
658
+ mod.superclass = superclass_full_path
659
+ end
660
+ end
661
+ else
662
+ mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name)
663
+ end
664
+
665
+ mod.store = @store
666
+ mod.line = start_line
667
+ record_location(mod)
668
+ handle_modifier_directive(mod, start_line)
669
+ handle_modifier_directive(mod, end_line)
670
+ mod.add_comment(comment, @top_level) if comment
671
+ mod
672
+ end
673
+
674
+ class RDocVisitor < Prism::Visitor # :nodoc:
675
+ def initialize(scanner, top_level, store)
676
+ @scanner = scanner
677
+ @top_level = top_level
678
+ @store = store
679
+ end
680
+
681
+ def visit_call_node(node)
682
+ @scanner.process_comments_until(node.location.start_line - 1)
683
+ if node.receiver.nil?
684
+ case node.name
685
+ when :attr
686
+ _visit_call_attr_reader_writer_accessor(node, 'R')
687
+ when :attr_reader
688
+ _visit_call_attr_reader_writer_accessor(node, 'R')
689
+ when :attr_writer
690
+ _visit_call_attr_reader_writer_accessor(node, 'W')
691
+ when :attr_accessor
692
+ _visit_call_attr_reader_writer_accessor(node, 'RW')
693
+ when :include
694
+ _visit_call_include(node)
695
+ when :extend
696
+ _visit_call_extend(node)
697
+ when :public
698
+ _visit_call_public_private_protected(node, :public) { super }
699
+ when :private
700
+ _visit_call_public_private_protected(node, :private) { super }
701
+ when :protected
702
+ _visit_call_public_private_protected(node, :protected) { super }
703
+ when :private_constant
704
+ _visit_call_private_constant(node)
705
+ when :public_constant
706
+ _visit_call_public_constant(node)
707
+ when :require
708
+ _visit_call_require(node)
709
+ when :alias_method
710
+ _visit_call_alias_method(node)
711
+ when :module_function
712
+ _visit_call_module_function(node) { super }
713
+ when :public_class_method
714
+ _visit_call_public_private_class_method(node, :public) { super }
715
+ when :private_class_method
716
+ _visit_call_public_private_class_method(node, :private) { super }
717
+ else
718
+ super
719
+ end
720
+ else
721
+ super
722
+ end
723
+ end
724
+
725
+ def visit_alias_method_node(node)
726
+ @scanner.process_comments_until(node.location.start_line - 1)
727
+ return unless node.old_name.is_a?(Prism::SymbolNode) && node.new_name.is_a?(Prism::SymbolNode)
728
+ @scanner.add_alias_method(node.old_name.value.to_s, node.new_name.value.to_s, node.location.start_line)
729
+ end
730
+
731
+ def visit_module_node(node)
732
+ @scanner.process_comments_until(node.location.start_line - 1)
733
+ module_name = constant_path_string(node.constant_path)
734
+ mod = @scanner.add_module_or_class(module_name, node.location.start_line, node.location.end_line) if module_name
735
+ if mod
736
+ @scanner.with_container(mod) do
737
+ super
738
+ @scanner.process_comments_until(node.location.end_line)
739
+ end
740
+ else
741
+ @scanner.skip_comments_until(node.location.end_line)
742
+ end
743
+ end
744
+
745
+ def visit_class_node(node)
746
+ @scanner.process_comments_until(node.location.start_line - 1)
747
+ superclass_name = constant_path_string(node.superclass) if node.superclass
748
+ class_name = constant_path_string(node.constant_path)
749
+ 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
750
+ if klass
751
+ @scanner.with_container(klass) do
752
+ super
753
+ @scanner.process_comments_until(node.location.end_line)
754
+ end
755
+ else
756
+ @scanner.skip_comments_until(node.location.end_line)
757
+ end
758
+ end
759
+
760
+ def visit_singleton_class_node(node)
761
+ @scanner.process_comments_until(node.location.start_line - 1)
762
+
763
+ expression = node.expression
764
+ expression = expression.body.body.first if expression.is_a?(Prism::ParenthesesNode) && expression.body&.body&.size == 1
765
+
766
+ case expression
767
+ when Prism::ConstantWriteNode
768
+ # Accept `class << (NameErrorCheckers = Object.new)` as a module which is not actually a module
769
+ mod = @scanner.container.add_module(RDoc::NormalModule, expression.name.to_s)
770
+ when Prism::ConstantPathNode, Prism::ConstantReadNode
771
+ expression_name = constant_path_string(expression)
772
+ # If a constant_path does not exist, RDoc creates a module
773
+ mod = @scanner.find_or_create_module_path(expression_name, :module) if expression_name
774
+ when Prism::SelfNode
775
+ mod = @scanner.container if @scanner.container != @top_level
776
+ end
777
+ if mod
778
+ @scanner.with_container(mod, singleton: true) do
779
+ super
780
+ @scanner.process_comments_until(node.location.end_line)
781
+ end
782
+ else
783
+ @scanner.skip_comments_until(node.location.end_line)
784
+ end
785
+ end
786
+
787
+ def visit_def_node(node)
788
+ start_line = node.location.start_line
789
+ end_line = node.location.end_line
790
+ @scanner.process_comments_until(start_line - 1)
791
+
792
+ case node.receiver
793
+ when Prism::NilNode, Prism::TrueNode, Prism::FalseNode
794
+ visibility = :public
795
+ singleton = false
796
+ receiver_name =
797
+ case node.receiver
798
+ when Prism::NilNode
799
+ 'NilClass'
800
+ when Prism::TrueNode
801
+ 'TrueClass'
802
+ when Prism::FalseNode
803
+ 'FalseClass'
804
+ end
805
+ receiver_fallback_type = :class
806
+ when Prism::SelfNode
807
+ # singleton method of a singleton class is not documentable
808
+ return if @scanner.singleton
809
+ visibility = :public
810
+ singleton = true
811
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
812
+ visibility = :public
813
+ singleton = true
814
+ receiver_name = constant_path_string(node.receiver)
815
+ receiver_fallback_type = :module
816
+ return unless receiver_name
817
+ when nil
818
+ visibility = @scanner.visibility
819
+ singleton = @scanner.singleton
820
+ else
821
+ # `def (unknown expression).method_name` is not documentable
822
+ return
823
+ end
824
+ name = node.name.to_s
825
+ params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node)
826
+ tokens = @scanner.visible_tokens_from_location(node.location)
827
+
828
+ @scanner.add_method(
829
+ name,
830
+ receiver_name: receiver_name,
831
+ receiver_fallback_type: receiver_fallback_type,
832
+ visibility: visibility,
833
+ singleton: singleton,
834
+ params: params,
835
+ block_params: block_params,
836
+ calls_super: calls_super,
837
+ tokens: tokens,
838
+ start_line: start_line,
839
+ end_line: end_line
840
+ )
841
+ ensure
842
+ @scanner.skip_comments_until(end_line)
843
+ end
844
+
845
+ def visit_constant_path_write_node(node)
846
+ @scanner.process_comments_until(node.location.start_line - 1)
847
+ path = constant_path_string(node.target)
848
+ return unless path
849
+
850
+ @scanner.add_constant(
851
+ path,
852
+ constant_path_string(node.value) || node.value.slice,
853
+ node.location.start_line,
854
+ node.location.end_line
855
+ )
856
+ @scanner.skip_comments_until(node.location.end_line)
857
+ # Do not traverse rhs not to document `A::B = Struct.new{def undocumentable_method; end}`
858
+ end
859
+
860
+ def visit_constant_write_node(node)
861
+ @scanner.process_comments_until(node.location.start_line - 1)
862
+ @scanner.add_constant(
863
+ node.name.to_s,
864
+ constant_path_string(node.value) || node.value.slice,
865
+ node.location.start_line,
866
+ node.location.end_line
867
+ )
868
+ @scanner.skip_comments_until(node.location.end_line)
869
+ # Do not traverse rhs not to document `A = Struct.new{def undocumentable_method; end}`
870
+ end
871
+
872
+ private
873
+
874
+ def constant_arguments_names(call_node)
875
+ return unless call_node.arguments
876
+ names = call_node.arguments.arguments.map { |arg| constant_path_string(arg) }
877
+ names.all? ? names : nil
878
+ end
879
+
880
+ def symbol_arguments(call_node)
881
+ arguments_node = call_node.arguments
882
+ return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)}
883
+ arguments_node.arguments.map { |arg| arg.value.to_sym }
884
+ end
885
+
886
+ def visibility_method_arguments(call_node, singleton:)
887
+ arguments_node = call_node.arguments
888
+ return unless arguments_node
889
+ symbols = symbol_arguments(call_node)
890
+ if symbols
891
+ # module_function :foo, :bar
892
+ return symbols.map(&:to_s)
893
+ else
894
+ return unless arguments_node.arguments.size == 1
895
+ arg = arguments_node.arguments.first
896
+ return unless arg.is_a?(Prism::DefNode)
897
+
898
+ if singleton
899
+ # `private_class_method def foo; end` `private_class_method def not_self.foo; end` should be ignored
900
+ return unless arg.receiver.is_a?(Prism::SelfNode)
901
+ else
902
+ # `module_function def something.foo` should be ignored
903
+ return if arg.receiver
904
+ end
905
+ # `module_function def foo; end` or `private_class_method def self.foo; end`
906
+ [arg.name.to_s]
907
+ end
908
+ end
909
+
910
+ def constant_path_string(node)
911
+ case node
912
+ when Prism::ConstantReadNode
913
+ node.name.to_s
914
+ when Prism::ConstantPathNode
915
+ parent_name = node.parent ? constant_path_string(node.parent) : ''
916
+ "#{parent_name}::#{node.name}" if parent_name
917
+ end
918
+ end
919
+
920
+ def _visit_call_require(call_node)
921
+ return unless call_node.arguments&.arguments&.size == 1
922
+ arg = call_node.arguments.arguments.first
923
+ return unless arg.is_a?(Prism::StringNode)
924
+ @scanner.container.add_require(RDoc::Require.new(arg.unescaped, nil))
925
+ end
926
+
927
+ def _visit_call_module_function(call_node)
928
+ yield
929
+ return if @scanner.singleton
930
+ names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s)
931
+ @scanner.change_method_to_module_function(names) if names
932
+ end
933
+
934
+ def _visit_call_public_private_class_method(call_node, visibility)
935
+ yield
936
+ return if @scanner.singleton
937
+ names = visibility_method_arguments(call_node, singleton: true)
938
+ @scanner.change_method_visibility(names, visibility, singleton: true) if names
939
+ end
940
+
941
+ def _visit_call_public_private_protected(call_node, visibility)
942
+ arguments_node = call_node.arguments
943
+ if arguments_node.nil? # `public` `private`
944
+ @scanner.visibility = visibility
945
+ else # `public :foo, :bar`, `private def foo; end`
946
+ yield
947
+ names = visibility_method_arguments(call_node, singleton: @scanner.singleton)
948
+ @scanner.change_method_visibility(names, visibility) if names
949
+ end
950
+ end
951
+
952
+ def _visit_call_alias_method(call_node)
953
+ new_name, old_name, *rest = symbol_arguments(call_node)
954
+ return unless old_name && new_name && rest.empty?
955
+ @scanner.add_alias_method(old_name.to_s, new_name.to_s, call_node.location.start_line)
956
+ end
957
+
958
+ def _visit_call_include(call_node)
959
+ names = constant_arguments_names(call_node)
960
+ line_no = call_node.location.start_line
961
+ return unless names
962
+
963
+ if @scanner.singleton
964
+ @scanner.add_extends(names, line_no)
965
+ else
966
+ @scanner.add_includes(names, line_no)
967
+ end
968
+ end
969
+
970
+ def _visit_call_extend(call_node)
971
+ names = constant_arguments_names(call_node)
972
+ @scanner.add_extends(names, call_node.location.start_line) if names && !@scanner.singleton
973
+ end
974
+
975
+ def _visit_call_public_constant(call_node)
976
+ return if @scanner.singleton
977
+ names = symbol_arguments(call_node)
978
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :public) if names
979
+ end
980
+
981
+ def _visit_call_private_constant(call_node)
982
+ return if @scanner.singleton
983
+ names = symbol_arguments(call_node)
984
+ @scanner.container.set_constant_visibility_for(names.map(&:to_s), :private) if names
985
+ end
986
+
987
+ def _visit_call_attr_reader_writer_accessor(call_node, rw)
988
+ names = symbol_arguments(call_node)
989
+ @scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names
990
+ end
991
+ class MethodSignatureVisitor < Prism::Visitor # :nodoc:
992
+ class << self
993
+ def scan_signature(def_node)
994
+ visitor = new
995
+ def_node.body&.accept(visitor)
996
+ params = "(#{def_node.parameters&.slice})"
997
+ block_params = visitor.yields.first
998
+ [params, block_params, visitor.calls_super]
999
+ end
1000
+ end
1001
+
1002
+ attr_reader :params, :yields, :calls_super
1003
+
1004
+ def initialize
1005
+ @params = nil
1006
+ @calls_super = false
1007
+ @yields = []
1008
+ end
1009
+
1010
+ def visit_def_node(node)
1011
+ # stop traverse inside nested def
1012
+ end
1013
+
1014
+ def visit_yield_node(node)
1015
+ @yields << (node.arguments&.slice || '')
1016
+ end
1017
+
1018
+ def visit_super_node(node)
1019
+ @calls_super = true
1020
+ super
1021
+ end
1022
+
1023
+ def visit_forwarding_super_node(node)
1024
+ @calls_super = true
1025
+ end
1026
+ end
1027
+ end
1028
+ end