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.
17 from collections
import OrderedDict
21 from .error
import QAPISemError
, QAPISourceError
22 from .source
import QAPISourceInfo
25 class QAPIParseError(QAPISourceError
):
26 """Error class for all QAPI schema parsing errors."""
27 def __init__(self
, parser
, msg
):
29 for ch
in parser
.src
[parser
.line_pos
:parser
.pos
]:
31 col
= (col
+ 7) % 8 + 1
34 super().__init
__(parser
.info
, msg
, col
)
37 class QAPISchemaParser
:
39 def __init__(self
, fname
, previously_included
=None, incl_info
=None):
40 previously_included
= previously_included
or set()
41 previously_included
.add(os
.path
.abspath(fname
))
44 fp
= open(fname
, 'r', encoding
='utf-8')
47 raise QAPISemError(incl_info
or QAPISourceInfo(None, None, None),
48 "can't read %s file '%s': %s"
49 % ("include" if incl_info
else "schema",
53 if self
.src
== '' or self
.src
[-1] != '\n':
56 self
.info
= QAPISourceInfo(fname
, 1, incl_info
)
63 while self
.tok
is not None:
66 self
.reject_expr_doc(cur_doc
)
67 for cur_doc
in self
.get_doc(info
):
68 self
.docs
.append(cur_doc
)
71 expr
= self
.get_expr(False)
73 self
.reject_expr_doc(cur_doc
)
75 raise QAPISemError(info
, "invalid 'include' directive")
76 include
= expr
['include']
77 if not isinstance(include
, str):
78 raise QAPISemError(info
,
79 "value of 'include' must be a string")
80 incl_fname
= os
.path
.join(os
.path
.dirname(fname
),
82 self
.exprs
.append({'expr': {'include': incl_fname
},
84 exprs_include
= self
._include
(include
, info
, incl_fname
,
87 self
.exprs
.extend(exprs_include
.exprs
)
88 self
.docs
.extend(exprs_include
.docs
)
89 elif "pragma" in expr
:
90 self
.reject_expr_doc(cur_doc
)
92 raise QAPISemError(info
, "invalid 'pragma' directive")
93 pragma
= expr
['pragma']
94 if not isinstance(pragma
, dict):
96 info
, "value of 'pragma' must be an object")
97 for name
, value
in pragma
.items():
98 self
._pragma
(name
, value
, info
)
100 expr_elem
= {'expr': expr
,
103 if not cur_doc
.symbol
:
105 cur_doc
.info
, "definition documentation required")
106 expr_elem
['doc'] = cur_doc
107 self
.exprs
.append(expr_elem
)
109 self
.reject_expr_doc(cur_doc
)
112 def reject_expr_doc(doc
):
113 if doc
and doc
.symbol
:
116 "documentation for '%s' is not followed by the definition"
119 def _include(self
, include
, info
, incl_fname
, previously_included
):
120 incl_abs_fname
= os
.path
.abspath(incl_fname
)
121 # catch inclusion cycle
124 if incl_abs_fname
== os
.path
.abspath(inf
.fname
):
125 raise QAPISemError(info
, "inclusion loop for %s" % include
)
128 # skip multiple include of the same file
129 if incl_abs_fname
in previously_included
:
132 return QAPISchemaParser(incl_fname
, previously_included
, info
)
134 def _check_pragma_list_of_str(self
, name
, value
, info
):
135 if (not isinstance(value
, list)
136 or any([not isinstance(elt
, str) for elt
in value
])):
139 "pragma %s must be a list of strings" % name
)
141 def _pragma(self
, name
, value
, info
):
142 if name
== 'doc-required':
143 if not isinstance(value
, bool):
144 raise QAPISemError(info
,
145 "pragma 'doc-required' must be boolean")
146 info
.pragma
.doc_required
= value
147 elif name
== 'command-name-exceptions':
148 self
._check
_pragma
_list
_of
_str
(name
, value
, info
)
149 info
.pragma
.command_name_exceptions
= value
150 elif name
== 'command-returns-exceptions':
151 self
._check
_pragma
_list
_of
_str
(name
, value
, info
)
152 info
.pragma
.command_returns_exceptions
= value
153 elif name
== 'member-name-exceptions':
154 self
._check
_pragma
_list
_of
_str
(name
, value
, info
)
155 info
.pragma
.member_name_exceptions
= value
157 raise QAPISemError(info
, "unknown pragma '%s'" % name
)
159 def accept(self
, skip_comment
=True):
161 self
.tok
= self
.src
[self
.cursor
]
162 self
.pos
= self
.cursor
167 if self
.src
[self
.cursor
] == '#':
168 # Start of doc comment
170 self
.cursor
= self
.src
.find('\n', self
.cursor
)
172 self
.val
= self
.src
[self
.pos
:self
.cursor
]
174 elif self
.tok
in '{}:,[]':
176 elif self
.tok
== "'":
177 # Note: we accept only printable ASCII
181 ch
= self
.src
[self
.cursor
]
184 raise QAPIParseError(self
, "missing terminating \"'\"")
186 # Note: we recognize only \\ because we have
187 # no use for funny characters in strings
189 raise QAPIParseError(self
,
190 "unknown escape \\%s" % ch
)
198 if ord(ch
) < 32 or ord(ch
) >= 127:
199 raise QAPIParseError(
200 self
, "funny character in string")
202 elif self
.src
.startswith('true', self
.pos
):
206 elif self
.src
.startswith('false', self
.pos
):
210 elif self
.tok
== '\n':
211 if self
.cursor
== len(self
.src
):
214 self
.info
= self
.info
.next_line()
215 self
.line_pos
= self
.cursor
216 elif not self
.tok
.isspace():
217 # Show up to next structural, whitespace or quote
219 match
= re
.match('[^[\\]{}:,\\s\'"]+',
220 self
.src
[self
.cursor
-1:])
221 raise QAPIParseError(self
, "stray '%s'" % match
.group(0))
223 def get_members(self
):
229 raise QAPIParseError(self
, "expected string or '}'")
234 raise QAPIParseError(self
, "expected ':'")
237 raise QAPIParseError(self
, "duplicate key '%s'" % key
)
238 expr
[key
] = self
.get_expr(True)
243 raise QAPIParseError(self
, "expected ',' or '}'")
246 raise QAPIParseError(self
, "expected string")
248 def get_values(self
):
253 if self
.tok
not in "{['tf":
254 raise QAPIParseError(
255 self
, "expected '{', '[', ']', string, or boolean")
257 expr
.append(self
.get_expr(True))
262 raise QAPIParseError(self
, "expected ',' or ']'")
265 def get_expr(self
, nested
):
266 if self
.tok
!= '{' and not nested
:
267 raise QAPIParseError(self
, "expected '{'")
270 expr
= self
.get_members()
271 elif self
.tok
== '[':
273 expr
= self
.get_values()
274 elif self
.tok
in "'tf":
278 raise QAPIParseError(
279 self
, "expected '{', '[', string, or boolean")
282 def get_doc(self
, info
):
284 raise QAPIParseError(
285 self
, "junk after '##' at start of documentation comment")
288 cur_doc
= QAPIDoc(self
, info
)
290 while self
.tok
== '#':
291 if self
.val
.startswith('##'):
294 raise QAPIParseError(
296 "junk after '##' at end of documentation comment")
297 cur_doc
.end_comment()
301 if self
.val
.startswith('# ='):
303 raise QAPIParseError(
305 "unexpected '=' markup in definition documentation")
306 if cur_doc
.body
.text
:
307 cur_doc
.end_comment()
309 cur_doc
= QAPIDoc(self
, info
)
310 cur_doc
.append(self
.val
)
313 raise QAPIParseError(self
, "documentation comment must end with '##'")
318 A documentation comment block, either definition or free-form
320 Definition documentation blocks consist of
322 * a body section: one line naming the definition, followed by an
323 overview (any number of lines)
325 * argument sections: a description of each argument (for commands
326 and events) or member (for structs, unions and alternates)
328 * features sections: a description of each feature flag
330 * additional (non-argument) sections, possibly tagged
332 Free-form documentation blocks consist only of a body section.
336 def __init__(self
, parser
, name
=None, indent
=0):
337 # parser, for error messages about indentation
338 self
._parser
= parser
339 # optional section name (argument/member or section name)
342 # the expected indent level of the text of this section
343 self
._indent
= indent
345 def append(self
, line
):
346 # Strip leading spaces corresponding to the expected indent level
347 # Blank lines are always OK.
349 indent
= re
.match(r
'\s*', line
).end()
350 if indent
< self
._indent
:
351 raise QAPIParseError(
353 "unexpected de-indent (expected at least %d spaces)" %
355 line
= line
[self
._indent
:]
357 self
.text
+= line
.rstrip() + '\n'
359 class ArgSection(Section
):
360 def __init__(self
, parser
, name
, indent
=0):
361 super().__init
__(parser
, name
, indent
)
364 def connect(self
, member
):
367 def __init__(self
, parser
, info
):
368 # self._parser is used to report errors with QAPIParseError. The
369 # resulting error position depends on the state of the parser.
370 # It happens to be the beginning of the comment. More or less
371 # servicable, but action at a distance.
372 self
._parser
= parser
375 self
.body
= QAPIDoc
.Section(parser
)
376 # dict mapping parameter name to ArgSection
377 self
.args
= OrderedDict()
378 self
.features
= OrderedDict()
381 # the current section
382 self
._section
= self
.body
383 self
._append
_line
= self
._append
_body
_line
385 def has_section(self
, name
):
386 """Return True if we have a section with this name."""
387 for i
in self
.sections
:
392 def append(self
, line
):
394 Parse a comment line and add it to the documentation.
396 The way that the line is dealt with depends on which part of
397 the documentation we're parsing right now:
398 * The body section: ._append_line is ._append_body_line
399 * An argument section: ._append_line is ._append_args_line
400 * A features section: ._append_line is ._append_features_line
401 * An additional section: ._append_line is ._append_various_line
405 self
._append
_freeform
(line
)
409 raise QAPIParseError(self
._parser
, "missing space after #")
411 self
._append
_line
(line
)
413 def end_comment(self
):
417 def _is_section_tag(name
):
418 return name
in ('Returns:', 'Since:',
419 # those are often singular or plural
421 'Example:', 'Examples:',
424 def _append_body_line(self
, line
):
426 Process a line of documentation text in the body section.
428 If this a symbol line and it is the section's first line, this
429 is a definition documentation block for that symbol.
431 If it's a definition documentation block, another symbol line
432 begins the argument section for the argument named by it, and
433 a section tag begins an additional section. Start that
434 section and append the line to it.
436 Else, append the line to the current section.
438 name
= line
.split(' ', 1)[0]
439 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
440 # recognized, and get silently treated as ordinary text
441 if not self
.symbol
and not self
.body
.text
and line
.startswith('@'):
442 if not line
.endswith(':'):
443 raise QAPIParseError(self
._parser
, "line should end with ':'")
444 self
.symbol
= line
[1:-1]
445 # FIXME invalid names other than the empty string aren't flagged
447 raise QAPIParseError(self
._parser
, "invalid name")
449 # This is a definition documentation block
450 if name
.startswith('@') and name
.endswith(':'):
451 self
._append
_line
= self
._append
_args
_line
452 self
._append
_args
_line
(line
)
453 elif line
== 'Features:':
454 self
._append
_line
= self
._append
_features
_line
455 elif self
._is
_section
_tag
(name
):
456 self
._append
_line
= self
._append
_various
_line
457 self
._append
_various
_line
(line
)
459 self
._append
_freeform
(line
)
461 # This is a free-form documentation block
462 self
._append
_freeform
(line
)
464 def _append_args_line(self
, line
):
466 Process a line of documentation text in an argument section.
468 A symbol line begins the next argument section, a section tag
469 section or a non-indented line after a blank line begins an
470 additional section. Start that section and append the line to
473 Else, append the line to the current section.
476 name
= line
.split(' ', 1)[0]
478 if name
.startswith('@') and name
.endswith(':'):
479 # If line is "@arg: first line of description", find
480 # the index of 'f', which is the indent we expect for any
481 # following lines. We then remove the leading "@arg:"
482 # from line and replace it with spaces so that 'f' has the
483 # same index as it did in the original line and can be
484 # handled the same way we will handle following lines.
485 indent
= re
.match(r
'@\S*:\s*', line
).end()
488 # Line was just the "@arg:" header; following lines
492 line
= ' ' * indent
+ line
493 self
._start
_args
_section
(name
[1:-1], indent
)
494 elif self
._is
_section
_tag
(name
):
495 self
._append
_line
= self
._append
_various
_line
496 self
._append
_various
_line
(line
)
498 elif (self
._section
.text
.endswith('\n\n')
499 and line
and not line
[0].isspace()):
500 if line
== 'Features:':
501 self
._append
_line
= self
._append
_features
_line
503 self
._start
_section
()
504 self
._append
_line
= self
._append
_various
_line
505 self
._append
_various
_line
(line
)
508 self
._append
_freeform
(line
)
510 def _append_features_line(self
, line
):
511 name
= line
.split(' ', 1)[0]
513 if name
.startswith('@') and name
.endswith(':'):
514 # If line is "@arg: first line of description", find
515 # the index of 'f', which is the indent we expect for any
516 # following lines. We then remove the leading "@arg:"
517 # from line and replace it with spaces so that 'f' has the
518 # same index as it did in the original line and can be
519 # handled the same way we will handle following lines.
520 indent
= re
.match(r
'@\S*:\s*', line
).end()
523 # Line was just the "@arg:" header; following lines
527 line
= ' ' * indent
+ line
528 self
._start
_features
_section
(name
[1:-1], indent
)
529 elif self
._is
_section
_tag
(name
):
530 self
._append
_line
= self
._append
_various
_line
531 self
._append
_various
_line
(line
)
533 elif (self
._section
.text
.endswith('\n\n')
534 and line
and not line
[0].isspace()):
535 self
._start
_section
()
536 self
._append
_line
= self
._append
_various
_line
537 self
._append
_various
_line
(line
)
540 self
._append
_freeform
(line
)
542 def _append_various_line(self
, line
):
544 Process a line of documentation text in an additional section.
546 A symbol line is an error.
548 A section tag begins an additional section. Start that
549 section and append the line to it.
551 Else, append the line to the current section.
553 name
= line
.split(' ', 1)[0]
555 if name
.startswith('@') and name
.endswith(':'):
556 raise QAPIParseError(self
._parser
,
557 "'%s' can't follow '%s' section"
558 % (name
, self
.sections
[0].name
))
559 if self
._is
_section
_tag
(name
):
560 # If line is "Section: first line of description", find
561 # the index of 'f', which is the indent we expect for any
562 # following lines. We then remove the leading "Section:"
563 # from line and replace it with spaces so that 'f' has the
564 # same index as it did in the original line and can be
565 # handled the same way we will handle following lines.
566 indent
= re
.match(r
'\S*:\s*', line
).end()
569 # Line was just the "Section:" header; following lines
573 line
= ' ' * indent
+ line
574 self
._start
_section
(name
[:-1], indent
)
576 self
._append
_freeform
(line
)
578 def _start_symbol_section(self
, symbols_dict
, name
, indent
):
579 # FIXME invalid names other than the empty string aren't flagged
581 raise QAPIParseError(self
._parser
, "invalid parameter name")
582 if name
in symbols_dict
:
583 raise QAPIParseError(self
._parser
,
584 "'%s' parameter name duplicated" % name
)
585 assert not self
.sections
587 self
._section
= QAPIDoc
.ArgSection(self
._parser
, name
, indent
)
588 symbols_dict
[name
] = self
._section
590 def _start_args_section(self
, name
, indent
):
591 self
._start
_symbol
_section
(self
.args
, name
, indent
)
593 def _start_features_section(self
, name
, indent
):
594 self
._start
_symbol
_section
(self
.features
, name
, indent
)
596 def _start_section(self
, name
=None, indent
=0):
597 if name
in ('Returns', 'Since') and self
.has_section(name
):
598 raise QAPIParseError(self
._parser
,
599 "duplicated '%s' section" % name
)
601 self
._section
= QAPIDoc
.Section(self
._parser
, name
, indent
)
602 self
.sections
.append(self
._section
)
604 def _end_section(self
):
606 text
= self
._section
.text
= self
._section
.text
.strip()
607 if self
._section
.name
and (not text
or text
.isspace()):
608 raise QAPIParseError(
610 "empty doc section '%s'" % self
._section
.name
)
613 def _append_freeform(self
, line
):
614 match
= re
.match(r
'(@\S+:)', line
)
616 raise QAPIParseError(self
._parser
,
617 "'%s' not allowed in free-form documentation"
619 self
._section
.append(line
)
621 def connect_member(self
, member
):
622 if member
.name
not in self
.args
:
623 # Undocumented TODO outlaw
624 self
.args
[member
.name
] = QAPIDoc
.ArgSection(self
._parser
,
626 self
.args
[member
.name
].connect(member
)
628 def connect_feature(self
, feature
):
629 if feature
.name
not in self
.features
:
630 raise QAPISemError(feature
.info
,
631 "feature '%s' lacks documentation"
633 self
.features
[feature
.name
].connect(feature
)
635 def check_expr(self
, expr
):
636 if self
.has_section('Returns') and 'command' not in expr
:
637 raise QAPISemError(self
.info
,
638 "'Returns:' is only valid for commands")
642 def check_args_section(args
, info
, what
):
643 bogus
= [name
for name
, section
in args
.items()
644 if not section
.member
]
648 "documented member%s '%s' %s not exist"
649 % ("s" if len(bogus
) > 1 else "",
651 "do" if len(bogus
) > 1 else "does"))
653 check_args_section(self
.args
, self
.info
, 'members')
654 check_args_section(self
.features
, self
.info
, 'features')