parser.py (30970B)
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 # pylint: disable=too-few-public-methods 465 def __init__(self, parser: QAPISchemaParser, 466 name: Optional[str] = None, indent: int = 0): 467 468 # parser, for error messages about indentation 469 self._parser = parser 470 # optional section name (argument/member or section name) 471 self.name = name 472 self.text = '' 473 # the expected indent level of the text of this section 474 self._indent = indent 475 476 def append(self, line: str) -> None: 477 # Strip leading spaces corresponding to the expected indent level 478 # Blank lines are always OK. 479 if line: 480 indent = must_match(r'\s*', line).end() 481 if indent < self._indent: 482 raise QAPIParseError( 483 self._parser, 484 "unexpected de-indent (expected at least %d spaces)" % 485 self._indent) 486 line = line[self._indent:] 487 488 self.text += line.rstrip() + '\n' 489 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 495 496 def connect(self, member: 'QAPISchemaMember') -> None: 497 self.member = member 498 499 class NullSection(Section): 500 """ 501 Immutable dummy section for use at the end of a doc block. 502 """ 503 # pylint: disable=too-few-public-methods 504 def append(self, line: str) -> None: 505 assert False, "Text appended after end_comment() called." 506 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 513 self.info = info 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 523 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: 527 if i.name == name: 528 return True 529 return False 530 531 def append(self, line: str) -> None: 532 """ 533 Parse a comment line and add it to the documentation. 534 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 541 """ 542 line = line[1:] 543 if not line: 544 self._append_freeform(line) 545 return 546 547 if line[0] != ' ': 548 raise QAPIParseError(self._parser, "missing space after #") 549 line = line[1:] 550 self._append_line(line) 551 552 def end_comment(self) -> None: 553 self._switch_section(QAPIDoc.NullSection(self._parser)) 554 555 @staticmethod 556 def _is_section_tag(name: str) -> bool: 557 return name in ('Returns:', 'Since:', 558 # those are often singular or plural 559 'Note:', 'Notes:', 560 'Example:', 'Examples:', 561 'TODO:') 562 563 def _append_body_line(self, line: str) -> None: 564 """ 565 Process a line of documentation text in the body section. 566 567 If this a symbol line and it is the section's first line, this 568 is a definition documentation block for that symbol. 569 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. 574 575 Else, append the line to the current section. 576 """ 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. 586 if not self.symbol: 587 raise QAPIParseError( 588 self._parser, "name required after '@'") 589 elif self.symbol: 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) 599 else: 600 self._append_freeform(line) 601 else: 602 # This is a free-form documentation block 603 self._append_freeform(line) 604 605 def _append_args_line(self, line: str) -> None: 606 """ 607 Process a line of documentation text in an argument section. 608 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 612 it. 613 614 Else, append the line to the current section. 615 616 """ 617 name = line.split(' ', 1)[0] 618 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() 627 line = line[indent:] 628 if not line: 629 # Line was just the "@arg:" header; following lines 630 # are not indented 631 indent = 0 632 else: 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) 638 return 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 643 else: 644 self._start_section() 645 self._append_line = self._append_various_line 646 self._append_various_line(line) 647 return 648 649 self._append_freeform(line) 650 651 def _append_features_line(self, line: str) -> None: 652 name = line.split(' ', 1)[0] 653 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() 662 line = line[indent:] 663 if not line: 664 # Line was just the "@arg:" header; following lines 665 # are not indented 666 indent = 0 667 else: 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) 673 return 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) 679 return 680 681 self._append_freeform(line) 682 683 def _append_various_line(self, line: str) -> None: 684 """ 685 Process a line of documentation text in an additional section. 686 687 A symbol line is an error. 688 689 A section tag begins an additional section. Start that 690 section and append the line to it. 691 692 Else, append the line to the current section. 693 """ 694 name = line.split(' ', 1)[0] 695 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() 708 line = line[indent:] 709 if not line: 710 # Line was just the "Section:" header; following lines 711 # are not indented 712 indent = 0 713 else: 714 line = ' ' * indent + line 715 self._start_section(name[:-1], indent) 716 717 self._append_freeform(line) 718 719 def _start_symbol_section( 720 self, 721 symbols_dict: Dict[str, 'QAPIDoc.ArgSection'], 722 name: str, 723 indent: int) -> None: 724 # FIXME invalid names other than the empty string aren't flagged 725 if not name: 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 734 735 def _start_args_section(self, name: str, indent: int) -> None: 736 self._start_symbol_section(self.args, name, indent) 737 738 def _start_features_section(self, name: str, indent: int) -> None: 739 self._start_symbol_section(self.features, name, indent) 740 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) 749 750 def _switch_section(self, new_section: 'QAPIDoc.Section') -> None: 751 text = self._section.text = self._section.text.strip() 752 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( 760 self._parser, 761 "empty doc section '%s'" % self._section.name) 762 763 self._section = new_section 764 765 def _append_freeform(self, line: str) -> None: 766 match = re.match(r'(@\S+:)', line) 767 if match: 768 raise QAPIParseError(self._parser, 769 "'%s' not allowed in free-form documentation" 770 % match.group(1)) 771 self._section.append(line) 772 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, 777 member.name) 778 self.args[member.name].connect(member) 779 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" 784 % feature.name) 785 self.features[feature.name].connect(feature) 786 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") 791 792 def check(self) -> None: 793 794 def check_args_section( 795 args: Dict[str, QAPIDoc.ArgSection], what: str 796 ) -> None: 797 bogus = [name for name, section in args.items() 798 if not section.member] 799 if bogus: 800 raise QAPISemError( 801 self.info, 802 "documented %s%s '%s' %s not exist" % ( 803 what, 804 "s" if len(bogus) > 1 else "", 805 "', '".join(bogus), 806 "do" if len(bogus) > 1 else "does" 807 )) 808 809 check_args_section(self.args, 'member') 810 check_args_section(self.features, 'feature')