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
29 from .common
import must_match
30 from .error
import QAPISemError
, QAPISourceError
31 from .source
import QAPISourceInfo
35 # pylint: disable=cyclic-import
36 # TODO: Remove cycle. [schema -> expr -> parser -> schema]
37 from .schema
import QAPISchemaFeature
, QAPISchemaMember
40 #: Represents a single Top Level QAPI schema expression.
41 TopLevelExpr
= Dict
[str, object]
43 # Return value alias for get_expr().
44 _ExprValue
= Union
[List
[object], Dict
[str, object], str, bool]
46 # FIXME: Consolidate and centralize definitions for TopLevelExpr,
47 # _ExprValue, _JSONValue, and _JSONObject; currently scattered across
51 class QAPIParseError(QAPISourceError
):
52 """Error class for all QAPI schema parsing errors."""
53 def __init__(self
, parser
: 'QAPISchemaParser', msg
: str):
55 for ch
in parser
.src
[parser
.line_pos
:parser
.pos
]:
57 col
= (col
+ 7) % 8 + 1
60 super().__init
__(parser
.info
, msg
, col
)
63 class QAPISchemaParser
:
65 Parse QAPI schema source.
67 Parse a JSON-esque schema file and process directives. See
68 qapi-code-gen.txt section "Schema Syntax" for the exact syntax.
69 Grammatical validation is handled later by `expr.check_exprs()`.
71 :param fname: Source file name.
72 :param previously_included:
73 The absolute names of previously included source files,
74 if being invoked from another parser.
76 `QAPISourceInfo` belonging to the parent module.
77 ``None`` implies this is the root module.
79 :ivar exprs: Resulting parsed expressions.
80 :ivar docs: Resulting parsed documentation blocks.
82 :raise OSError: For problems reading the root schema document.
83 :raise QAPIError: For errors in the schema source.
87 previously_included
: Optional
[Set
[str]] = None,
88 incl_info
: Optional
[QAPISourceInfo
] = None):
90 self
._included
= previously_included
or set()
91 self
._included
.add(os
.path
.abspath(self
._fname
))
94 # Lexer state (see `accept` for details):
95 self
.info
= QAPISourceInfo(self
._fname
, incl_info
)
96 self
.tok
: Union
[None, str] = None
99 self
.val
: Optional
[Union
[bool, str]] = None
103 self
.exprs
: List
[Dict
[str, object]] = []
104 self
.docs
: List
[QAPIDoc
] = []
109 def _parse(self
) -> None:
111 Parse the QAPI schema document.
113 :return: None. Results are stored in ``.exprs`` and ``.docs``.
117 # May raise OSError; allow the caller to handle it.
118 with
open(self
._fname
, 'r', encoding
='utf-8') as fp
:
120 if self
.src
== '' or self
.src
[-1] != '\n':
127 while self
.tok
is not None:
130 self
.reject_expr_doc(cur_doc
)
131 for cur_doc
in self
.get_doc(info
):
132 self
.docs
.append(cur_doc
)
135 expr
= self
.get_expr()
136 if not isinstance(expr
, dict):
138 info
, "top-level expression must be an object")
140 if 'include' in expr
:
141 self
.reject_expr_doc(cur_doc
)
143 raise QAPISemError(info
, "invalid 'include' directive")
144 include
= expr
['include']
145 if not isinstance(include
, str):
146 raise QAPISemError(info
,
147 "value of 'include' must be a string")
148 incl_fname
= os
.path
.join(os
.path
.dirname(self
._fname
),
150 self
.exprs
.append({'expr': {'include': incl_fname
},
152 exprs_include
= self
._include
(include
, info
, incl_fname
,
155 self
.exprs
.extend(exprs_include
.exprs
)
156 self
.docs
.extend(exprs_include
.docs
)
157 elif "pragma" in expr
:
158 self
.reject_expr_doc(cur_doc
)
160 raise QAPISemError(info
, "invalid 'pragma' directive")
161 pragma
= expr
['pragma']
162 if not isinstance(pragma
, dict):
164 info
, "value of 'pragma' must be an object")
165 for name
, value
in pragma
.items():
166 self
._pragma
(name
, value
, info
)
168 expr_elem
= {'expr': expr
,
171 if not cur_doc
.symbol
:
173 cur_doc
.info
, "definition documentation required")
174 expr_elem
['doc'] = cur_doc
175 self
.exprs
.append(expr_elem
)
177 self
.reject_expr_doc(cur_doc
)
180 def reject_expr_doc(doc
: Optional
['QAPIDoc']) -> None:
181 if doc
and doc
.symbol
:
184 "documentation for '%s' is not followed by the definition"
188 def _include(include
: str,
189 info
: QAPISourceInfo
,
191 previously_included
: Set
[str]
192 ) -> Optional
['QAPISchemaParser']:
193 incl_abs_fname
= os
.path
.abspath(incl_fname
)
194 # catch inclusion cycle
195 inf
: Optional
[QAPISourceInfo
] = info
197 if incl_abs_fname
== os
.path
.abspath(inf
.fname
):
198 raise QAPISemError(info
, "inclusion loop for %s" % include
)
201 # skip multiple include of the same file
202 if incl_abs_fname
in previously_included
:
206 return QAPISchemaParser(incl_fname
, previously_included
, info
)
207 except OSError as err
:
210 f
"can't read include file '{incl_fname}': {err.strerror}"
214 def _pragma(name
: str, value
: object, info
: QAPISourceInfo
) -> None:
216 def check_list_str(name
: str, value
: object) -> List
[str]:
217 if (not isinstance(value
, list) or
218 any(not isinstance(elt
, str) for elt
in value
)):
221 "pragma %s must be a list of strings" % name
)
226 if name
== 'doc-required':
227 if not isinstance(value
, bool):
228 raise QAPISemError(info
,
229 "pragma 'doc-required' must be boolean")
230 pragma
.doc_required
= value
231 elif name
== 'command-name-exceptions':
232 pragma
.command_name_exceptions
= check_list_str(name
, value
)
233 elif name
== 'command-returns-exceptions':
234 pragma
.command_returns_exceptions
= check_list_str(name
, value
)
235 elif name
== 'member-name-exceptions':
236 pragma
.member_name_exceptions
= check_list_str(name
, value
)
238 raise QAPISemError(info
, "unknown pragma '%s'" % name
)
240 def accept(self
, skip_comment
: bool = True) -> None:
242 Read and store the next token.
245 When false, return COMMENT tokens ("#").
246 This is used when reading documentation blocks.
249 None. Several instance attributes are updated instead:
251 - ``.tok`` represents the token type. See below for values.
252 - ``.info`` describes the token's source location.
253 - ``.val`` is the token's value, if any. See below.
254 - ``.pos`` is the buffer index of the first character of
257 * Single-character tokens:
259 These are "{", "}", ":", ",", "[", and "]".
260 ``.tok`` holds the single character and ``.val`` is None.
262 * Multi-character tokens:
266 This token is not normally returned by the lexer, but it can
267 be when ``skip_comment`` is False. ``.tok`` is "#", and
268 ``.val`` is a string including all chars until end-of-line,
269 including the "#" itself.
273 ``.tok`` is "'", the single quote. ``.val`` contains the
274 string, excluding the surrounding quotes.
278 ``.tok`` is either "t" or "f", ``.val`` will be the
279 corresponding bool value.
283 ``.tok`` and ``.val`` will both be None at EOF.
286 self
.tok
= self
.src
[self
.cursor
]
287 self
.pos
= self
.cursor
292 if self
.src
[self
.cursor
] == '#':
293 # Start of doc comment
295 self
.cursor
= self
.src
.find('\n', self
.cursor
)
297 self
.val
= self
.src
[self
.pos
:self
.cursor
]
299 elif self
.tok
in '{}:,[]':
301 elif self
.tok
== "'":
302 # Note: we accept only printable ASCII
306 ch
= self
.src
[self
.cursor
]
309 raise QAPIParseError(self
, "missing terminating \"'\"")
311 # Note: we recognize only \\ because we have
312 # no use for funny characters in strings
314 raise QAPIParseError(self
,
315 "unknown escape \\%s" % ch
)
323 if ord(ch
) < 32 or ord(ch
) >= 127:
324 raise QAPIParseError(
325 self
, "funny character in string")
327 elif self
.src
.startswith('true', self
.pos
):
331 elif self
.src
.startswith('false', self
.pos
):
335 elif self
.tok
== '\n':
336 if self
.cursor
== len(self
.src
):
339 self
.info
= self
.info
.next_line()
340 self
.line_pos
= self
.cursor
341 elif not self
.tok
.isspace():
342 # Show up to next structural, whitespace or quote
344 match
= must_match('[^[\\]{}:,\\s\'"]+',
345 self
.src
[self
.cursor
-1:])
346 raise QAPIParseError(self
, "stray '%s'" % match
.group(0))
348 def get_members(self
) -> Dict
[str, object]:
349 expr
: Dict
[str, object] = OrderedDict()
354 raise QAPIParseError(self
, "expected string or '}'")
357 assert isinstance(key
, str) # Guaranteed by tok == "'"
361 raise QAPIParseError(self
, "expected ':'")
364 raise QAPIParseError(self
, "duplicate key '%s'" % key
)
365 expr
[key
] = self
.get_expr()
370 raise QAPIParseError(self
, "expected ',' or '}'")
373 raise QAPIParseError(self
, "expected string")
375 def get_values(self
) -> List
[object]:
376 expr
: List
[object] = []
380 if self
.tok
not in tuple("{['tf"):
381 raise QAPIParseError(
382 self
, "expected '{', '[', ']', string, or boolean")
384 expr
.append(self
.get_expr())
389 raise QAPIParseError(self
, "expected ',' or ']'")
392 def get_expr(self
) -> _ExprValue
:
396 expr
= self
.get_members()
397 elif self
.tok
== '[':
399 expr
= self
.get_values()
400 elif self
.tok
in tuple("'tf"):
401 assert isinstance(self
.val
, (str, bool))
405 raise QAPIParseError(
406 self
, "expected '{', '[', string, or boolean")
409 def get_doc(self
, info
: QAPISourceInfo
) -> List
['QAPIDoc']:
411 raise QAPIParseError(
412 self
, "junk after '##' at start of documentation comment")
415 cur_doc
= QAPIDoc(self
, info
)
417 while self
.tok
== '#':
418 assert isinstance(self
.val
, str)
419 if self
.val
.startswith('##'):
422 raise QAPIParseError(
424 "junk after '##' at end of documentation comment")
425 cur_doc
.end_comment()
429 if self
.val
.startswith('# ='):
431 raise QAPIParseError(
433 "unexpected '=' markup in definition documentation")
434 if cur_doc
.body
.text
:
435 cur_doc
.end_comment()
437 cur_doc
= QAPIDoc(self
, info
)
438 cur_doc
.append(self
.val
)
441 raise QAPIParseError(self
, "documentation comment must end with '##'")
446 A documentation comment block, either definition or free-form
448 Definition documentation blocks consist of
450 * a body section: one line naming the definition, followed by an
451 overview (any number of lines)
453 * argument sections: a description of each argument (for commands
454 and events) or member (for structs, unions and alternates)
456 * features sections: a description of each feature flag
458 * additional (non-argument) sections, possibly tagged
460 Free-form documentation blocks consist only of a body section.
464 # pylint: disable=too-few-public-methods
465 def __init__(self
, parser
: QAPISchemaParser
,
466 name
: Optional
[str] = None, indent
: int = 0):
468 # parser, for error messages about indentation
469 self
._parser
= parser
470 # optional section name (argument/member or section name)
473 # the expected indent level of the text of this section
474 self
._indent
= indent
476 def append(self
, line
: str) -> None:
477 # Strip leading spaces corresponding to the expected indent level
478 # Blank lines are always OK.
480 indent
= must_match(r
'\s*', line
).end()
481 if indent
< self
._indent
:
482 raise QAPIParseError(
484 "unexpected de-indent (expected at least %d spaces)" %
486 line
= line
[self
._indent
:]
488 self
.text
+= line
.rstrip() + '\n'
490 class ArgSection(Section
):
491 def __init__(self
, parser
: QAPISchemaParser
,
492 name
: str, indent
: int = 0):
493 super().__init
__(parser
, name
, indent
)
494 self
.member
: Optional
['QAPISchemaMember'] = None
496 def connect(self
, member
: 'QAPISchemaMember') -> None:
499 class NullSection(Section
):
501 Immutable dummy section for use at the end of a doc block.
503 # pylint: disable=too-few-public-methods
504 def append(self
, line
: str) -> None:
505 assert False, "Text appended after end_comment() called."
507 def __init__(self
, parser
: QAPISchemaParser
, info
: QAPISourceInfo
):
508 # self._parser is used to report errors with QAPIParseError. The
509 # resulting error position depends on the state of the parser.
510 # It happens to be the beginning of the comment. More or less
511 # servicable, but action at a distance.
512 self
._parser
= parser
514 self
.symbol
: Optional
[str] = None
515 self
.body
= QAPIDoc
.Section(parser
)
516 # dicts mapping parameter/feature names to their ArgSection
517 self
.args
: Dict
[str, QAPIDoc
.ArgSection
] = OrderedDict()
518 self
.features
: Dict
[str, QAPIDoc
.ArgSection
] = OrderedDict()
519 self
.sections
: List
[QAPIDoc
.Section
] = []
520 # the current section
521 self
._section
= self
.body
522 self
._append
_line
= self
._append
_body
_line
524 def has_section(self
, name
: str) -> bool:
525 """Return True if we have a section with this name."""
526 for i
in self
.sections
:
531 def append(self
, line
: str) -> None:
533 Parse a comment line and add it to the documentation.
535 The way that the line is dealt with depends on which part of
536 the documentation we're parsing right now:
537 * The body section: ._append_line is ._append_body_line
538 * An argument section: ._append_line is ._append_args_line
539 * A features section: ._append_line is ._append_features_line
540 * An additional section: ._append_line is ._append_various_line
544 self
._append
_freeform
(line
)
548 raise QAPIParseError(self
._parser
, "missing space after #")
550 self
._append
_line
(line
)
552 def end_comment(self
) -> None:
553 self
._switch
_section
(QAPIDoc
.NullSection(self
._parser
))
556 def _is_section_tag(name
: str) -> bool:
557 return name
in ('Returns:', 'Since:',
558 # those are often singular or plural
560 'Example:', 'Examples:',
563 def _append_body_line(self
, line
: str) -> None:
565 Process a line of documentation text in the body section.
567 If this a symbol line and it is the section's first line, this
568 is a definition documentation block for that symbol.
570 If it's a definition documentation block, another symbol line
571 begins the argument section for the argument named by it, and
572 a section tag begins an additional section. Start that
573 section and append the line to it.
575 Else, append the line to the current section.
577 name
= line
.split(' ', 1)[0]
578 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
579 # recognized, and get silently treated as ordinary text
580 if not self
.symbol
and not self
.body
.text
and line
.startswith('@'):
581 if not line
.endswith(':'):
582 raise QAPIParseError(self
._parser
, "line should end with ':'")
583 self
.symbol
= line
[1:-1]
584 # Invalid names are not checked here, but the name provided MUST
585 # match the following definition, which *is* validated in expr.py.
587 raise QAPIParseError(
588 self
._parser
, "name required after '@'")
590 # This is a definition documentation block
591 if name
.startswith('@') and name
.endswith(':'):
592 self
._append
_line
= self
._append
_args
_line
593 self
._append
_args
_line
(line
)
594 elif line
== 'Features:':
595 self
._append
_line
= self
._append
_features
_line
596 elif self
._is
_section
_tag
(name
):
597 self
._append
_line
= self
._append
_various
_line
598 self
._append
_various
_line
(line
)
600 self
._append
_freeform
(line
)
602 # This is a free-form documentation block
603 self
._append
_freeform
(line
)
605 def _append_args_line(self
, line
: str) -> None:
607 Process a line of documentation text in an argument section.
609 A symbol line begins the next argument section, a section tag
610 section or a non-indented line after a blank line begins an
611 additional section. Start that section and append the line to
614 Else, append the line to the current section.
617 name
= line
.split(' ', 1)[0]
619 if name
.startswith('@') and name
.endswith(':'):
620 # If line is "@arg: first line of description", find
621 # the index of 'f', which is the indent we expect for any
622 # following lines. We then remove the leading "@arg:"
623 # from line and replace it with spaces so that 'f' has the
624 # same index as it did in the original line and can be
625 # handled the same way we will handle following lines.
626 indent
= must_match(r
'@\S*:\s*', line
).end()
629 # Line was just the "@arg:" header; following lines
633 line
= ' ' * indent
+ line
634 self
._start
_args
_section
(name
[1:-1], indent
)
635 elif self
._is
_section
_tag
(name
):
636 self
._append
_line
= self
._append
_various
_line
637 self
._append
_various
_line
(line
)
639 elif (self
._section
.text
.endswith('\n\n')
640 and line
and not line
[0].isspace()):
641 if line
== 'Features:':
642 self
._append
_line
= self
._append
_features
_line
644 self
._start
_section
()
645 self
._append
_line
= self
._append
_various
_line
646 self
._append
_various
_line
(line
)
649 self
._append
_freeform
(line
)
651 def _append_features_line(self
, line
: str) -> None:
652 name
= line
.split(' ', 1)[0]
654 if name
.startswith('@') and name
.endswith(':'):
655 # If line is "@arg: first line of description", find
656 # the index of 'f', which is the indent we expect for any
657 # following lines. We then remove the leading "@arg:"
658 # from line and replace it with spaces so that 'f' has the
659 # same index as it did in the original line and can be
660 # handled the same way we will handle following lines.
661 indent
= must_match(r
'@\S*:\s*', line
).end()
664 # Line was just the "@arg:" header; following lines
668 line
= ' ' * indent
+ line
669 self
._start
_features
_section
(name
[1:-1], indent
)
670 elif self
._is
_section
_tag
(name
):
671 self
._append
_line
= self
._append
_various
_line
672 self
._append
_various
_line
(line
)
674 elif (self
._section
.text
.endswith('\n\n')
675 and line
and not line
[0].isspace()):
676 self
._start
_section
()
677 self
._append
_line
= self
._append
_various
_line
678 self
._append
_various
_line
(line
)
681 self
._append
_freeform
(line
)
683 def _append_various_line(self
, line
: str) -> None:
685 Process a line of documentation text in an additional section.
687 A symbol line is an error.
689 A section tag begins an additional section. Start that
690 section and append the line to it.
692 Else, append the line to the current section.
694 name
= line
.split(' ', 1)[0]
696 if name
.startswith('@') and name
.endswith(':'):
697 raise QAPIParseError(self
._parser
,
698 "'%s' can't follow '%s' section"
699 % (name
, self
.sections
[0].name
))
700 if self
._is
_section
_tag
(name
):
701 # If line is "Section: first line of description", find
702 # the index of 'f', which is the indent we expect for any
703 # following lines. We then remove the leading "Section:"
704 # from line and replace it with spaces so that 'f' has the
705 # same index as it did in the original line and can be
706 # handled the same way we will handle following lines.
707 indent
= must_match(r
'\S*:\s*', line
).end()
710 # Line was just the "Section:" header; following lines
714 line
= ' ' * indent
+ line
715 self
._start
_section
(name
[:-1], indent
)
717 self
._append
_freeform
(line
)
719 def _start_symbol_section(
721 symbols_dict
: Dict
[str, 'QAPIDoc.ArgSection'],
723 indent
: int) -> None:
724 # FIXME invalid names other than the empty string aren't flagged
726 raise QAPIParseError(self
._parser
, "invalid parameter name")
727 if name
in symbols_dict
:
728 raise QAPIParseError(self
._parser
,
729 "'%s' parameter name duplicated" % name
)
730 assert not self
.sections
731 new_section
= QAPIDoc
.ArgSection(self
._parser
, name
, indent
)
732 self
._switch
_section
(new_section
)
733 symbols_dict
[name
] = new_section
735 def _start_args_section(self
, name
: str, indent
: int) -> None:
736 self
._start
_symbol
_section
(self
.args
, name
, indent
)
738 def _start_features_section(self
, name
: str, indent
: int) -> None:
739 self
._start
_symbol
_section
(self
.features
, name
, indent
)
741 def _start_section(self
, name
: Optional
[str] = None,
742 indent
: int = 0) -> None:
743 if name
in ('Returns', 'Since') and self
.has_section(name
):
744 raise QAPIParseError(self
._parser
,
745 "duplicated '%s' section" % name
)
746 new_section
= QAPIDoc
.Section(self
._parser
, name
, indent
)
747 self
._switch
_section
(new_section
)
748 self
.sections
.append(new_section
)
750 def _switch_section(self
, new_section
: 'QAPIDoc.Section') -> None:
751 text
= self
._section
.text
= self
._section
.text
.strip()
753 # Only the 'body' section is allowed to have an empty body.
754 # All other sections, including anonymous ones, must have text.
755 if self
._section
!= self
.body
and not text
:
756 # We do not create anonymous sections unless there is
757 # something to put in them; this is a parser bug.
758 assert self
._section
.name
759 raise QAPIParseError(
761 "empty doc section '%s'" % self
._section
.name
)
763 self
._section
= new_section
765 def _append_freeform(self
, line
: str) -> None:
766 match
= re
.match(r
'(@\S+:)', line
)
768 raise QAPIParseError(self
._parser
,
769 "'%s' not allowed in free-form documentation"
771 self
._section
.append(line
)
773 def connect_member(self
, member
: 'QAPISchemaMember') -> None:
774 if member
.name
not in self
.args
:
775 # Undocumented TODO outlaw
776 self
.args
[member
.name
] = QAPIDoc
.ArgSection(self
._parser
,
778 self
.args
[member
.name
].connect(member
)
780 def connect_feature(self
, feature
: 'QAPISchemaFeature') -> None:
781 if feature
.name
not in self
.features
:
782 raise QAPISemError(feature
.info
,
783 "feature '%s' lacks documentation"
785 self
.features
[feature
.name
].connect(feature
)
787 def check_expr(self
, expr
: TopLevelExpr
) -> None:
788 if self
.has_section('Returns') and 'command' not in expr
:
789 raise QAPISemError(self
.info
,
790 "'Returns:' is only valid for commands")
792 def check(self
) -> None:
794 def check_args_section(
795 args
: Dict
[str, QAPIDoc
.ArgSection
], what
: str
797 bogus
= [name
for name
, section
in args
.items()
798 if not section
.member
]
802 "documented %s%s '%s' %s not exist" % (
804 "s" if len(bogus
) > 1 else "",
806 "do" if len(bogus
) > 1 else "does"
809 check_args_section(self
.args
, 'member')
810 check_args_section(self
.features
, 'feature')