1# -*- coding: utf-8 -*- 2# 3# QAPI schema parser 4# 5# Copyright IBM, Corp. 2011 6# Copyright (c) 2013-2019 Red Hat Inc. 7# 8# Authors: 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> 13# 14# This work is licensed under the terms of the GNU GPL, version 2. 15# See the COPYING file in the top-level directory. 16 17from collections import OrderedDict 18import os 19import re 20from typing import ( 21 TYPE_CHECKING, 22 Dict, 23 List, 24 Optional, 25 Set, 26 Union, 27) 28 29from .common import must_match 30from .error import QAPISemError, QAPISourceError 31from .source import QAPISourceInfo 32 33 34if TYPE_CHECKING: 35 # pylint: disable=cyclic-import 36 # TODO: Remove cycle. [schema -> expr -> parser -> schema] 37 from .schema import QAPISchemaFeature, QAPISchemaMember 38 39 40#: Represents a single Top Level QAPI schema expression. 41TopLevelExpr = Dict[str, object] 42 43# Return value alias for get_expr(). 44_ExprValue = Union[List[object], Dict[str, object], str, bool] 45 46 47class QAPIParseError(QAPISourceError): 48 """Error class for all QAPI schema parsing errors.""" 49 def __init__(self, parser: 'QAPISchemaParser', msg: str): 50 col = 1 51 for ch in parser.src[parser.line_pos:parser.pos]: 52 if ch == '\t': 53 col = (col + 7) % 8 + 1 54 else: 55 col += 1 56 super().__init__(parser.info, msg, col) 57 58 59class QAPISchemaParser: 60 """ 61 Parse QAPI schema source. 62 63 Parse a JSON-esque schema file and process directives. See 64 qapi-code-gen.txt section "Schema Syntax" for the exact syntax. 65 Grammatical validation is handled later by `expr.check_exprs()`. 66 67 :param fname: Source file name. 68 :param previously_included: 69 The absolute names of previously included source files, 70 if being invoked from another parser. 71 :param incl_info: 72 `QAPISourceInfo` belonging to the parent module. 73 ``None`` implies this is the root module. 74 75 :ivar exprs: Resulting parsed expressions. 76 :ivar docs: Resulting parsed documentation blocks. 77 78 :raise OSError: For problems reading the root schema document. 79 :raise QAPIError: For errors in the schema source. 80 """ 81 def __init__(self, 82 fname: str, 83 previously_included: Optional[Set[str]] = None, 84 incl_info: Optional[QAPISourceInfo] = None): 85 self._fname = fname 86 self._included = previously_included or set() 87 self._included.add(os.path.abspath(self._fname)) 88 self.src = '' 89 90 # Lexer state (see `accept` for details): 91 self.info = QAPISourceInfo(self._fname, incl_info) 92 self.tok: Union[None, str] = None 93 self.pos = 0 94 self.cursor = 0 95 self.val: Optional[Union[bool, str]] = None 96 self.line_pos = 0 97 98 # Parser output: 99 self.exprs: List[Dict[str, object]] = [] 100 self.docs: List[QAPIDoc] = [] 101 102 # Showtime! 103 self._parse() 104 105 def _parse(self) -> None: 106 """ 107 Parse the QAPI schema document. 108 109 :return: None. Results are stored in ``.exprs`` and ``.docs``. 110 """ 111 cur_doc = None 112 113 # May raise OSError; allow the caller to handle it. 114 with open(self._fname, 'r', encoding='utf-8') as fp: 115 self.src = fp.read() 116 if self.src == '' or self.src[-1] != '\n': 117 self.src += '\n' 118 119 # Prime the lexer: 120 self.accept() 121 122 # Parse until done: 123 while self.tok is not None: 124 info = self.info 125 if self.tok == '#': 126 self.reject_expr_doc(cur_doc) 127 for cur_doc in self.get_doc(info): 128 self.docs.append(cur_doc) 129 continue 130 131 expr = self.get_expr() 132 if not isinstance(expr, dict): 133 raise QAPISemError( 134 info, "top-level expression must be an object") 135 136 if 'include' in expr: 137 self.reject_expr_doc(cur_doc) 138 if len(expr) != 1: 139 raise QAPISemError(info, "invalid 'include' directive") 140 include = expr['include'] 141 if not isinstance(include, str): 142 raise QAPISemError(info, 143 "value of 'include' must be a string") 144 incl_fname = os.path.join(os.path.dirname(self._fname), 145 include) 146 self.exprs.append({'expr': {'include': incl_fname}, 147 'info': info}) 148 exprs_include = self._include(include, info, incl_fname, 149 self._included) 150 if exprs_include: 151 self.exprs.extend(exprs_include.exprs) 152 self.docs.extend(exprs_include.docs) 153 elif "pragma" in expr: 154 self.reject_expr_doc(cur_doc) 155 if len(expr) != 1: 156 raise QAPISemError(info, "invalid 'pragma' directive") 157 pragma = expr['pragma'] 158 if not isinstance(pragma, dict): 159 raise QAPISemError( 160 info, "value of 'pragma' must be an object") 161 for name, value in pragma.items(): 162 self._pragma(name, value, info) 163 else: 164 expr_elem = {'expr': expr, 165 'info': info} 166 if cur_doc: 167 if not cur_doc.symbol: 168 raise QAPISemError( 169 cur_doc.info, "definition documentation required") 170 expr_elem['doc'] = cur_doc 171 self.exprs.append(expr_elem) 172 cur_doc = None 173 self.reject_expr_doc(cur_doc) 174 175 @staticmethod 176 def reject_expr_doc(doc: Optional['QAPIDoc']) -> None: 177 if doc and doc.symbol: 178 raise QAPISemError( 179 doc.info, 180 "documentation for '%s' is not followed by the definition" 181 % doc.symbol) 182 183 @staticmethod 184 def _include(include: str, 185 info: QAPISourceInfo, 186 incl_fname: str, 187 previously_included: Set[str] 188 ) -> Optional['QAPISchemaParser']: 189 incl_abs_fname = os.path.abspath(incl_fname) 190 # catch inclusion cycle 191 inf: Optional[QAPISourceInfo] = info 192 while inf: 193 if incl_abs_fname == os.path.abspath(inf.fname): 194 raise QAPISemError(info, "inclusion loop for %s" % include) 195 inf = inf.parent 196 197 # skip multiple include of the same file 198 if incl_abs_fname in previously_included: 199 return None 200 201 try: 202 return QAPISchemaParser(incl_fname, previously_included, info) 203 except OSError as err: 204 raise QAPISemError( 205 info, 206 f"can't read include file '{incl_fname}': {err.strerror}" 207 ) from err 208 209 @staticmethod 210 def _pragma(name: str, value: object, info: QAPISourceInfo) -> None: 211 212 def check_list_str(name: str, value: object) -> List[str]: 213 if (not isinstance(value, list) or 214 any(not isinstance(elt, str) for elt in value)): 215 raise QAPISemError( 216 info, 217 "pragma %s must be a list of strings" % name) 218 return value 219 220 pragma = info.pragma 221 222 if name == 'doc-required': 223 if not isinstance(value, bool): 224 raise QAPISemError(info, 225 "pragma 'doc-required' must be boolean") 226 pragma.doc_required = value 227 elif name == 'command-name-exceptions': 228 pragma.command_name_exceptions = check_list_str(name, value) 229 elif name == 'command-returns-exceptions': 230 pragma.command_returns_exceptions = check_list_str(name, value) 231 elif name == 'member-name-exceptions': 232 pragma.member_name_exceptions = check_list_str(name, value) 233 else: 234 raise QAPISemError(info, "unknown pragma '%s'" % name) 235 236 def accept(self, skip_comment: bool = True) -> None: 237 """ 238 Read and store the next token. 239 240 :param skip_comment: 241 When false, return COMMENT tokens ("#"). 242 This is used when reading documentation blocks. 243 244 :return: 245 None. Several instance attributes are updated instead: 246 247 - ``.tok`` represents the token type. See below for values. 248 - ``.info`` describes the token's source location. 249 - ``.val`` is the token's value, if any. See below. 250 - ``.pos`` is the buffer index of the first character of 251 the token. 252 253 * Single-character tokens: 254 255 These are "{", "}", ":", ",", "[", and "]". 256 ``.tok`` holds the single character and ``.val`` is None. 257 258 * Multi-character tokens: 259 260 * COMMENT: 261 262 This token is not normally returned by the lexer, but it can 263 be when ``skip_comment`` is False. ``.tok`` is "#", and 264 ``.val`` is a string including all chars until end-of-line, 265 including the "#" itself. 266 267 * STRING: 268 269 ``.tok`` is "'", the single quote. ``.val`` contains the 270 string, excluding the surrounding quotes. 271 272 * TRUE and FALSE: 273 274 ``.tok`` is either "t" or "f", ``.val`` will be the 275 corresponding bool value. 276 277 * EOF: 278 279 ``.tok`` and ``.val`` will both be None at EOF. 280 """ 281 while True: 282 self.tok = self.src[self.cursor] 283 self.pos = self.cursor 284 self.cursor += 1 285 self.val = None 286 287 if self.tok == '#': 288 if self.src[self.cursor] == '#': 289 # Start of doc comment 290 skip_comment = False 291 self.cursor = self.src.find('\n', self.cursor) 292 if not skip_comment: 293 self.val = self.src[self.pos:self.cursor] 294 return 295 elif self.tok in '{}:,[]': 296 return 297 elif self.tok == "'": 298 # Note: we accept only printable ASCII 299 string = '' 300 esc = False 301 while True: 302 ch = self.src[self.cursor] 303 self.cursor += 1 304 if ch == '\n': 305 raise QAPIParseError(self, "missing terminating \"'\"") 306 if esc: 307 # Note: we recognize only \\ because we have 308 # no use for funny characters in strings 309 if ch != '\\': 310 raise QAPIParseError(self, 311 "unknown escape \\%s" % ch) 312 esc = False 313 elif ch == '\\': 314 esc = True 315 continue 316 elif ch == "'": 317 self.val = string 318 return 319 if ord(ch) < 32 or ord(ch) >= 127: 320 raise QAPIParseError( 321 self, "funny character in string") 322 string += ch 323 elif self.src.startswith('true', self.pos): 324 self.val = True 325 self.cursor += 3 326 return 327 elif self.src.startswith('false', self.pos): 328 self.val = False 329 self.cursor += 4 330 return 331 elif self.tok == '\n': 332 if self.cursor == len(self.src): 333 self.tok = None 334 return 335 self.info = self.info.next_line() 336 self.line_pos = self.cursor 337 elif not self.tok.isspace(): 338 # Show up to next structural, whitespace or quote 339 # character 340 match = must_match('[^[\\]{}:,\\s\'"]+', 341 self.src[self.cursor-1:]) 342 raise QAPIParseError(self, "stray '%s'" % match.group(0)) 343 344 def get_members(self) -> Dict[str, object]: 345 expr: Dict[str, object] = OrderedDict() 346 if self.tok == '}': 347 self.accept() 348 return expr 349 if self.tok != "'": 350 raise QAPIParseError(self, "expected string or '}'") 351 while True: 352 key = self.val 353 assert isinstance(key, str) # Guaranteed by tok == "'" 354 355 self.accept() 356 if self.tok != ':': 357 raise QAPIParseError(self, "expected ':'") 358 self.accept() 359 if key in expr: 360 raise QAPIParseError(self, "duplicate key '%s'" % key) 361 expr[key] = self.get_expr() 362 if self.tok == '}': 363 self.accept() 364 return expr 365 if self.tok != ',': 366 raise QAPIParseError(self, "expected ',' or '}'") 367 self.accept() 368 if self.tok != "'": 369 raise QAPIParseError(self, "expected string") 370 371 def get_values(self) -> List[object]: 372 expr: List[object] = [] 373 if self.tok == ']': 374 self.accept() 375 return expr 376 if self.tok not in tuple("{['tf"): 377 raise QAPIParseError( 378 self, "expected '{', '[', ']', string, or boolean") 379 while True: 380 expr.append(self.get_expr()) 381 if self.tok == ']': 382 self.accept() 383 return expr 384 if self.tok != ',': 385 raise QAPIParseError(self, "expected ',' or ']'") 386 self.accept() 387 388 def get_expr(self) -> _ExprValue: 389 expr: _ExprValue 390 if self.tok == '{': 391 self.accept() 392 expr = self.get_members() 393 elif self.tok == '[': 394 self.accept() 395 expr = self.get_values() 396 elif self.tok in tuple("'tf"): 397 assert isinstance(self.val, (str, bool)) 398 expr = self.val 399 self.accept() 400 else: 401 raise QAPIParseError( 402 self, "expected '{', '[', string, or boolean") 403 return expr 404 405 def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']: 406 if self.val != '##': 407 raise QAPIParseError( 408 self, "junk after '##' at start of documentation comment") 409 410 docs = [] 411 cur_doc = QAPIDoc(self, info) 412 self.accept(False) 413 while self.tok == '#': 414 assert isinstance(self.val, str) 415 if self.val.startswith('##'): 416 # End of doc comment 417 if self.val != '##': 418 raise QAPIParseError( 419 self, 420 "junk after '##' at end of documentation comment") 421 cur_doc.end_comment() 422 docs.append(cur_doc) 423 self.accept() 424 return docs 425 if self.val.startswith('# ='): 426 if cur_doc.symbol: 427 raise QAPIParseError( 428 self, 429 "unexpected '=' markup in definition documentation") 430 if cur_doc.body.text: 431 cur_doc.end_comment() 432 docs.append(cur_doc) 433 cur_doc = QAPIDoc(self, info) 434 cur_doc.append(self.val) 435 self.accept(False) 436 437 raise QAPIParseError(self, "documentation comment must end with '##'") 438 439 440class QAPIDoc: 441 """ 442 A documentation comment block, either definition or free-form 443 444 Definition documentation blocks consist of 445 446 * a body section: one line naming the definition, followed by an 447 overview (any number of lines) 448 449 * argument sections: a description of each argument (for commands 450 and events) or member (for structs, unions and alternates) 451 452 * features sections: a description of each feature flag 453 454 * additional (non-argument) sections, possibly tagged 455 456 Free-form documentation blocks consist only of a body section. 457 """ 458 459 class Section: 460 def __init__(self, parser: QAPISchemaParser, 461 name: Optional[str] = None, indent: int = 0): 462 # parser, for error messages about indentation 463 self._parser = parser 464 # optional section name (argument/member or section name) 465 self.name = name 466 self.text = '' 467 # the expected indent level of the text of this section 468 self._indent = indent 469 470 def append(self, line: str) -> None: 471 # Strip leading spaces corresponding to the expected indent level 472 # Blank lines are always OK. 473 if line: 474 indent = must_match(r'\s*', line).end() 475 if indent < self._indent: 476 raise QAPIParseError( 477 self._parser, 478 "unexpected de-indent (expected at least %d spaces)" % 479 self._indent) 480 line = line[self._indent:] 481 482 self.text += line.rstrip() + '\n' 483 484 class ArgSection(Section): 485 def __init__(self, parser: QAPISchemaParser, 486 name: str, indent: int = 0): 487 super().__init__(parser, name, indent) 488 self.member: Optional['QAPISchemaMember'] = None 489 490 def connect(self, member: 'QAPISchemaMember') -> None: 491 self.member = member 492 493 class NullSection(Section): 494 """ 495 Immutable dummy section for use at the end of a doc block. 496 """ 497 def append(self, line: str) -> None: 498 assert False, "Text appended after end_comment() called." 499 500 def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo): 501 # self._parser is used to report errors with QAPIParseError. The 502 # resulting error position depends on the state of the parser. 503 # It happens to be the beginning of the comment. More or less 504 # servicable, but action at a distance. 505 self._parser = parser 506 self.info = info 507 self.symbol: Optional[str] = None 508 self.body = QAPIDoc.Section(parser) 509 # dicts mapping parameter/feature names to their ArgSection 510 self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict() 511 self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict() 512 self.sections: List[QAPIDoc.Section] = [] 513 # the current section 514 self._section = self.body 515 self._append_line = self._append_body_line 516 517 def has_section(self, name: str) -> bool: 518 """Return True if we have a section with this name.""" 519 for i in self.sections: 520 if i.name == name: 521 return True 522 return False 523 524 def append(self, line: str) -> None: 525 """ 526 Parse a comment line and add it to the documentation. 527 528 The way that the line is dealt with depends on which part of 529 the documentation we're parsing right now: 530 * The body section: ._append_line is ._append_body_line 531 * An argument section: ._append_line is ._append_args_line 532 * A features section: ._append_line is ._append_features_line 533 * An additional section: ._append_line is ._append_various_line 534 """ 535 line = line[1:] 536 if not line: 537 self._append_freeform(line) 538 return 539 540 if line[0] != ' ': 541 raise QAPIParseError(self._parser, "missing space after #") 542 line = line[1:] 543 self._append_line(line) 544 545 def end_comment(self) -> None: 546 self._switch_section(QAPIDoc.NullSection(self._parser)) 547 548 @staticmethod 549 def _is_section_tag(name: str) -> bool: 550 return name in ('Returns:', 'Since:', 551 # those are often singular or plural 552 'Note:', 'Notes:', 553 'Example:', 'Examples:', 554 'TODO:') 555 556 def _append_body_line(self, line: str) -> None: 557 """ 558 Process a line of documentation text in the body section. 559 560 If this a symbol line and it is the section's first line, this 561 is a definition documentation block for that symbol. 562 563 If it's a definition documentation block, another symbol line 564 begins the argument section for the argument named by it, and 565 a section tag begins an additional section. Start that 566 section and append the line to it. 567 568 Else, append the line to the current section. 569 """ 570 name = line.split(' ', 1)[0] 571 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't 572 # recognized, and get silently treated as ordinary text 573 if not self.symbol and not self.body.text and line.startswith('@'): 574 if not line.endswith(':'): 575 raise QAPIParseError(self._parser, "line should end with ':'") 576 self.symbol = line[1:-1] 577 # Invalid names are not checked here, but the name provided MUST 578 # match the following definition, which *is* validated in expr.py. 579 if not self.symbol: 580 raise QAPIParseError( 581 self._parser, "name required after '@'") 582 elif self.symbol: 583 # This is a definition documentation block 584 if name.startswith('@') and name.endswith(':'): 585 self._append_line = self._append_args_line 586 self._append_args_line(line) 587 elif line == 'Features:': 588 self._append_line = self._append_features_line 589 elif self._is_section_tag(name): 590 self._append_line = self._append_various_line 591 self._append_various_line(line) 592 else: 593 self._append_freeform(line) 594 else: 595 # This is a free-form documentation block 596 self._append_freeform(line) 597 598 def _append_args_line(self, line: str) -> None: 599 """ 600 Process a line of documentation text in an argument section. 601 602 A symbol line begins the next argument section, a section tag 603 section or a non-indented line after a blank line begins an 604 additional section. Start that section and append the line to 605 it. 606 607 Else, append the line to the current section. 608 609 """ 610 name = line.split(' ', 1)[0] 611 612 if name.startswith('@') and name.endswith(':'): 613 # If line is "@arg: first line of description", find 614 # the index of 'f', which is the indent we expect for any 615 # following lines. We then remove the leading "@arg:" 616 # from line and replace it with spaces so that 'f' has the 617 # same index as it did in the original line and can be 618 # handled the same way we will handle following lines. 619 indent = must_match(r'@\S*:\s*', line).end() 620 line = line[indent:] 621 if not line: 622 # Line was just the "@arg:" header; following lines 623 # are not indented 624 indent = 0 625 else: 626 line = ' ' * indent + line 627 self._start_args_section(name[1:-1], indent) 628 elif self._is_section_tag(name): 629 self._append_line = self._append_various_line 630 self._append_various_line(line) 631 return 632 elif (self._section.text.endswith('\n\n') 633 and line and not line[0].isspace()): 634 if line == 'Features:': 635 self._append_line = self._append_features_line 636 else: 637 self._start_section() 638 self._append_line = self._append_various_line 639 self._append_various_line(line) 640 return 641 642 self._append_freeform(line) 643 644 def _append_features_line(self, line: str) -> None: 645 name = line.split(' ', 1)[0] 646 647 if name.startswith('@') and name.endswith(':'): 648 # If line is "@arg: first line of description", find 649 # the index of 'f', which is the indent we expect for any 650 # following lines. We then remove the leading "@arg:" 651 # from line and replace it with spaces so that 'f' has the 652 # same index as it did in the original line and can be 653 # handled the same way we will handle following lines. 654 indent = must_match(r'@\S*:\s*', line).end() 655 line = line[indent:] 656 if not line: 657 # Line was just the "@arg:" header; following lines 658 # are not indented 659 indent = 0 660 else: 661 line = ' ' * indent + line 662 self._start_features_section(name[1:-1], indent) 663 elif self._is_section_tag(name): 664 self._append_line = self._append_various_line 665 self._append_various_line(line) 666 return 667 elif (self._section.text.endswith('\n\n') 668 and line and not line[0].isspace()): 669 self._start_section() 670 self._append_line = self._append_various_line 671 self._append_various_line(line) 672 return 673 674 self._append_freeform(line) 675 676 def _append_various_line(self, line: str) -> None: 677 """ 678 Process a line of documentation text in an additional section. 679 680 A symbol line is an error. 681 682 A section tag begins an additional section. Start that 683 section and append the line to it. 684 685 Else, append the line to the current section. 686 """ 687 name = line.split(' ', 1)[0] 688 689 if name.startswith('@') and name.endswith(':'): 690 raise QAPIParseError(self._parser, 691 "'%s' can't follow '%s' section" 692 % (name, self.sections[0].name)) 693 if self._is_section_tag(name): 694 # If line is "Section: first line of description", find 695 # the index of 'f', which is the indent we expect for any 696 # following lines. We then remove the leading "Section:" 697 # from line and replace it with spaces so that 'f' has the 698 # same index as it did in the original line and can be 699 # handled the same way we will handle following lines. 700 indent = must_match(r'\S*:\s*', line).end() 701 line = line[indent:] 702 if not line: 703 # Line was just the "Section:" header; following lines 704 # are not indented 705 indent = 0 706 else: 707 line = ' ' * indent + line 708 self._start_section(name[:-1], indent) 709 710 self._append_freeform(line) 711 712 def _start_symbol_section( 713 self, 714 symbols_dict: Dict[str, 'QAPIDoc.ArgSection'], 715 name: str, 716 indent: int) -> None: 717 # FIXME invalid names other than the empty string aren't flagged 718 if not name: 719 raise QAPIParseError(self._parser, "invalid parameter name") 720 if name in symbols_dict: 721 raise QAPIParseError(self._parser, 722 "'%s' parameter name duplicated" % name) 723 assert not self.sections 724 new_section = QAPIDoc.ArgSection(self._parser, name, indent) 725 self._switch_section(new_section) 726 symbols_dict[name] = new_section 727 728 def _start_args_section(self, name: str, indent: int) -> None: 729 self._start_symbol_section(self.args, name, indent) 730 731 def _start_features_section(self, name: str, indent: int) -> None: 732 self._start_symbol_section(self.features, name, indent) 733 734 def _start_section(self, name: Optional[str] = None, 735 indent: int = 0) -> None: 736 if name in ('Returns', 'Since') and self.has_section(name): 737 raise QAPIParseError(self._parser, 738 "duplicated '%s' section" % name) 739 new_section = QAPIDoc.Section(self._parser, name, indent) 740 self._switch_section(new_section) 741 self.sections.append(new_section) 742 743 def _switch_section(self, new_section: 'QAPIDoc.Section') -> None: 744 text = self._section.text = self._section.text.strip() 745 746 # Only the 'body' section is allowed to have an empty body. 747 # All other sections, including anonymous ones, must have text. 748 if self._section != self.body and not text: 749 # We do not create anonymous sections unless there is 750 # something to put in them; this is a parser bug. 751 assert self._section.name 752 raise QAPIParseError( 753 self._parser, 754 "empty doc section '%s'" % self._section.name) 755 756 self._section = new_section 757 758 def _append_freeform(self, line: str) -> None: 759 match = re.match(r'(@\S+:)', line) 760 if match: 761 raise QAPIParseError(self._parser, 762 "'%s' not allowed in free-form documentation" 763 % match.group(1)) 764 self._section.append(line) 765 766 def connect_member(self, member: 'QAPISchemaMember') -> None: 767 if member.name not in self.args: 768 # Undocumented TODO outlaw 769 self.args[member.name] = QAPIDoc.ArgSection(self._parser, 770 member.name) 771 self.args[member.name].connect(member) 772 773 def connect_feature(self, feature: 'QAPISchemaFeature') -> None: 774 if feature.name not in self.features: 775 raise QAPISemError(feature.info, 776 "feature '%s' lacks documentation" 777 % feature.name) 778 self.features[feature.name].connect(feature) 779 780 def check_expr(self, expr: TopLevelExpr) -> None: 781 if self.has_section('Returns') and 'command' not in expr: 782 raise QAPISemError(self.info, 783 "'Returns:' is only valid for commands") 784 785 def check(self) -> None: 786 787 def check_args_section( 788 args: Dict[str, QAPIDoc.ArgSection], what: str 789 ) -> None: 790 bogus = [name for name, section in args.items() 791 if not section.member] 792 if bogus: 793 raise QAPISemError( 794 self.info, 795 "documented %s%s '%s' %s not exist" % ( 796 what, 797 "s" if len(bogus) > 1 else "", 798 "', '".join(bogus), 799 "do" if len(bogus) > 1 else "does" 800 )) 801 802 check_args_section(self.args, 'member') 803 check_args_section(self.features, 'feature') 804