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