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