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