/
docstring_parser.rb
345 lines (303 loc) · 11.3 KB
/
docstring_parser.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# frozen_string_literal: true
require 'ostruct'
module YARD
# Parses text and creates a {Docstring} object to represent documentation
# for a {CodeObjects::Base}. To create a new docstring, you should initialize
# the parser and call {#parse} followed by {#to_docstring}.
#
# == Subclassing Notes
#
# The DocstringParser can be subclassed and subtituted during parsing by
# setting the {Docstring.default_parser} attribute with the name of the
# subclass. This allows developers to change the way docstrings are
# parsed, allowing for completely different docstring syntaxes.
#
# @example Creating a Docstring with a DocstringParser
# DocstringParser.new.parse("text here").to_docstring
# @example Creating a Custom DocstringParser
# # Parses docstrings backwards!
# class ReverseDocstringParser
# def parse_content(content)
# super(content.reverse)
# end
# end
#
# # Set the parser as default when parsing
# YARD::Docstring.default_parser = ReverseDocstringParser
# @see #parse_content
# @since 0.8.0
class DocstringParser
# @return [String] the parsed text portion of the docstring,
# with tags removed.
attr_accessor :text
# @return [String] the complete input string to the parser.
attr_accessor :raw_text
# @return [Array<Tags::Tag>] the list of meta-data tags identified
# by the parser
attr_accessor :tags
# @return [Array<Tags::Directive>] a list of directives identified
# by the parser. This list will not be passed on to the
# Docstring object.
attr_accessor :directives
# @return [OpenStruct] any arbitrary state to be passed between
# tags during parsing. Mainly used by directives to coordinate
# behaviour (so that directives can be aware of other directives
# used in a docstring).
attr_accessor :state
# @return [CodeObjects::Base, nil] the object associated with
# the docstring being parsed. May be nil if the docstring is
# not attached to any object.
attr_accessor :object
# @return [CodeObjects::Base, nil] the object referenced by
# the docstring being parsed. May be nil if the docstring doesn't
# refer to any object.
attr_accessor :reference
# @return [Handlers::Base, nil] the handler parsing this
# docstring. May be nil if this docstring parser is not
# initialized through
attr_accessor :handler
# @return [Tags::Library] the tag library being used to
# identify registered tags in the docstring.
attr_accessor :library
# The regular expression to match the tag syntax
META_MATCH = /^@(!)?((?:\w\.?)+)(?:\s+(.*))?$/i
# @!group Creation and Conversion Methods
# Creates a new parser to parse docstring data
#
# @param [Tags::Library] library a tag library for recognizing
# tags.
def initialize(library = Tags::Library.instance)
@text = ""
@raw_text = ""
@tags = []
@directives = []
@library = library
@object = nil
@reference = nil
@handler = nil
@state = OpenStruct.new
end
# @return [Docstring] translates parsed text into
# a Docstring object.
def to_docstring
Docstring.new!(text, tags, object, raw_text, reference)
end
# @!group Parsing Methods
# Parses all content and returns itself.
#
# @param [String] content the docstring text to parse
# @param [CodeObjects::Base] object the object that the docstring
# is attached to. Will be passed to directives to act on
# this object.
# @param [Handlers::Base, nil] handler the handler object that is
# parsing this object. May be nil if this parser is not being
# called from a {Parser::SourceParser} context.
# @return [self] the parser object. To get the docstring,
# call {#to_docstring}.
# @see #to_docstring
def parse(content, object = nil, handler = nil)
@object = object
@handler = handler
@reference, @raw_text = detect_reference(content)
text = parse_content(@raw_text)
@text = text.strip
call_directives_after_parse
post_process
self
end
# Parses a given block of text.
#
# @param [String] content the content to parse
# @note Subclasses can override this method to perform custom
# parsing of content data.
def parse_content(content)
content = content.split(/\r?\n/) if content.is_a?(String)
return '' if !content || content.empty?
docstring = String.new("")
indent = content.first[/^\s*/].length
last_indent = 0
orig_indent = 0
directive = false
last_line = ""
tag_name = nil
tag_buf = []
(content + ['']).each_with_index do |line, index|
indent = line[/^\s*/].length
empty = (line =~ /^\s*$/ ? true : false)
done = content.size == index
if tag_name && (((indent < orig_indent && !empty) || done ||
(indent == 0 && !empty)) || (indent <= last_indent && line =~ META_MATCH))
buf = tag_buf.join("\n")
if directive || tag_is_directive?(tag_name)
directive = create_directive(tag_name, buf)
if directive
docstring << parse_content(directive.expanded_text).chomp
end
else
create_tag(tag_name, buf)
end
tag_name = nil
tag_buf = []
directive = false
orig_indent = 0
end
# Found a meta tag
if line =~ META_MATCH
directive = $1
tag_name = $2
tag_buf = [($3 || '')]
elsif tag_name && indent >= orig_indent && !empty
orig_indent = indent if orig_indent == 0
# Extra data added to the tag on the next line
last_empty = last_line =~ /^[ \t]*$/ ? true : false
tag_buf << '' if last_empty
tag_buf << line.gsub(/^[ \t]{#{orig_indent}}/, '')
elsif !tag_name
# Regular docstring text
docstring << line
docstring << "\n"
end
last_indent = indent
last_line = line
end
docstring
end
# @!group Parser Callback Methods
# Call post processing callbacks on parser.
# This is called implicitly by parser. Use this when
# manually configuring a {Docstring} object.
#
# @return [void]
def post_process
call_after_parse_callbacks
end
# @!group Tag Manipulation Methods
# Creates a tag from the {Tags::DefaultFactory tag factory}.
#
# To add an already created tag object, append it to {#tags}.
#
# @param [String] tag_name the tag name
# @param [String] tag_buf the text attached to the tag with newlines removed.
# @return [Tags::Tag, Tags::RefTag] a tag
def create_tag(tag_name, tag_buf = '')
if tag_buf =~ /\A\s*(?:(\S+)\s+)?\(\s*see\s+(\S+)\s*\)\s*\Z/
return create_ref_tag(tag_name, $1, $2)
end
if library.has_tag?(tag_name)
@tags += [library.tag_create(tag_name, tag_buf)].flatten
else
log.warn "Unknown tag @#{tag_name}" +
(object ? " in file `#{object.file}` near line #{object.line}" : "")
end
rescue Tags::TagFormatError
log.warn "Invalid tag format for @#{tag_name}" +
(object ? " in file `#{object.file}` near line #{object.line}" : "")
end
# Creates a {Tags::RefTag}
def create_ref_tag(tag_name, name, object_name)
@tags << Tags::RefTagList.new(tag_name, P(object, object_name), name)
end
# Creates a new directive using the registered {#library}
# @return [Tags::Directive] the directive object that is created
def create_directive(tag_name, tag_buf)
if library.has_directive?(tag_name)
dir = library.directive_create(tag_name, tag_buf, self)
if dir.is_a?(Tags::Directive)
@directives << dir
dir
end
else
log.warn "Unknown directive @!#{tag_name}" +
(object ? " in file `#{object.file}` near line #{object.line}" : "")
nil
end
rescue Tags::TagFormatError
log.warn "Invalid directive format for @!#{tag_name}" +
(object ? " in file `#{object.file}` near line #{object.line}" : "")
nil
end
# Backward compatibility to detect old tags that should be specified
# as directives in 0.8 and onward.
def tag_is_directive?(tag_name)
list = %w(attribute endgroup group macro method scope visibility)
list.include?(tag_name)
end
# Creates a callback that is called after a docstring is successfully
# parsed. Use this method to perform sanity checks on a docstring's
# tag data, or add any extra tags automatically to a docstring.
#
# @yield [parser] a block to be called after a docstring is parsed
# @yieldparam [DocstringParser] parser the docstring parser object
# with all directives and tags created.
# @yieldreturn [void]
# @return [void]
def self.after_parse(&block)
after_parse_callbacks << block
end
# @return [Array<Proc>] the {after_parse} callback proc objects
def self.after_parse_callbacks
@after_parse_callbacks ||= []
end
# Define a callback to check that @param tags are properly named
after_parse do |parser|
next unless parser.object
next unless parser.object.is_a?(CodeObjects::MethodObject)
next if parser.object.is_alias?
names = parser.object.parameters.map {|l| l.first.gsub(/\W/, '') }
seen_names = []
infile_info = "\n in file `#{parser.object.file}' " \
"near line #{parser.object.line}"
parser.tags.each do |tag|
next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
next unless tag.tag_name == "param"
if seen_names.include?(tag.name)
log.warn "@param tag has duplicate parameter name: " \
"#{tag.name} #{infile_info}"
elsif names.include?(tag.name)
seen_names << tag.name
else
log.warn "@param tag has unknown parameter name: " \
"#{tag.name} #{infile_info}"
end
end
end
private
def namespace
object && object.namespace
end
def detect_reference(content)
if content =~ /\A\s*\(see (\S+)\s*\)(?:\s|$)/
path = $1
extra = $'
[CodeObjects::Proxy.new(namespace, path), extra]
else
[nil, content]
end
end
# @!group Parser Callback Methods
# Calls the {Tags::Directive#after_parse} callback on all the
# created directives.
def call_directives_after_parse
directives.each(&:after_parse)
end
# Calls all {after_parse} callbacks
def call_after_parse_callbacks
self.class.after_parse_callbacks.each do |cb|
cb.call(self)
end
end
# Define a callback to check that @see tags do not use {}.
after_parse do |parser|
next unless parser.object
parser.tags.each_with_index do |tag, i|
next if tag.is_a?(Tags::RefTagList) # we don't handle this yet
next unless tag.tag_name == "see"
next unless "#{tag.name}#{tag.text}" =~ /\A\{.*\}\Z/
infile_info = "\n in file `#{parser.object.file}' " \
"near line #{parser.object.line}"
log.warn "@see tag (##{i + 1}) should not be wrapped in {} " \
"(causes rendering issues): #{infile_info}"
end
end
end
end