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