1 # -*- coding: utf-8 -*-
5 # Copyright IBM, Corp. 2011
6 # Copyright (c) 2013-2019 Red Hat Inc.
9 # Anthony Liguori <aliguori@us.ibm.com>
10 # Markus Armbruster <armbru@redhat.com>
11 # Marc-André Lureau <marcandre.lureau@redhat.com>
12 # Kevin Wolf <kwolf@redhat.com>
14 # This work is licensed under the terms of the GNU GPL, version 2.
15 # See the COPYING file in the top-level directory.
19 from collections
import OrderedDict
21 from qapi
.error
import QAPIParseError
, QAPISemError
22 from qapi
.source
import QAPISourceInfo
25 class QAPISchemaParser
:
27 def __init__(self
, fname
, previously_included
=None, incl_info
=None):
28 previously_included
= previously_included
or set()
29 previously_included
.add(os
.path
.abspath(fname
))
32 fp
= open(fname
, 'r', encoding
='utf-8')
35 raise QAPISemError(incl_info
or QAPISourceInfo(None, None, None),
36 "can't read %s file '%s': %s"
37 % ("include" if incl_info
else "schema",
41 if self
.src
== '' or self
.src
[-1] != '\n':
44 self
.info
= QAPISourceInfo(fname
, 1, incl_info
)
51 while self
.tok
is not None:
54 self
.reject_expr_doc(cur_doc
)
55 cur_doc
= self
.get_doc(info
)
56 self
.docs
.append(cur_doc
)
59 expr
= self
.get_expr(False)
61 self
.reject_expr_doc(cur_doc
)
63 raise QAPISemError(info
, "invalid 'include' directive")
64 include
= expr
['include']
65 if not isinstance(include
, str):
66 raise QAPISemError(info
,
67 "value of 'include' must be a string")
68 incl_fname
= os
.path
.join(os
.path
.dirname(fname
),
70 self
.exprs
.append({'expr': {'include': incl_fname
},
72 exprs_include
= self
._include
(include
, info
, incl_fname
,
75 self
.exprs
.extend(exprs_include
.exprs
)
76 self
.docs
.extend(exprs_include
.docs
)
77 elif "pragma" in expr
:
78 self
.reject_expr_doc(cur_doc
)
80 raise QAPISemError(info
, "invalid 'pragma' directive")
81 pragma
= expr
['pragma']
82 if not isinstance(pragma
, dict):
84 info
, "value of 'pragma' must be an object")
85 for name
, value
in pragma
.items():
86 self
._pragma
(name
, value
, info
)
88 expr_elem
= {'expr': expr
,
91 if not cur_doc
.symbol
:
93 cur_doc
.info
, "definition documentation required")
94 expr_elem
['doc'] = cur_doc
95 self
.exprs
.append(expr_elem
)
97 self
.reject_expr_doc(cur_doc
)
100 def reject_expr_doc(doc
):
101 if doc
and doc
.symbol
:
104 "documentation for '%s' is not followed by the definition"
107 def _include(self
, include
, info
, incl_fname
, previously_included
):
108 incl_abs_fname
= os
.path
.abspath(incl_fname
)
109 # catch inclusion cycle
112 if incl_abs_fname
== os
.path
.abspath(inf
.fname
):
113 raise QAPISemError(info
, "inclusion loop for %s" % include
)
116 # skip multiple include of the same file
117 if incl_abs_fname
in previously_included
:
120 return QAPISchemaParser(incl_fname
, previously_included
, info
)
122 def _pragma(self
, name
, value
, info
):
123 if name
== 'doc-required':
124 if not isinstance(value
, bool):
125 raise QAPISemError(info
,
126 "pragma 'doc-required' must be boolean")
127 info
.pragma
.doc_required
= value
128 elif name
== 'returns-whitelist':
129 if (not isinstance(value
, list)
130 or any([not isinstance(elt
, str) for elt
in value
])):
133 "pragma returns-whitelist must be a list of strings")
134 info
.pragma
.returns_whitelist
= value
135 elif name
== 'name-case-whitelist':
136 if (not isinstance(value
, list)
137 or any([not isinstance(elt
, str) for elt
in value
])):
140 "pragma name-case-whitelist must be a list of strings")
141 info
.pragma
.name_case_whitelist
= value
143 raise QAPISemError(info
, "unknown pragma '%s'" % name
)
145 def accept(self
, skip_comment
=True):
147 self
.tok
= self
.src
[self
.cursor
]
148 self
.pos
= self
.cursor
153 if self
.src
[self
.cursor
] == '#':
154 # Start of doc comment
156 self
.cursor
= self
.src
.find('\n', self
.cursor
)
158 self
.val
= self
.src
[self
.pos
:self
.cursor
]
160 elif self
.tok
in '{}:,[]':
162 elif self
.tok
== "'":
163 # Note: we accept only printable ASCII
167 ch
= self
.src
[self
.cursor
]
170 raise QAPIParseError(self
, "missing terminating \"'\"")
172 # Note: we recognize only \\ because we have
173 # no use for funny characters in strings
175 raise QAPIParseError(self
,
176 "unknown escape \\%s" % ch
)
184 if ord(ch
) < 32 or ord(ch
) >= 127:
185 raise QAPIParseError(
186 self
, "funny character in string")
188 elif self
.src
.startswith('true', self
.pos
):
192 elif self
.src
.startswith('false', self
.pos
):
196 elif self
.tok
== '\n':
197 if self
.cursor
== len(self
.src
):
200 self
.info
= self
.info
.next_line()
201 self
.line_pos
= self
.cursor
202 elif not self
.tok
.isspace():
203 # Show up to next structural, whitespace or quote
205 match
= re
.match('[^[\\]{}:,\\s\'"]+',
206 self
.src
[self
.cursor
-1:])
207 raise QAPIParseError(self
, "stray '%s'" % match
.group(0))
209 def get_members(self
):
215 raise QAPIParseError(self
, "expected string or '}'")
220 raise QAPIParseError(self
, "expected ':'")
223 raise QAPIParseError(self
, "duplicate key '%s'" % key
)
224 expr
[key
] = self
.get_expr(True)
229 raise QAPIParseError(self
, "expected ',' or '}'")
232 raise QAPIParseError(self
, "expected string")
234 def get_values(self
):
239 if self
.tok
not in "{['tfn":
240 raise QAPIParseError(
241 self
, "expected '{', '[', ']', string, boolean or 'null'")
243 expr
.append(self
.get_expr(True))
248 raise QAPIParseError(self
, "expected ',' or ']'")
251 def get_expr(self
, nested
):
252 if self
.tok
!= '{' and not nested
:
253 raise QAPIParseError(self
, "expected '{'")
256 expr
= self
.get_members()
257 elif self
.tok
== '[':
259 expr
= self
.get_values()
260 elif self
.tok
in "'tfn":
264 raise QAPIParseError(
265 self
, "expected '{', '[', string, boolean or 'null'")
268 def get_doc(self
, info
):
270 raise QAPIParseError(
271 self
, "junk after '##' at start of documentation comment")
273 doc
= QAPIDoc(self
, info
)
275 while self
.tok
== '#':
276 if self
.val
.startswith('##'):
279 raise QAPIParseError(
281 "junk after '##' at end of documentation comment")
288 raise QAPIParseError(self
, "documentation comment must end with '##'")
293 A documentation comment block, either definition or free-form
295 Definition documentation blocks consist of
297 * a body section: one line naming the definition, followed by an
298 overview (any number of lines)
300 * argument sections: a description of each argument (for commands
301 and events) or member (for structs, unions and alternates)
303 * features sections: a description of each feature flag
305 * additional (non-argument) sections, possibly tagged
307 Free-form documentation blocks consist only of a body section.
311 def __init__(self
, name
=None):
312 # optional section name (argument/member or section name)
314 # the list of lines for this section
317 def append(self
, line
):
318 self
.text
+= line
.rstrip() + '\n'
320 class ArgSection(Section
):
321 def __init__(self
, name
):
322 super().__init
__(name
)
325 def connect(self
, member
):
328 def __init__(self
, parser
, info
):
329 # self._parser is used to report errors with QAPIParseError. The
330 # resulting error position depends on the state of the parser.
331 # It happens to be the beginning of the comment. More or less
332 # servicable, but action at a distance.
333 self
._parser
= parser
336 self
.body
= QAPIDoc
.Section()
337 # dict mapping parameter name to ArgSection
338 self
.args
= OrderedDict()
339 self
.features
= OrderedDict()
342 # the current section
343 self
._section
= self
.body
344 self
._append
_line
= self
._append
_body
_line
346 def has_section(self
, name
):
347 """Return True if we have a section with this name."""
348 for i
in self
.sections
:
353 def append(self
, line
):
355 Parse a comment line and add it to the documentation.
357 The way that the line is dealt with depends on which part of
358 the documentation we're parsing right now:
359 * The body section: ._append_line is ._append_body_line
360 * An argument section: ._append_line is ._append_args_line
361 * A features section: ._append_line is ._append_features_line
362 * An additional section: ._append_line is ._append_various_line
366 self
._append
_freeform
(line
)
370 raise QAPIParseError(self
._parser
, "missing space after #")
372 self
._append
_line
(line
)
374 def end_comment(self
):
378 def _is_section_tag(name
):
379 return name
in ('Returns:', 'Since:',
380 # those are often singular or plural
382 'Example:', 'Examples:',
385 def _append_body_line(self
, line
):
387 Process a line of documentation text in the body section.
389 If this a symbol line and it is the section's first line, this
390 is a definition documentation block for that symbol.
392 If it's a definition documentation block, another symbol line
393 begins the argument section for the argument named by it, and
394 a section tag begins an additional section. Start that
395 section and append the line to it.
397 Else, append the line to the current section.
399 name
= line
.split(' ', 1)[0]
400 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
401 # recognized, and get silently treated as ordinary text
402 if not self
.symbol
and not self
.body
.text
and line
.startswith('@'):
403 if not line
.endswith(':'):
404 raise QAPIParseError(self
._parser
, "line should end with ':'")
405 self
.symbol
= line
[1:-1]
406 # FIXME invalid names other than the empty string aren't flagged
408 raise QAPIParseError(self
._parser
, "invalid name")
410 # This is a definition documentation block
411 if name
.startswith('@') and name
.endswith(':'):
412 self
._append
_line
= self
._append
_args
_line
413 self
._append
_args
_line
(line
)
414 elif line
== 'Features:':
415 self
._append
_line
= self
._append
_features
_line
416 elif self
._is
_section
_tag
(name
):
417 self
._append
_line
= self
._append
_various
_line
418 self
._append
_various
_line
(line
)
420 self
._append
_freeform
(line
.strip())
422 # This is a free-form documentation block
423 self
._append
_freeform
(line
.strip())
425 def _append_args_line(self
, line
):
427 Process a line of documentation text in an argument section.
429 A symbol line begins the next argument section, a section tag
430 section or a non-indented line after a blank line begins an
431 additional section. Start that section and append the line to
434 Else, append the line to the current section.
437 name
= line
.split(' ', 1)[0]
439 if name
.startswith('@') and name
.endswith(':'):
440 line
= line
[len(name
)+1:]
441 self
._start
_args
_section
(name
[1:-1])
442 elif self
._is
_section
_tag
(name
):
443 self
._append
_line
= self
._append
_various
_line
444 self
._append
_various
_line
(line
)
446 elif (self
._section
.text
.endswith('\n\n')
447 and line
and not line
[0].isspace()):
448 if line
== 'Features:':
449 self
._append
_line
= self
._append
_features
_line
451 self
._start
_section
()
452 self
._append
_line
= self
._append
_various
_line
453 self
._append
_various
_line
(line
)
456 self
._append
_freeform
(line
.strip())
458 def _append_features_line(self
, line
):
459 name
= line
.split(' ', 1)[0]
461 if name
.startswith('@') and name
.endswith(':'):
462 line
= line
[len(name
)+1:]
463 self
._start
_features
_section
(name
[1:-1])
464 elif self
._is
_section
_tag
(name
):
465 self
._append
_line
= self
._append
_various
_line
466 self
._append
_various
_line
(line
)
468 elif (self
._section
.text
.endswith('\n\n')
469 and line
and not line
[0].isspace()):
470 self
._start
_section
()
471 self
._append
_line
= self
._append
_various
_line
472 self
._append
_various
_line
(line
)
475 self
._append
_freeform
(line
.strip())
477 def _append_various_line(self
, line
):
479 Process a line of documentation text in an additional section.
481 A symbol line is an error.
483 A section tag begins an additional section. Start that
484 section and append the line to it.
486 Else, append the line to the current section.
488 name
= line
.split(' ', 1)[0]
490 if name
.startswith('@') and name
.endswith(':'):
491 raise QAPIParseError(self
._parser
,
492 "'%s' can't follow '%s' section"
493 % (name
, self
.sections
[0].name
))
494 if self
._is
_section
_tag
(name
):
495 line
= line
[len(name
)+1:]
496 self
._start
_section
(name
[:-1])
498 if (not self
._section
.name
or
499 not self
._section
.name
.startswith('Example')):
502 self
._append
_freeform
(line
)
504 def _start_symbol_section(self
, symbols_dict
, name
):
505 # FIXME invalid names other than the empty string aren't flagged
507 raise QAPIParseError(self
._parser
, "invalid parameter name")
508 if name
in symbols_dict
:
509 raise QAPIParseError(self
._parser
,
510 "'%s' parameter name duplicated" % name
)
511 assert not self
.sections
513 self
._section
= QAPIDoc
.ArgSection(name
)
514 symbols_dict
[name
] = self
._section
516 def _start_args_section(self
, name
):
517 self
._start
_symbol
_section
(self
.args
, name
)
519 def _start_features_section(self
, name
):
520 self
._start
_symbol
_section
(self
.features
, name
)
522 def _start_section(self
, name
=None):
523 if name
in ('Returns', 'Since') and self
.has_section(name
):
524 raise QAPIParseError(self
._parser
,
525 "duplicated '%s' section" % name
)
527 self
._section
= QAPIDoc
.Section(name
)
528 self
.sections
.append(self
._section
)
530 def _end_section(self
):
532 text
= self
._section
.text
= self
._section
.text
.strip()
533 if self
._section
.name
and (not text
or text
.isspace()):
534 raise QAPIParseError(
536 "empty doc section '%s'" % self
._section
.name
)
539 def _append_freeform(self
, line
):
540 match
= re
.match(r
'(@\S+:)', line
)
542 raise QAPIParseError(self
._parser
,
543 "'%s' not allowed in free-form documentation"
545 self
._section
.append(line
)
547 def connect_member(self
, member
):
548 if member
.name
not in self
.args
:
549 # Undocumented TODO outlaw
550 self
.args
[member
.name
] = QAPIDoc
.ArgSection(member
.name
)
551 self
.args
[member
.name
].connect(member
)
553 def connect_feature(self
, feature
):
554 if feature
.name
not in self
.features
:
555 raise QAPISemError(feature
.info
,
556 "feature '%s' lacks documentation"
558 self
.features
[feature
.name
].connect(feature
)
560 def check_expr(self
, expr
):
561 if self
.has_section('Returns') and 'command' not in expr
:
562 raise QAPISemError(self
.info
,
563 "'Returns:' is only valid for commands")
567 def check_args_section(args
, info
, what
):
568 bogus
= [name
for name
, section
in args
.items()
569 if not section
.member
]
573 "documented member%s '%s' %s not exist"
574 % ("s" if len(bogus
) > 1 else "",
576 "do" if len(bogus
) > 1 else "does"))
578 check_args_section(self
.args
, self
.info
, 'members')
579 check_args_section(self
.features
, self
.info
, 'features')