rdoc 6.7.0 → 6.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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