Skip to content

Commit 83deaf4

Browse files
committed
Change comment directive parsing
1 parent eb85efb commit 83deaf4

12 files changed

+624
-210
lines changed

lib/rdoc/comment.rb

+169-8
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ def normalize
165165
self
166166
end
167167

168+
# Change normalized, when creating already normalized comment.
169+
170+
def normalized=(value)
171+
@normalized = value
172+
end
173+
168174
##
169175
# Was this text normalized?
170176

@@ -226,14 +232,169 @@ def tomdoc?
226232
@format == 'tomdoc'
227233
end
228234

229-
##
230-
# Create a new parsed comment from a document
235+
MULTILINE_DIRECTIVES = %w[call-seq].freeze # :nodoc:
231236

232-
def self.from_document(document) # :nodoc:
233-
comment = RDoc::Comment.new('')
234-
comment.document = document
235-
comment.location = RDoc::TopLevel.new(document.file) if document.file
236-
comment
237-
end
237+
# There are more, but already handled by RDoc::Parser::C
238+
COLON_LESS_DIRECTIVES = %w[call-seq Document-method].freeze # :nodoc:
239+
240+
private_constant :MULTILINE_DIRECTIVES, :COLON_LESS_DIRECTIVES
241+
242+
class << self
238243

244+
##
245+
# Create a new parsed comment from a document
246+
247+
def from_document(document) # :nodoc:
248+
comment = RDoc::Comment.new('')
249+
comment.document = document
250+
comment.location = RDoc::TopLevel.new(document.file) if document.file
251+
comment
252+
end
253+
254+
# Parse comment, collect directives as an attribute and return [normalized_comment_text, directives_hash]
255+
# This method expands include and removes everything not needed in the document text, such as
256+
# private section, directive line, comment characters `# /* * */` and indent spaces.
257+
#
258+
# RDoc comment consists of include, directive, multiline directive, private section and comment text.
259+
#
260+
# Include
261+
# # :include: filename
262+
#
263+
# Directive
264+
# # :directive-without-value:
265+
# # :directive-with-value: value
266+
#
267+
# Multiline directive (only :call-seq:)
268+
# # :multiline-directive:
269+
# # value1
270+
# # value2
271+
#
272+
# Private section
273+
# #--
274+
# # private comment
275+
# #++
276+
277+
def parse(text, filename, line_no, type)
278+
case type
279+
when :ruby
280+
text = text.gsub(/^#+/, '') if text.start_with?('#')
281+
private_start_regexp = /^-{2,}$/
282+
private_end_regexp = /^\+{2}$/
283+
indent_regexp = /^\s*/
284+
when :c
285+
private_start_regexp = /^(\s*\*)?-{2,}$/
286+
private_end_regexp = /^(\s*\*)?\+{2}$/
287+
indent_regexp = /^\s*(\/\*+|\*)?\s*/
288+
text = text.gsub(/\s*\*+\/\s*\z/, '')
289+
# TODO: should not be here. Looks like another type of directive
290+
# text = text.gsub %r%Document-method:\s+[\w:.#=!?|^&<>~+\-/*\%@`\[\]]+%, ''
291+
when :simple
292+
# Unlike other types, this implementation only looks for two dashes at
293+
# the beginning of the line. Three or more dashes are considered to be
294+
# a rule and ignored.
295+
private_start_regexp = /^-{2}$/
296+
private_end_regexp = /^\+{2}$/
297+
indent_regexp = /^\s*/
298+
end
299+
300+
directives = {}
301+
lines = text.split("\n")
302+
in_private = false
303+
comment_lines = []
304+
until lines.empty?
305+
line = lines.shift
306+
read_lines = 1
307+
if in_private
308+
in_private = false if line.match?(private_end_regexp)
309+
line_no += read_lines
310+
next
311+
elsif line.match?(private_start_regexp)
312+
in_private = true
313+
line_no += read_lines
314+
next
315+
end
316+
317+
prefix = line[indent_regexp]
318+
prefix_indent = ' ' * prefix.size
319+
line = line.byteslice(prefix.bytesize..)
320+
/\A(?<colon>\\?:|:?)(?<directive>[\w-]+):(?<param>.*)/ =~ line
321+
322+
if colon == '\\:'
323+
# unescape if escaped
324+
comment_lines << prefix_indent + line.sub('\\:', ':')
325+
elsif !directive || param.start_with?(':') || (colon.empty? && !COLON_LESS_DIRECTIVES.include?(directive))
326+
# Something like `:toto::` is not a directive
327+
# Only few directives allows to start without a colon
328+
comment_lines << prefix_indent + line
329+
elsif directive == 'include'
330+
filename_to_include = param.strip
331+
yield(filename_to_include, prefix_indent).lines.each { |l| comment_lines << l.chomp }
332+
elsif MULTILINE_DIRECTIVES.include?(directive)
333+
param = param.strip
334+
value_lines = take_multiline_directive_value_lines(directive, filename, line_no, lines, prefix_indent.size, indent_regexp, !param.empty?)
335+
read_lines += value_lines.size
336+
lines.shift(value_lines.size)
337+
unless param.empty?
338+
# Accept `:call-seq: first-line\n second-line` for now
339+
value_lines.unshift(param)
340+
end
341+
value = value_lines.join("\n")
342+
directives[directive] = [value.empty? ? nil : value, line_no]
343+
else
344+
value = param.strip
345+
directives[directive] = [value.empty? ? nil : value, line_no]
346+
end
347+
line_no += read_lines
348+
end
349+
# normalize comment
350+
min_spaces = nil
351+
comment_lines.each do |l|
352+
next if l.match?(/\A\s*\z/)
353+
n = l[/\A */].size
354+
min_spaces = n if !min_spaces || n < min_spaces
355+
end
356+
comment_lines.map! { |l| l[min_spaces..] || '' } if min_spaces
357+
comment_lines.shift while comment_lines.first&.match?(/\A\s*\z/)
358+
[String.new(encoding: text.encoding) << comment_lines.join("\n"), directives]
359+
end
360+
361+
# Take value lines of multiline directive
362+
363+
private def take_multiline_directive_value_lines(directive, filename, line_no, lines, base_indent_size, indent_regexp, has_param)
364+
return [] if lines.empty?
365+
366+
first_indent_size = lines.first[indent_regexp].size
367+
368+
# Blank line or unindented line is not part of multiline-directive value
369+
return [] if first_indent_size <= base_indent_size
370+
371+
if has_param
372+
# :multiline-directive: line1
373+
# line2
374+
# line3
375+
#
376+
value_lines = lines.take_while do |l|
377+
l.rstrip[indent_regexp].size > base_indent_size
378+
end
379+
min_indent = value_lines.map { |l| l[indent_regexp].size }.min
380+
value_lines.map { |l| l[min_indent..] }
381+
else
382+
# Take indented lines accepting blank lines between them
383+
value_lines = lines.take_while do |l|
384+
l = l.rstrip
385+
indent = l[indent_regexp]
386+
if indent == l || indent.size >= first_indent_size
387+
true
388+
end
389+
end
390+
value_lines.map! { |l| (l[first_indent_size..] || '').chomp }
391+
392+
if value_lines.size != lines.size && !value_lines.last.empty?
393+
warn "#{filename}:#{line_no} Multiline directive :#{directive}: should end with a blank line."
394+
end
395+
value_lines.pop while value_lines.last&.empty?
396+
value_lines
397+
end
398+
end
399+
end
239400
end

lib/rdoc/markup/pre_process.rb

+35-10
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,15 @@ def initialize(input_file_name, include_path)
9797
# RDoc::CodeObject#metadata for details.
9898

9999
def handle text, code_object = nil, &block
100-
first_line = 1
101100
if RDoc::Comment === text then
102101
comment = text
103102
text = text.text
104-
first_line = comment.line || 1
105103
end
106104

107105
# regexp helper (square brackets for optional)
108106
# $1 $2 $3 $4 $5
109107
# [prefix][\]:directive:[spaces][param]newline
110-
text = text.lines.map.with_index(first_line) do |line, num|
111-
next line unless line =~ /\A([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/
108+
text = text.gsub(/^([ \t]*(?:#|\/?\*)?[ \t]*)(\\?):([\w-]+):([ \t]*)(.+)?(\r?\n|$)/) do
112109
# skip something like ':toto::'
113110
next $& if $4.empty? and $5 and $5[0, 1] == ':'
114111

@@ -122,21 +119,49 @@ def handle text, code_object = nil, &block
122119
comment.format = $5.downcase
123120
next "#{$1.strip}\n"
124121
end
125-
126-
handle_directive $1, $3, $5, code_object, text.encoding, num, &block
127-
end.join
122+
handle_directive $1, $3, $5, code_object, text.encoding, &block
123+
end
128124

129125
if comment then
130126
comment.text = text
131127
else
132128
comment = text
133129
end
134130

131+
run_post_processes(comment, code_object)
132+
133+
text
134+
end
135+
136+
# Apply directives to a code object
137+
138+
def run_pre_processes(comment_text, code_object, start_line_no, type)
139+
comment_text, directives = parse_comment(comment_text, start_line_no, type)
140+
directives.each do |directive, (param, line_no)|
141+
handle_directive('', directive, param, code_object)
142+
end
143+
if code_object.is_a?(RDoc::AnyMethod) && (call_seq, = directives['call-seq']) && call_seq
144+
code_object.call_seq = call_seq.lines.map(&:chomp).reject(&:empty?).join("\n") if call_seq
145+
end
146+
format, = directives['markup']
147+
[comment_text, format]
148+
end
149+
150+
151+
# Perform post preocesses to a code object
152+
153+
def run_post_processes(comment, code_object)
135154
self.class.post_processors.each do |handler|
136155
handler.call comment, code_object
137156
end
157+
end
138158

139-
text
159+
# Parse comment and return [normalized_comment_text, directives_hash]
160+
161+
def parse_comment(text, line_no, type)
162+
RDoc::Comment.parse(text, @input_file_name, line_no, type) do |filename, prefix_indent|
163+
include_file(filename, prefix_indent, text.encoding)
164+
end
140165
end
141166

142167
##
@@ -151,7 +176,7 @@ def handle text, code_object = nil, &block
151176
# When 1.8.7 support is ditched prefix can be defaulted to ''
152177

153178
def handle_directive prefix, directive, param, code_object = nil,
154-
encoding = nil, line = nil
179+
encoding = nil
155180
blankline = "#{prefix.strip}\n"
156181
directive = directive.downcase
157182

@@ -244,7 +269,7 @@ def handle_directive prefix, directive, param, code_object = nil,
244269

245270
blankline
246271
else
247-
result = yield directive, param, line if block_given?
272+
result = yield directive, param if block_given?
248273

249274
case result
250275
when nil then

lib/rdoc/parser/c.rb

+6-40
Original file line numberDiff line numberDiff line change
@@ -609,8 +609,6 @@ def find_body class_name, meth_name, meth_obj, file_content, quiet = false
609609
body = args[1]
610610
offset, = args[2]
611611

612-
comment.remove_private if comment
613-
614612
# try to find the whole body
615613
body = $& if /#{Regexp.escape body}[^(]*?\{.*?^\}/m =~ file_content
616614

@@ -623,7 +621,6 @@ def find_body class_name, meth_name, meth_obj, file_content, quiet = false
623621
override_comment = find_override_comment class_name, meth_obj
624622
comment = override_comment if override_comment
625623

626-
comment.normalize
627624
find_modifiers comment, meth_obj if comment
628625

629626
#meth_obj.params = params
@@ -641,7 +638,6 @@ def find_body class_name, meth_name, meth_obj, file_content, quiet = false
641638

642639
find_body class_name, args[3], meth_obj, file_content, true
643640

644-
comment.normalize
645641
find_modifiers comment, meth_obj
646642

647643
meth_obj.start_collecting_tokens
@@ -665,7 +661,6 @@ def find_body class_name, meth_name, meth_obj, file_content, quiet = false
665661
comment = find_override_comment class_name, meth_obj
666662

667663
if comment then
668-
comment.normalize
669664
find_modifiers comment, meth_obj
670665
meth_obj.comment = comment
671666

@@ -744,7 +739,6 @@ def find_class_comment class_name, class_mod
744739
end
745740

746741
comment = new_comment comment, @top_level, :c
747-
comment.normalize
748742

749743
look_for_directives_in class_mod, comment
750744

@@ -809,9 +803,6 @@ def find_const_comment(type, const_name, class_name = nil)
809803
# Handles modifiers in +comment+ and updates +meth_obj+ as appropriate.
810804

811805
def find_modifiers comment, meth_obj
812-
comment.normalize
813-
comment.extract_call_seq meth_obj
814-
815806
look_for_directives_in meth_obj, comment
816807
end
817808

@@ -825,10 +816,10 @@ def find_override_comment class_name, meth_obj
825816
comment = if @content =~ %r%Document-method:
826817
\s+#{class_name}#{prefix}#{name}
827818
\s*?\n((?>.*?\*/))%xm then
828-
"/*#{$1}"
819+
"/*\n#{$1}"
829820
elsif @content =~ %r%Document-method:
830821
\s#{name}\s*?\n((?>.*?\*/))%xm then
831-
"/*#{$1}"
822+
"/*\n#{$1}"
832823
end
833824

834825
return unless comment
@@ -1105,35 +1096,10 @@ def load_variable_map map_name
11051096
# Both :main: and :title: directives are deprecated and will be removed in RDoc 7.
11061097

11071098
def look_for_directives_in context, comment
1108-
@preprocess.handle comment, context do |directive, param|
1109-
case directive
1110-
when 'main' then
1111-
@options.main_page = param
1112-
1113-
warn <<~MSG
1114-
The :main: directive is deprecated and will be removed in RDoc 7.
1115-
1116-
You can use these options to specify the initial page displayed instead:
1117-
- `--main=#{param}` via the command line
1118-
- `rdoc.main = "#{param}"` if you use `RDoc::Task`
1119-
- `main_page: #{param}` in your `.rdoc_options` file
1120-
MSG
1121-
''
1122-
when 'title' then
1123-
@options.default_title = param if @options.respond_to? :default_title=
1124-
1125-
warn <<~MSG
1126-
The :title: directive is deprecated and will be removed in RDoc 7.
1127-
1128-
You can use these options to specify the title displayed instead:
1129-
- `--title=#{param}` via the command line
1130-
- `rdoc.title = "#{param}"` if you use `RDoc::Task`
1131-
- `title: #{param}` in your `.rdoc_options` file
1132-
MSG
1133-
''
1134-
end
1135-
end
1136-
1099+
comment.text, format = @preprocess.run_pre_processes(comment.text, context, comment.line || 1, :c)
1100+
comment.format = format if format
1101+
@preprocess.run_post_processes(comment, context)
1102+
comment.normalized = true
11371103
comment
11381104
end
11391105

0 commit comments

Comments
 (0)