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