]> git.proxmox.com Git - mirror_qemu.git/blame - scripts/qapi/parser.py
qapi/parser: Remove superfluous list comprehension
[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
JS
20from typing import (
21 Dict,
22 List,
23 Optional,
24 Set,
25 Union,
26)
e6c42b96 27
e0e8a0ac 28from .common import must_match
ac6a7d88 29from .error import QAPISemError, QAPISourceError
7137a960 30from .source import QAPISourceInfo
e6c42b96
MA
31
32
810aff8f
JS
33# Return value alias for get_expr().
34_ExprValue = Union[List[object], Dict[str, object], str, bool]
35
36
ac6a7d88
JS
37class QAPIParseError(QAPISourceError):
38 """Error class for all QAPI schema parsing errors."""
810aff8f 39 def __init__(self, parser: 'QAPISchemaParser', msg: str):
ac6a7d88
JS
40 col = 1
41 for ch in parser.src[parser.line_pos:parser.pos]:
42 if ch == '\t':
43 col = (col + 7) % 8 + 1
44 else:
45 col += 1
46 super().__init__(parser.info, msg, col)
47
48
baa310f1 49class QAPISchemaParser:
e6c42b96 50
810aff8f
JS
51 def __init__(self,
52 fname: str,
53 previously_included: Optional[Set[str]] = None,
54 incl_info: Optional[QAPISourceInfo] = None):
16ff40ac
JS
55 self._fname = fname
56 self._included = previously_included or set()
57 self._included.add(os.path.abspath(self._fname))
58 self.src = ''
e6c42b96 59
16ff40ac
JS
60 # Lexer state (see `accept` for details):
61 self.info = QAPISourceInfo(self._fname, incl_info)
810aff8f 62 self.tok: Union[None, str] = None
16ff40ac 63 self.pos = 0
e6c42b96 64 self.cursor = 0
810aff8f 65 self.val: Optional[Union[bool, str]] = None
e6c42b96 66 self.line_pos = 0
16ff40ac
JS
67
68 # Parser output:
810aff8f
JS
69 self.exprs: List[Dict[str, object]] = []
70 self.docs: List[QAPIDoc] = []
16ff40ac
JS
71
72 # Showtime!
73 self._parse()
74
810aff8f 75 def _parse(self) -> None:
e6c42b96
MA
76 cur_doc = None
77
16ff40ac
JS
78 # May raise OSError; allow the caller to handle it.
79 with open(self._fname, 'r', encoding='utf-8') as fp:
80 self.src = fp.read()
81 if self.src == '' or self.src[-1] != '\n':
82 self.src += '\n'
83
84 # Prime the lexer:
85 self.accept()
86
87 # Parse until done:
e6c42b96
MA
88 while self.tok is not None:
89 info = self.info
90 if self.tok == '#':
91 self.reject_expr_doc(cur_doc)
dcdc07a9
MA
92 for cur_doc in self.get_doc(info):
93 self.docs.append(cur_doc)
e6c42b96
MA
94 continue
95
9cd0205d
JS
96 expr = self.get_expr()
97 if not isinstance(expr, dict):
98 raise QAPISemError(
99 info, "top-level expression must be an object")
100
e6c42b96
MA
101 if 'include' in expr:
102 self.reject_expr_doc(cur_doc)
103 if len(expr) != 1:
104 raise QAPISemError(info, "invalid 'include' directive")
105 include = expr['include']
106 if not isinstance(include, str):
107 raise QAPISemError(info,
108 "value of 'include' must be a string")
16ff40ac 109 incl_fname = os.path.join(os.path.dirname(self._fname),
e6c42b96
MA
110 include)
111 self.exprs.append({'expr': {'include': incl_fname},
112 'info': info})
113 exprs_include = self._include(include, info, incl_fname,
16ff40ac 114 self._included)
e6c42b96
MA
115 if exprs_include:
116 self.exprs.extend(exprs_include.exprs)
117 self.docs.extend(exprs_include.docs)
118 elif "pragma" in expr:
119 self.reject_expr_doc(cur_doc)
120 if len(expr) != 1:
121 raise QAPISemError(info, "invalid 'pragma' directive")
122 pragma = expr['pragma']
123 if not isinstance(pragma, dict):
124 raise QAPISemError(
125 info, "value of 'pragma' must be an object")
126 for name, value in pragma.items():
127 self._pragma(name, value, info)
128 else:
129 expr_elem = {'expr': expr,
130 'info': info}
131 if cur_doc:
132 if not cur_doc.symbol:
133 raise QAPISemError(
134 cur_doc.info, "definition documentation required")
135 expr_elem['doc'] = cur_doc
136 self.exprs.append(expr_elem)
137 cur_doc = None
138 self.reject_expr_doc(cur_doc)
139
140 @staticmethod
810aff8f 141 def reject_expr_doc(doc: Optional['QAPIDoc']) -> None:
e6c42b96
MA
142 if doc and doc.symbol:
143 raise QAPISemError(
144 doc.info,
145 "documentation for '%s' is not followed by the definition"
146 % doc.symbol)
147
43b1be65 148 @staticmethod
810aff8f
JS
149 def _include(include: str,
150 info: QAPISourceInfo,
151 incl_fname: str,
152 previously_included: Set[str]
153 ) -> Optional['QAPISchemaParser']:
e6c42b96
MA
154 incl_abs_fname = os.path.abspath(incl_fname)
155 # catch inclusion cycle
810aff8f 156 inf: Optional[QAPISourceInfo] = info
e6c42b96
MA
157 while inf:
158 if incl_abs_fname == os.path.abspath(inf.fname):
159 raise QAPISemError(info, "inclusion loop for %s" % include)
160 inf = inf.parent
161
162 # skip multiple include of the same file
163 if incl_abs_fname in previously_included:
164 return None
165
3404e574
JS
166 try:
167 return QAPISchemaParser(incl_fname, previously_included, info)
168 except OSError as err:
169 raise QAPISemError(
170 info,
171 f"can't read include file '{incl_fname}': {err.strerror}"
172 ) from err
e6c42b96 173
43b1be65 174 @staticmethod
810aff8f 175 def _pragma(name: str, value: object, info: QAPISourceInfo) -> None:
03386200 176
810aff8f 177 def check_list_str(name: str, value: object) -> List[str]:
03386200 178 if (not isinstance(value, list) or
013a3ace 179 any(not isinstance(elt, str) for elt in value)):
03386200
JS
180 raise QAPISemError(
181 info,
182 "pragma %s must be a list of strings" % name)
183 return value
184
185 pragma = info.pragma
4a67bd31 186
e6c42b96
MA
187 if name == 'doc-required':
188 if not isinstance(value, bool):
189 raise QAPISemError(info,
190 "pragma 'doc-required' must be boolean")
03386200 191 pragma.doc_required = value
05ebf841 192 elif name == 'command-name-exceptions':
03386200 193 pragma.command_name_exceptions = check_list_str(name, value)
b86df374 194 elif name == 'command-returns-exceptions':
03386200 195 pragma.command_returns_exceptions = check_list_str(name, value)
b86df374 196 elif name == 'member-name-exceptions':
03386200 197 pragma.member_name_exceptions = check_list_str(name, value)
e6c42b96
MA
198 else:
199 raise QAPISemError(info, "unknown pragma '%s'" % name)
200
810aff8f 201 def accept(self, skip_comment: bool = True) -> None:
e6c42b96
MA
202 while True:
203 self.tok = self.src[self.cursor]
204 self.pos = self.cursor
205 self.cursor += 1
206 self.val = None
207
208 if self.tok == '#':
209 if self.src[self.cursor] == '#':
210 # Start of doc comment
211 skip_comment = False
212 self.cursor = self.src.find('\n', self.cursor)
213 if not skip_comment:
214 self.val = self.src[self.pos:self.cursor]
215 return
216 elif self.tok in '{}:,[]':
217 return
218 elif self.tok == "'":
219 # Note: we accept only printable ASCII
220 string = ''
221 esc = False
222 while True:
223 ch = self.src[self.cursor]
224 self.cursor += 1
225 if ch == '\n':
226 raise QAPIParseError(self, "missing terminating \"'\"")
227 if esc:
228 # Note: we recognize only \\ because we have
229 # no use for funny characters in strings
230 if ch != '\\':
231 raise QAPIParseError(self,
232 "unknown escape \\%s" % ch)
233 esc = False
234 elif ch == '\\':
235 esc = True
236 continue
237 elif ch == "'":
238 self.val = string
239 return
240 if ord(ch) < 32 or ord(ch) >= 127:
241 raise QAPIParseError(
242 self, "funny character in string")
243 string += ch
244 elif self.src.startswith('true', self.pos):
245 self.val = True
246 self.cursor += 3
247 return
248 elif self.src.startswith('false', self.pos):
249 self.val = False
250 self.cursor += 4
251 return
252 elif self.tok == '\n':
253 if self.cursor == len(self.src):
254 self.tok = None
255 return
256 self.info = self.info.next_line()
257 self.line_pos = self.cursor
258 elif not self.tok.isspace():
259 # Show up to next structural, whitespace or quote
260 # character
e0e8a0ac
JS
261 match = must_match('[^[\\]{}:,\\s\'"]+',
262 self.src[self.cursor-1:])
e6c42b96
MA
263 raise QAPIParseError(self, "stray '%s'" % match.group(0))
264
810aff8f
JS
265 def get_members(self) -> Dict[str, object]:
266 expr: Dict[str, object] = OrderedDict()
e6c42b96
MA
267 if self.tok == '}':
268 self.accept()
269 return expr
270 if self.tok != "'":
271 raise QAPIParseError(self, "expected string or '}'")
272 while True:
273 key = self.val
234dce2c
JS
274 assert isinstance(key, str) # Guaranteed by tok == "'"
275
e6c42b96
MA
276 self.accept()
277 if self.tok != ':':
278 raise QAPIParseError(self, "expected ':'")
279 self.accept()
280 if key in expr:
281 raise QAPIParseError(self, "duplicate key '%s'" % key)
9cd0205d 282 expr[key] = self.get_expr()
e6c42b96
MA
283 if self.tok == '}':
284 self.accept()
285 return expr
286 if self.tok != ',':
287 raise QAPIParseError(self, "expected ',' or '}'")
288 self.accept()
289 if self.tok != "'":
290 raise QAPIParseError(self, "expected string")
291
810aff8f
JS
292 def get_values(self) -> List[object]:
293 expr: List[object] = []
e6c42b96
MA
294 if self.tok == ']':
295 self.accept()
296 return expr
c256263f 297 if self.tok not in tuple("{['tf"):
e6c42b96 298 raise QAPIParseError(
0e92a19b 299 self, "expected '{', '[', ']', string, or boolean")
e6c42b96 300 while True:
9cd0205d 301 expr.append(self.get_expr())
e6c42b96
MA
302 if self.tok == ']':
303 self.accept()
304 return expr
305 if self.tok != ',':
306 raise QAPIParseError(self, "expected ',' or ']'")
307 self.accept()
308
810aff8f
JS
309 def get_expr(self) -> _ExprValue:
310 expr: _ExprValue
e6c42b96
MA
311 if self.tok == '{':
312 self.accept()
313 expr = self.get_members()
314 elif self.tok == '[':
315 self.accept()
316 expr = self.get_values()
c256263f
JS
317 elif self.tok in tuple("'tf"):
318 assert isinstance(self.val, (str, bool))
e6c42b96
MA
319 expr = self.val
320 self.accept()
321 else:
322 raise QAPIParseError(
0e92a19b 323 self, "expected '{', '[', string, or boolean")
e6c42b96
MA
324 return expr
325
810aff8f 326 def get_doc(self, info: QAPISourceInfo) -> List['QAPIDoc']:
e6c42b96
MA
327 if self.val != '##':
328 raise QAPIParseError(
329 self, "junk after '##' at start of documentation comment")
330
dcdc07a9
MA
331 docs = []
332 cur_doc = QAPIDoc(self, info)
e6c42b96
MA
333 self.accept(False)
334 while self.tok == '#':
7c610ce6 335 assert isinstance(self.val, str)
e6c42b96
MA
336 if self.val.startswith('##'):
337 # End of doc comment
338 if self.val != '##':
339 raise QAPIParseError(
340 self,
341 "junk after '##' at end of documentation comment")
dcdc07a9
MA
342 cur_doc.end_comment()
343 docs.append(cur_doc)
e6c42b96 344 self.accept()
dcdc07a9 345 return docs
d98884b7 346 if self.val.startswith('# ='):
dcdc07a9 347 if cur_doc.symbol:
d98884b7
MA
348 raise QAPIParseError(
349 self,
350 "unexpected '=' markup in definition documentation")
dcdc07a9
MA
351 if cur_doc.body.text:
352 cur_doc.end_comment()
353 docs.append(cur_doc)
354 cur_doc = QAPIDoc(self, info)
355 cur_doc.append(self.val)
e6c42b96
MA
356 self.accept(False)
357
358 raise QAPIParseError(self, "documentation comment must end with '##'")
359
360
baa310f1 361class QAPIDoc:
e6c42b96
MA
362 """
363 A documentation comment block, either definition or free-form
364
365 Definition documentation blocks consist of
366
367 * a body section: one line naming the definition, followed by an
368 overview (any number of lines)
369
370 * argument sections: a description of each argument (for commands
371 and events) or member (for structs, unions and alternates)
372
373 * features sections: a description of each feature flag
374
375 * additional (non-argument) sections, possibly tagged
376
377 Free-form documentation blocks consist only of a body section.
378 """
379
baa310f1 380 class Section:
a69a6d4b
PM
381 def __init__(self, parser, name=None, indent=0):
382 # parser, for error messages about indentation
383 self._parser = parser
e6c42b96
MA
384 # optional section name (argument/member or section name)
385 self.name = name
e6c42b96 386 self.text = ''
a69a6d4b
PM
387 # the expected indent level of the text of this section
388 self._indent = indent
e6c42b96
MA
389
390 def append(self, line):
a69a6d4b
PM
391 # Strip leading spaces corresponding to the expected indent level
392 # Blank lines are always OK.
393 if line:
e0e8a0ac 394 indent = must_match(r'\s*', line).end()
a69a6d4b
PM
395 if indent < self._indent:
396 raise QAPIParseError(
397 self._parser,
398 "unexpected de-indent (expected at least %d spaces)" %
399 self._indent)
400 line = line[self._indent:]
401
e6c42b96
MA
402 self.text += line.rstrip() + '\n'
403
404 class ArgSection(Section):
a69a6d4b
PM
405 def __init__(self, parser, name, indent=0):
406 super().__init__(parser, name, indent)
e6c42b96
MA
407 self.member = None
408
409 def connect(self, member):
410 self.member = member
411
412 def __init__(self, parser, info):
413 # self._parser is used to report errors with QAPIParseError. The
414 # resulting error position depends on the state of the parser.
415 # It happens to be the beginning of the comment. More or less
416 # servicable, but action at a distance.
417 self._parser = parser
418 self.info = info
419 self.symbol = None
a69a6d4b 420 self.body = QAPIDoc.Section(parser)
e6c42b96
MA
421 # dict mapping parameter name to ArgSection
422 self.args = OrderedDict()
423 self.features = OrderedDict()
424 # a list of Section
425 self.sections = []
426 # the current section
427 self._section = self.body
428 self._append_line = self._append_body_line
429
430 def has_section(self, name):
431 """Return True if we have a section with this name."""
432 for i in self.sections:
433 if i.name == name:
434 return True
435 return False
436
437 def append(self, line):
438 """
439 Parse a comment line and add it to the documentation.
440
441 The way that the line is dealt with depends on which part of
442 the documentation we're parsing right now:
443 * The body section: ._append_line is ._append_body_line
444 * An argument section: ._append_line is ._append_args_line
445 * A features section: ._append_line is ._append_features_line
446 * An additional section: ._append_line is ._append_various_line
447 """
448 line = line[1:]
449 if not line:
450 self._append_freeform(line)
451 return
452
453 if line[0] != ' ':
454 raise QAPIParseError(self._parser, "missing space after #")
455 line = line[1:]
456 self._append_line(line)
457
458 def end_comment(self):
459 self._end_section()
460
461 @staticmethod
462 def _is_section_tag(name):
463 return name in ('Returns:', 'Since:',
464 # those are often singular or plural
465 'Note:', 'Notes:',
466 'Example:', 'Examples:',
467 'TODO:')
468
469 def _append_body_line(self, line):
470 """
471 Process a line of documentation text in the body section.
472
473 If this a symbol line and it is the section's first line, this
474 is a definition documentation block for that symbol.
475
476 If it's a definition documentation block, another symbol line
477 begins the argument section for the argument named by it, and
478 a section tag begins an additional section. Start that
479 section and append the line to it.
480
481 Else, append the line to the current section.
482 """
483 name = line.split(' ', 1)[0]
484 # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't
485 # recognized, and get silently treated as ordinary text
486 if not self.symbol and not self.body.text and line.startswith('@'):
487 if not line.endswith(':'):
488 raise QAPIParseError(self._parser, "line should end with ':'")
489 self.symbol = line[1:-1]
490 # FIXME invalid names other than the empty string aren't flagged
491 if not self.symbol:
492 raise QAPIParseError(self._parser, "invalid name")
493 elif self.symbol:
494 # This is a definition documentation block
495 if name.startswith('@') and name.endswith(':'):
496 self._append_line = self._append_args_line
497 self._append_args_line(line)
498 elif line == 'Features:':
499 self._append_line = self._append_features_line
500 elif self._is_section_tag(name):
501 self._append_line = self._append_various_line
502 self._append_various_line(line)
503 else:
99dff36d 504 self._append_freeform(line)
e6c42b96
MA
505 else:
506 # This is a free-form documentation block
99dff36d 507 self._append_freeform(line)
e6c42b96
MA
508
509 def _append_args_line(self, line):
510 """
511 Process a line of documentation text in an argument section.
512
513 A symbol line begins the next argument section, a section tag
514 section or a non-indented line after a blank line begins an
515 additional section. Start that section and append the line to
516 it.
517
518 Else, append the line to the current section.
519
520 """
521 name = line.split(' ', 1)[0]
522
523 if name.startswith('@') and name.endswith(':'):
a69a6d4b
PM
524 # If line is "@arg: first line of description", find
525 # the index of 'f', which is the indent we expect for any
526 # following lines. We then remove the leading "@arg:"
527 # from line and replace it with spaces so that 'f' has the
528 # same index as it did in the original line and can be
529 # handled the same way we will handle following lines.
e0e8a0ac 530 indent = must_match(r'@\S*:\s*', line).end()
a69a6d4b
PM
531 line = line[indent:]
532 if not line:
533 # Line was just the "@arg:" header; following lines
534 # are not indented
535 indent = 0
536 else:
537 line = ' ' * indent + line
538 self._start_args_section(name[1:-1], indent)
e6c42b96
MA
539 elif self._is_section_tag(name):
540 self._append_line = self._append_various_line
541 self._append_various_line(line)
542 return
543 elif (self._section.text.endswith('\n\n')
544 and line and not line[0].isspace()):
545 if line == 'Features:':
546 self._append_line = self._append_features_line
547 else:
548 self._start_section()
549 self._append_line = self._append_various_line
550 self._append_various_line(line)
551 return
552
99dff36d 553 self._append_freeform(line)
e6c42b96
MA
554
555 def _append_features_line(self, line):
556 name = line.split(' ', 1)[0]
557
558 if name.startswith('@') and name.endswith(':'):
a69a6d4b
PM
559 # If line is "@arg: first line of description", find
560 # the index of 'f', which is the indent we expect for any
561 # following lines. We then remove the leading "@arg:"
562 # from line and replace it with spaces so that 'f' has the
563 # same index as it did in the original line and can be
564 # handled the same way we will handle following lines.
e0e8a0ac 565 indent = must_match(r'@\S*:\s*', line).end()
a69a6d4b
PM
566 line = line[indent:]
567 if not line:
568 # Line was just the "@arg:" header; following lines
569 # are not indented
570 indent = 0
571 else:
572 line = ' ' * indent + line
573 self._start_features_section(name[1:-1], indent)
e6c42b96
MA
574 elif self._is_section_tag(name):
575 self._append_line = self._append_various_line
576 self._append_various_line(line)
577 return
578 elif (self._section.text.endswith('\n\n')
579 and line and not line[0].isspace()):
580 self._start_section()
581 self._append_line = self._append_various_line
582 self._append_various_line(line)
583 return
584
99dff36d 585 self._append_freeform(line)
e6c42b96
MA
586
587 def _append_various_line(self, line):
588 """
589 Process a line of documentation text in an additional section.
590
591 A symbol line is an error.
592
593 A section tag begins an additional section. Start that
594 section and append the line to it.
595
596 Else, append the line to the current section.
597 """
598 name = line.split(' ', 1)[0]
599
600 if name.startswith('@') and name.endswith(':'):
601 raise QAPIParseError(self._parser,
602 "'%s' can't follow '%s' section"
603 % (name, self.sections[0].name))
8ec0e1a4 604 if self._is_section_tag(name):
a69a6d4b
PM
605 # If line is "Section: first line of description", find
606 # the index of 'f', which is the indent we expect for any
607 # following lines. We then remove the leading "Section:"
608 # from line and replace it with spaces so that 'f' has the
609 # same index as it did in the original line and can be
610 # handled the same way we will handle following lines.
e0e8a0ac 611 indent = must_match(r'\S*:\s*', line).end()
a69a6d4b
PM
612 line = line[indent:]
613 if not line:
614 # Line was just the "Section:" header; following lines
615 # are not indented
616 indent = 0
617 else:
618 line = ' ' * indent + line
619 self._start_section(name[:-1], indent)
e6c42b96 620
e6c42b96
MA
621 self._append_freeform(line)
622
a69a6d4b 623 def _start_symbol_section(self, symbols_dict, name, indent):
e6c42b96
MA
624 # FIXME invalid names other than the empty string aren't flagged
625 if not name:
626 raise QAPIParseError(self._parser, "invalid parameter name")
627 if name in symbols_dict:
628 raise QAPIParseError(self._parser,
629 "'%s' parameter name duplicated" % name)
630 assert not self.sections
631 self._end_section()
a69a6d4b 632 self._section = QAPIDoc.ArgSection(self._parser, name, indent)
e6c42b96
MA
633 symbols_dict[name] = self._section
634
a69a6d4b
PM
635 def _start_args_section(self, name, indent):
636 self._start_symbol_section(self.args, name, indent)
e6c42b96 637
a69a6d4b
PM
638 def _start_features_section(self, name, indent):
639 self._start_symbol_section(self.features, name, indent)
e6c42b96 640
a69a6d4b 641 def _start_section(self, name=None, indent=0):
e6c42b96
MA
642 if name in ('Returns', 'Since') and self.has_section(name):
643 raise QAPIParseError(self._parser,
644 "duplicated '%s' section" % name)
645 self._end_section()
a69a6d4b 646 self._section = QAPIDoc.Section(self._parser, name, indent)
e6c42b96
MA
647 self.sections.append(self._section)
648
649 def _end_section(self):
650 if self._section:
651 text = self._section.text = self._section.text.strip()
652 if self._section.name and (not text or text.isspace()):
653 raise QAPIParseError(
654 self._parser,
655 "empty doc section '%s'" % self._section.name)
656 self._section = None
657
658 def _append_freeform(self, line):
659 match = re.match(r'(@\S+:)', line)
660 if match:
661 raise QAPIParseError(self._parser,
662 "'%s' not allowed in free-form documentation"
663 % match.group(1))
664 self._section.append(line)
665
666 def connect_member(self, member):
667 if member.name not in self.args:
668 # Undocumented TODO outlaw
a69a6d4b
PM
669 self.args[member.name] = QAPIDoc.ArgSection(self._parser,
670 member.name)
e6c42b96
MA
671 self.args[member.name].connect(member)
672
e151941d
MA
673 def connect_feature(self, feature):
674 if feature.name not in self.features:
675 raise QAPISemError(feature.info,
676 "feature '%s' lacks documentation"
677 % feature.name)
e151941d
MA
678 self.features[feature.name].connect(feature)
679
e6c42b96
MA
680 def check_expr(self, expr):
681 if self.has_section('Returns') and 'command' not in expr:
682 raise QAPISemError(self.info,
683 "'Returns:' is only valid for commands")
684
685 def check(self):
e151941d
MA
686
687 def check_args_section(args, info, what):
688 bogus = [name for name, section in args.items()
689 if not section.member]
690 if bogus:
691 raise QAPISemError(
692 self.info,
693 "documented member%s '%s' %s not exist"
694 % ("s" if len(bogus) > 1 else "",
695 "', '".join(bogus),
696 "do" if len(bogus) > 1 else "does"))
697
698 check_args_section(self.args, self.info, 'members')
699 check_args_section(self.features, self.info, 'features')