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