]> git.proxmox.com Git - mirror_qemu.git/blame - scripts/qapi/parser.py
qapi/parser.py: assert member.info is present in connect_member
[mirror_qemu.git] / scripts / qapi / parser.py
CommitLineData
e6c42b96
MA
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
67fea575 17from collections import OrderedDict
e6c42b96
MA
18import os
19import re
810aff8f 20from typing import (
e7ac60fc 21 TYPE_CHECKING,
7c6e4464 22 Any,
810aff8f
JS
23 Dict,
24 List,
42011059 25 Mapping,
a6c5d159 26 Match,
810aff8f
JS
27 Optional,
28 Set,
29 Union,
30)
e6c42b96 31
e0e8a0ac 32from .common import must_match
ac6a7d88 33from .error import QAPISemError, QAPISourceError
7137a960 34from .source import QAPISourceInfo
e6c42b96
MA
35
36
e7ac60fc
JS
37if TYPE_CHECKING:
38 # pylint: disable=cyclic-import
39 # TODO: Remove cycle. [schema -> expr -> parser -> schema]
40 from .schema import QAPISchemaFeature, QAPISchemaMember
41
42
810aff8f
JS
43# Return value alias for get_expr().
44_ExprValue = Union[List[object], Dict[str, object], str, bool]
45
42011059 46
7c6e4464 47class QAPIExpression(Dict[str, Any]):
42011059
JS
48 # pylint: disable=too-few-public-methods
49 def __init__(self,
50 data: Mapping[str, object],
51 info: QAPISourceInfo,
52 doc: Optional['QAPIDoc'] = None):
53 super().__init__(data)
54 self.info = info
55 self.doc: Optional['QAPIDoc'] = doc
15acf48c 56
810aff8f 57
ac6a7d88
JS
58class QAPIParseError(QAPISourceError):
59 """Error class for all QAPI schema parsing errors."""
810aff8f 60 def __init__(self, parser: 'QAPISchemaParser', msg: str):
ac6a7d88
JS
61 col = 1
62 for ch in parser.src[parser.line_pos:parser.pos]:
63 if ch == '\t':
64 col = (col + 7) % 8 + 1
65 else:
66 col += 1
67 super().__init__(parser.info, msg, col)
68
69
baa310f1 70class QAPISchemaParser:
d4092ffa
JS
71 """
72 Parse QAPI schema source.
e6c42b96 73
d4092ffa 74 Parse a JSON-esque schema file and process directives. See
b0b1313e 75 qapi-code-gen.rst section "Schema Syntax" for the exact syntax.
d4092ffa
JS
76 Grammatical validation is handled later by `expr.check_exprs()`.
77
78 :param fname: Source file name.
79 :param previously_included:
80 The absolute names of previously included source files,
81 if being invoked from another parser.
82 :param incl_info:
83 `QAPISourceInfo` belonging to the parent module.
84 ``None`` implies this is the root module.
85
86 :ivar exprs: Resulting parsed expressions.
87 :ivar docs: Resulting parsed documentation blocks.
88
89 :raise OSError: For problems reading the root schema document.
90 :raise QAPIError: For errors in the schema source.
91 """
810aff8f
JS
92 def __init__(self,
93 fname: str,
94 previously_included: Optional[Set[str]] = None,
95 incl_info: Optional[QAPISourceInfo] = None):
16ff40ac
JS
96 self._fname = fname
97 self._included = previously_included or set()
98 self._included.add(os.path.abspath(self._fname))
99 self.src = ''
e6c42b96 100
16ff40ac
JS
101 # Lexer state (see `accept` for details):
102 self.info = QAPISourceInfo(self._fname, incl_info)
810aff8f 103 self.tok: Union[None, str] = None
16ff40ac 104 self.pos = 0
e6c42b96 105 self.cursor = 0
810aff8f 106 self.val: Optional[Union[bool, str]] = None
e6c42b96 107 self.line_pos = 0
16ff40ac
JS
108
109 # Parser output:
42011059 110 self.exprs: List[QAPIExpression] = []
810aff8f 111 self.docs: List[QAPIDoc] = []
16ff40ac
JS
112
113 # Showtime!
114 self._parse()
115
810aff8f 116 def _parse(self) -> None:
d4092ffa
JS
117 """
118 Parse the QAPI schema document.
119
120 :return: None. Results are stored in ``.exprs`` and ``.docs``.
121 """
e6c42b96
MA
122 cur_doc = None
123
16ff40ac
JS
124 # May raise OSError; allow the caller to handle it.
125 with open(self._fname, 'r', encoding='utf-8') as fp:
126 self.src = fp.read()
127 if self.src == '' or self.src[-1] != '\n':
128 self.src += '\n'
129
130 # Prime the lexer:
131 self.accept()
132
133 # Parse until done:
e6c42b96
MA
134 while self.tok is not None:
135 info = self.info
136 if self.tok == '#':
137 self.reject_expr_doc(cur_doc)
3d035cd2
MA
138 cur_doc = self.get_doc()
139 self.docs.append(cur_doc)
e6c42b96
MA
140 continue
141
9cd0205d
JS
142 expr = self.get_expr()
143 if not isinstance(expr, dict):
144 raise QAPISemError(
145 info, "top-level expression must be an object")
146
e6c42b96
MA
147 if 'include' in expr:
148 self.reject_expr_doc(cur_doc)
149 if len(expr) != 1:
150 raise QAPISemError(info, "invalid 'include' directive")
151 include = expr['include']
152 if not isinstance(include, str):
153 raise QAPISemError(info,
154 "value of 'include' must be a string")
16ff40ac 155 incl_fname = os.path.join(os.path.dirname(self._fname),
e6c42b96 156 include)
42011059 157 self._add_expr(OrderedDict({'include': incl_fname}), info)
e6c42b96 158 exprs_include = self._include(include, info, incl_fname,
16ff40ac 159 self._included)
e6c42b96
MA
160 if exprs_include:
161 self.exprs.extend(exprs_include.exprs)
162 self.docs.extend(exprs_include.docs)
163 elif "pragma" in expr:
164 self.reject_expr_doc(cur_doc)
165 if len(expr) != 1:
166 raise QAPISemError(info, "invalid 'pragma' directive")
167 pragma = expr['pragma']
168 if not isinstance(pragma, dict):
169 raise QAPISemError(
170 info, "value of 'pragma' must be an object")
171 for name, value in pragma.items():
172 self._pragma(name, value, info)
173 else:
42011059
JS
174 if cur_doc and not cur_doc.symbol:
175 raise QAPISemError(
176 cur_doc.info, "definition documentation required")
177 self._add_expr(expr, info, cur_doc)
e6c42b96
MA
178 cur_doc = None
179 self.reject_expr_doc(cur_doc)
180
42011059
JS
181 def _add_expr(self, expr: Mapping[str, object],
182 info: QAPISourceInfo,
183 doc: Optional['QAPIDoc'] = None) -> None:
184 self.exprs.append(QAPIExpression(expr, info, doc))
185
e6c42b96 186 @staticmethod
810aff8f 187 def reject_expr_doc(doc: Optional['QAPIDoc']) -> None:
e6c42b96
MA
188 if doc and doc.symbol:
189 raise QAPISemError(
190 doc.info,
191 "documentation for '%s' is not followed by the definition"
192 % doc.symbol)
193
43b1be65 194 @staticmethod
810aff8f
JS
195 def _include(include: str,
196 info: QAPISourceInfo,
197 incl_fname: str,
198 previously_included: Set[str]
199 ) -> Optional['QAPISchemaParser']:
e6c42b96
MA
200 incl_abs_fname = os.path.abspath(incl_fname)
201 # catch inclusion cycle
810aff8f 202 inf: Optional[QAPISourceInfo] = info
e6c42b96
MA
203 while inf:
204 if incl_abs_fname == os.path.abspath(inf.fname):
205 raise QAPISemError(info, "inclusion loop for %s" % include)
206 inf = inf.parent
207
208 # skip multiple include of the same file
209 if incl_abs_fname in previously_included:
210 return None
211
3404e574
JS
212 try:
213 return QAPISchemaParser(incl_fname, previously_included, info)
214 except OSError as err:
215 raise QAPISemError(
216 info,
217 f"can't read include file '{incl_fname}': {err.strerror}"
218 ) from err
e6c42b96 219
43b1be65 220 @staticmethod
810aff8f 221 def _pragma(name: str, value: object, info: QAPISourceInfo) -> None:
03386200 222
810aff8f 223 def check_list_str(name: str, value: object) -> List[str]:
03386200 224 if (not isinstance(value, list) or
013a3ace 225 any(not isinstance(elt, str) for elt in value)):
03386200
JS
226 raise QAPISemError(
227 info,
228 "pragma %s must be a list of strings" % name)
229 return value
230
231 pragma = info.pragma
4a67bd31 232
e6c42b96
MA
233 if name == 'doc-required':
234 if not isinstance(value, bool):
235 raise QAPISemError(info,
236 "pragma 'doc-required' must be boolean")
03386200 237 pragma.doc_required = value
05ebf841 238 elif name == 'command-name-exceptions':
03386200 239 pragma.command_name_exceptions = check_list_str(name, value)
b86df374 240 elif name == 'command-returns-exceptions':
03386200 241 pragma.command_returns_exceptions = check_list_str(name, value)
0cec5011
MA
242 elif name == 'documentation-exceptions':
243 pragma.documentation_exceptions = check_list_str(name, value)
b86df374 244 elif name == 'member-name-exceptions':
03386200 245 pragma.member_name_exceptions = check_list_str(name, value)
e6c42b96
MA
246 else:
247 raise QAPISemError(info, "unknown pragma '%s'" % name)
248
810aff8f 249 def accept(self, skip_comment: bool = True) -> None:
d4092ffa
JS
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 """
e6c42b96
MA
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
5b5fe0e0 353 match = must_match('[^[\\]{}:,\\s\']+',
e0e8a0ac 354 self.src[self.cursor-1:])
e6c42b96
MA
355 raise QAPIParseError(self, "stray '%s'" % match.group(0))
356
810aff8f
JS
357 def get_members(self) -> Dict[str, object]:
358 expr: Dict[str, object] = OrderedDict()
e6c42b96
MA
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
234dce2c
JS
366 assert isinstance(key, str) # Guaranteed by tok == "'"
367
e6c42b96
MA
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)
9cd0205d 374 expr[key] = self.get_expr()
e6c42b96
MA
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
810aff8f
JS
384 def get_values(self) -> List[object]:
385 expr: List[object] = []
e6c42b96
MA
386 if self.tok == ']':
387 self.accept()
388 return expr
c256263f 389 if self.tok not in tuple("{['tf"):
e6c42b96 390 raise QAPIParseError(
0e92a19b 391 self, "expected '{', '[', ']', string, or boolean")
e6c42b96 392 while True:
9cd0205d 393 expr.append(self.get_expr())
e6c42b96
MA
394 if self.tok == ']':
395 self.accept()
396 return expr
397 if self.tok != ',':
398 raise QAPIParseError(self, "expected ',' or ']'")
399 self.accept()
400
810aff8f
JS
401 def get_expr(self) -> _ExprValue:
402 expr: _ExprValue
e6c42b96
MA
403 if self.tok == '{':
404 self.accept()
405 expr = self.get_members()
406 elif self.tok == '[':
407 self.accept()
408 expr = self.get_values()
c256263f
JS
409 elif self.tok in tuple("'tf"):
410 assert isinstance(self.val, (str, bool))
e6c42b96
MA
411 expr = self.val
412 self.accept()
413 else:
414 raise QAPIParseError(
0e92a19b 415 self, "expected '{', '[', string, or boolean")
e6c42b96
MA
416 return expr
417
3d035cd2
MA
418 def get_doc_line(self) -> Optional[str]:
419 if self.tok != '#':
420 raise QAPIParseError(
421 self, "documentation comment must end with '##'")
422 assert isinstance(self.val, str)
423 if self.val.startswith('##'):
424 # End of doc comment
425 if self.val != '##':
426 raise QAPIParseError(
427 self, "junk after '##' at end of documentation comment")
428 return None
429 if self.val == '#':
430 return ''
431 if self.val[1] != ' ':
432 raise QAPIParseError(self, "missing space after #")
433 return self.val[2:].rstrip()
434
435 @staticmethod
436 def _match_at_name_colon(string: str) -> Optional[Match[str]]:
437 return re.match(r'@([^:]*): *', string)
438
439 def get_doc_indented(self, doc: 'QAPIDoc') -> Optional[str]:
440 self.accept(False)
441 line = self.get_doc_line()
442 while line == '':
443 doc.append_line(line)
444 self.accept(False)
445 line = self.get_doc_line()
446 if line is None:
447 return line
448 indent = must_match(r'\s*', line).end()
449 if not indent:
450 return line
451 doc.append_line(line[indent:])
452 prev_line_blank = False
453 while True:
454 self.accept(False)
455 line = self.get_doc_line()
456 if line is None:
457 return line
458 if self._match_at_name_colon(line):
459 return line
460 cur_indent = must_match(r'\s*', line).end()
461 if line != '' and cur_indent < indent:
462 if prev_line_blank:
463 return line
464 raise QAPIParseError(
465 self,
466 "unexpected de-indent (expected at least %d spaces)" %
467 indent)
468 doc.append_line(line[indent:])
469 prev_line_blank = True
470
471 def get_doc_paragraph(self, doc: 'QAPIDoc') -> Optional[str]:
472 while True:
473 self.accept(False)
474 line = self.get_doc_line()
475 if line is None:
476 return line
477 if line == '':
478 return line
479 doc.append_line(line)
480
481 def get_doc(self) -> 'QAPIDoc':
e6c42b96
MA
482 if self.val != '##':
483 raise QAPIParseError(
484 self, "junk after '##' at start of documentation comment")
3d035cd2 485 info = self.info
e6c42b96 486 self.accept(False)
3d035cd2
MA
487 line = self.get_doc_line()
488 if line is not None and line.startswith('@'):
489 # Definition documentation
490 if not line.endswith(':'):
491 raise QAPIParseError(self, "line should end with ':'")
492 # Invalid names are not checked here, but the name
493 # provided *must* match the following definition,
494 # which *is* validated in expr.py.
495 symbol = line[1:-1]
496 if not symbol:
497 raise QAPIParseError(self, "name required after '@'")
adb0193b 498 doc = QAPIDoc(info, symbol)
3d035cd2
MA
499 self.accept(False)
500 line = self.get_doc_line()
501 no_more_args = False
502
503 while line is not None:
504 # Blank lines
505 while line == '':
506 self.accept(False)
507 line = self.get_doc_line()
508 if line is None:
509 break
510 # Non-blank line, first of a section
629c5075
MA
511 if line == 'Features:':
512 if doc.features:
513 raise QAPIParseError(
514 self, "duplicated 'Features:' line")
3d035cd2
MA
515 self.accept(False)
516 line = self.get_doc_line()
517 while line == '':
518 self.accept(False)
519 line = self.get_doc_line()
520 while (line is not None
521 and (match := self._match_at_name_colon(line))):
adb0193b 522 doc.new_feature(self.info, match.group(1))
3d035cd2
MA
523 text = line[match.end():]
524 if text:
525 doc.append_line(text)
526 line = self.get_doc_indented(doc)
629c5075
MA
527 if not doc.features:
528 raise QAPIParseError(
529 self, 'feature descriptions expected')
3d035cd2
MA
530 no_more_args = True
531 elif match := self._match_at_name_colon(line):
532 # description
533 if no_more_args:
534 raise QAPIParseError(
535 self,
536 "description of '@%s:' follows a section"
537 % match.group(1))
538 while (line is not None
539 and (match := self._match_at_name_colon(line))):
adb0193b 540 doc.new_argument(self.info, match.group(1))
3d035cd2
MA
541 text = line[match.end():]
542 if text:
543 doc.append_line(text)
544 line = self.get_doc_indented(doc)
545 no_more_args = True
546 elif match := re.match(
3a025d3d 547 r'(Returns|Errors|Since|Notes?|Examples?|TODO): *',
3d035cd2
MA
548 line):
549 # tagged section
adb0193b 550 doc.new_tagged_section(self.info, match.group(1))
3d035cd2
MA
551 text = line[match.end():]
552 if text:
553 doc.append_line(text)
554 line = self.get_doc_indented(doc)
555 no_more_args = True
556 elif line.startswith('='):
d98884b7
MA
557 raise QAPIParseError(
558 self,
559 "unexpected '=' markup in definition documentation")
3d035cd2
MA
560 else:
561 # tag-less paragraph
adb0193b 562 doc.ensure_untagged_section(self.info)
3d035cd2
MA
563 doc.append_line(line)
564 line = self.get_doc_paragraph(doc)
565 else:
566 # Free-form documentation
adb0193b
MA
567 doc = QAPIDoc(info)
568 doc.ensure_untagged_section(self.info)
3d035cd2
MA
569 first = True
570 while line is not None:
571 if match := self._match_at_name_colon(line):
56c64dd6
MA
572 raise QAPIParseError(
573 self,
3d035cd2
MA
574 "'@%s:' not allowed in free-form documentation"
575 % match.group(1))
576 if line.startswith('='):
577 if not first:
578 raise QAPIParseError(
579 self,
580 "'=' heading must come first in a comment block")
581 doc.append_line(line)
582 self.accept(False)
583 line = self.get_doc_line()
584 first = False
e6c42b96 585
3d035cd2
MA
586 self.accept(False)
587 doc.end()
588 return doc
e6c42b96
MA
589
590
baa310f1 591class QAPIDoc:
e6c42b96
MA
592 """
593 A documentation comment block, either definition or free-form
594
595 Definition documentation blocks consist of
596
597 * a body section: one line naming the definition, followed by an
598 overview (any number of lines)
599
600 * argument sections: a description of each argument (for commands
601 and events) or member (for structs, unions and alternates)
602
603 * features sections: a description of each feature flag
604
605 * additional (non-argument) sections, possibly tagged
606
607 Free-form documentation blocks consist only of a body section.
608 """
609
baa310f1 610 class Section:
2daf52df 611 # pylint: disable=too-few-public-methods
adb0193b 612 def __init__(self, info: QAPISourceInfo,
31c54b92 613 tag: Optional[str] = None):
15333abe 614 # section source info, i.e. where it begins
adb0193b 615 self.info = info
573e2223 616 # section tag, if any ('Returns', '@name', ...)
31c54b92 617 self.tag = tag
573e2223 618 # section text without tag
e6c42b96 619 self.text = ''
a69a6d4b 620
3d035cd2 621 def append_line(self, line: str) -> None:
08349786 622 self.text += line + '\n'
e6c42b96
MA
623
624 class ArgSection(Section):
adb0193b
MA
625 def __init__(self, info: QAPISourceInfo, tag: str):
626 super().__init__(info, tag)
e7ac60fc 627 self.member: Optional['QAPISchemaMember'] = None
e6c42b96 628
e7ac60fc 629 def connect(self, member: 'QAPISchemaMember') -> None:
e6c42b96
MA
630 self.member = member
631
adb0193b 632 def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
3d035cd2 633 # info points to the doc comment block's first line
e6c42b96 634 self.info = info
3d035cd2
MA
635 # definition doc's symbol, None for free-form doc
636 self.symbol: Optional[str] = symbol
637 # the sections in textual order
adb0193b 638 self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
3d035cd2
MA
639 # the body section
640 self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
641 # dicts mapping parameter/feature names to their description
642 self.args: Dict[str, QAPIDoc.ArgSection] = {}
643 self.features: Dict[str, QAPIDoc.ArgSection] = {}
3a025d3d 644 # a command's "Returns" and "Errors" section
ba7f63f9 645 self.returns: Optional[QAPIDoc.Section] = None
3a025d3d 646 self.errors: Optional[QAPIDoc.Section] = None
ba7f63f9
MA
647 # "Since" section
648 self.since: Optional[QAPIDoc.Section] = None
3d035cd2 649 # sections other than .body, .args, .features
5f0d9f3b 650 self.sections: List[QAPIDoc.Section] = []
3e32dca3 651
3d035cd2
MA
652 def end(self) -> None:
653 for section in self.all_sections:
654 section.text = section.text.strip('\n')
655 if section.tag is not None and section.text == '':
656 raise QAPISemError(
657 section.info, "text required after '%s:'" % section.tag)
e6c42b96 658
adb0193b 659 def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
3d035cd2
MA
660 if self.all_sections and not self.all_sections[-1].tag:
661 # extend current section
662 self.all_sections[-1].text += '\n'
e6c42b96 663 return
3d035cd2 664 # start new section
adb0193b 665 section = self.Section(info)
3d035cd2
MA
666 self.sections.append(section)
667 self.all_sections.append(section)
668
adb0193b 669 def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
adb0193b 670 section = self.Section(info, tag)
ba7f63f9
MA
671 if tag == 'Returns':
672 if self.returns:
673 raise QAPISemError(
674 info, "duplicated '%s' section" % tag)
675 self.returns = section
3a025d3d
MA
676 elif tag == 'Errors':
677 if self.errors:
678 raise QAPISemError(
679 info, "duplicated '%s' section" % tag)
680 self.errors = section
ba7f63f9
MA
681 elif tag == 'Since':
682 if self.since:
683 raise QAPISemError(
684 info, "duplicated '%s' section" % tag)
685 self.since = section
3d035cd2
MA
686 self.sections.append(section)
687 self.all_sections.append(section)
e6c42b96 688
adb0193b 689 def _new_description(self, info: QAPISourceInfo, name: str,
3d035cd2 690 desc: Dict[str, ArgSection]) -> None:
e6c42b96 691 if not name:
adb0193b 692 raise QAPISemError(info, "invalid parameter name")
3d035cd2 693 if name in desc:
adb0193b
MA
694 raise QAPISemError(info, "'%s' parameter name duplicated" % name)
695 section = self.ArgSection(info, '@' + name)
3d035cd2
MA
696 self.all_sections.append(section)
697 desc[name] = section
e6c42b96 698
adb0193b
MA
699 def new_argument(self, info: QAPISourceInfo, name: str) -> None:
700 self._new_description(info, name, self.args)
1e20a775 701
adb0193b
MA
702 def new_feature(self, info: QAPISourceInfo, name: str) -> None:
703 self._new_description(info, name, self.features)
e6c42b96 704
3d035cd2
MA
705 def append_line(self, line: str) -> None:
706 self.all_sections[-1].append_line(line)
e6c42b96 707
e7ac60fc 708 def connect_member(self, member: 'QAPISchemaMember') -> None:
e6c42b96 709 if member.name not in self.args:
d5e2f3d0 710 assert member.info
0cec5011
MA
711 if self.symbol not in member.info.pragma.documentation_exceptions:
712 raise QAPISemError(member.info,
713 "%s '%s' lacks documentation"
714 % (member.role, member.name))
3d035cd2 715 self.args[member.name] = QAPIDoc.ArgSection(
adb0193b 716 self.info, '@' + member.name)
e6c42b96
MA
717 self.args[member.name].connect(member)
718
e7ac60fc 719 def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
e151941d
MA
720 if feature.name not in self.features:
721 raise QAPISemError(feature.info,
722 "feature '%s' lacks documentation"
723 % feature.name)
e151941d
MA
724 self.features[feature.name].connect(feature)
725
42011059 726 def check_expr(self, expr: QAPIExpression) -> None:
e1f684ea
MA
727 if 'command' in expr:
728 if self.returns and 'returns' not in expr:
729 raise QAPISemError(
730 self.returns.info,
731 "'Returns' section, but command doesn't return anything")
732 else:
3a025d3d
MA
733 if self.returns:
734 raise QAPISemError(
735 self.returns.info,
736 "'Returns' section is only valid for commands")
737 if self.errors:
738 raise QAPISemError(
4f8f199f 739 self.errors.info,
3a025d3d 740 "'Errors' section is only valid for commands")
e6c42b96 741
5f0d9f3b 742 def check(self) -> None:
e151941d 743
5f0d9f3b
JS
744 def check_args_section(
745 args: Dict[str, QAPIDoc.ArgSection], what: str
746 ) -> None:
e151941d
MA
747 bogus = [name for name, section in args.items()
748 if not section.member]
749 if bogus:
750 raise QAPISemError(
15333abe 751 args[bogus[0]].info,
012336a1
JS
752 "documented %s%s '%s' %s not exist" % (
753 what,
754 "s" if len(bogus) > 1 else "",
755 "', '".join(bogus),
756 "do" if len(bogus) > 1 else "does"
757 ))
758
759 check_args_section(self.args, 'member')
760 check_args_section(self.features, 'feature')