xref: /openbmc/qemu/scripts/qapi/expr.py (revision b9ad358aa057e83f8039a1e222d6941d2bf1f70a)
1# -*- coding: utf-8 -*-
2#
3# Check (context-free) QAPI schema expression structure
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#  Eric Blake <eblake@redhat.com>
12#  Marc-André Lureau <marcandre.lureau@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
17import re
18from typing import (
19    Collection,
20    Dict,
21    Iterable,
22    List,
23    Optional,
24    Union,
25    cast,
26)
27
28from .common import c_name
29from .error import QAPISemError
30from .parser import QAPIDoc
31from .source import QAPISourceInfo
32
33
34# Deserialized JSON objects as returned by the parser.
35# The values of this mapping are not necessary to exhaustively type
36# here (and also not practical as long as mypy lacks recursive
37# types), because the purpose of this module is to interrogate that
38# type.
39_JSONObject = Dict[str, object]
40
41
42# Names consist of letters, digits, -, and _, starting with a letter.
43# An experimental name is prefixed with x-.  A name of a downstream
44# extension is prefixed with __RFQDN_.  The latter prefix goes first.
45valid_name = re.compile(r'(__[a-z0-9.-]+_)?'
46                        r'(x-)?'
47                        r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
48
49
50def check_name_is_str(name: object,
51                      info: QAPISourceInfo,
52                      source: str) -> None:
53    if not isinstance(name, str):
54        raise QAPISemError(info, "%s requires a string name" % source)
55
56
57def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
58    # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
59    # and 'q_obj_*' implicit type names.
60    match = valid_name.match(name)
61    if not match or c_name(name, False).startswith('q_'):
62        raise QAPISemError(info, "%s has an invalid name" % source)
63    return match.group(3)
64
65
66def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
67    stem = check_name_str(name, info, source)
68    if re.search(r'[a-z-]', stem):
69        raise QAPISemError(
70            info, "name of %s must not use lowercase or '-'" % source)
71
72
73def check_name_lower(name: str, info: QAPISourceInfo, source: str,
74                     permit_upper: bool = False,
75                     permit_underscore: bool = False) -> None:
76    stem = check_name_str(name, info, source)
77    if ((not permit_upper and re.search(r'[A-Z]', stem))
78            or (not permit_underscore and '_' in stem)):
79        raise QAPISemError(
80            info, "name of %s must not use uppercase or '_'" % source)
81
82
83def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
84    stem = check_name_str(name, info, source)
85    if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
86        raise QAPISemError(info, "name of %s must use CamelCase" % source)
87
88
89def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
90    if meta == 'event':
91        check_name_upper(name, info, meta)
92    elif meta == 'command':
93        check_name_lower(
94            name, info, meta,
95            permit_underscore=name in info.pragma.command_name_exceptions)
96    else:
97        check_name_camel(name, info, meta)
98    if name.endswith('Kind') or name.endswith('List'):
99        raise QAPISemError(
100            info, "%s name should not end in '%s'" % (meta, name[-4:]))
101
102
103def check_keys(value: _JSONObject,
104               info: QAPISourceInfo,
105               source: str,
106               required: Collection[str],
107               optional: Collection[str]) -> None:
108
109    def pprint(elems: Iterable[str]) -> str:
110        return ', '.join("'" + e + "'" for e in sorted(elems))
111
112    missing = set(required) - set(value)
113    if missing:
114        raise QAPISemError(
115            info,
116            "%s misses key%s %s"
117            % (source, 's' if len(missing) > 1 else '',
118               pprint(missing)))
119    allowed = set(required) | set(optional)
120    unknown = set(value) - allowed
121    if unknown:
122        raise QAPISemError(
123            info,
124            "%s has unknown key%s %s\nValid keys are %s."
125            % (source, 's' if len(unknown) > 1 else '',
126               pprint(unknown), pprint(allowed)))
127
128
129def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
130    for key in ['gen', 'success-response']:
131        if key in expr and expr[key] is not False:
132            raise QAPISemError(
133                info, "flag '%s' may only use false value" % key)
134    for key in ['boxed', 'allow-oob', 'allow-preconfig', 'coroutine']:
135        if key in expr and expr[key] is not True:
136            raise QAPISemError(
137                info, "flag '%s' may only use true value" % key)
138    if 'allow-oob' in expr and 'coroutine' in expr:
139        # This is not necessarily a fundamental incompatibility, but
140        # we don't have a use case and the desired semantics isn't
141        # obvious.  The simplest solution is to forbid it until we get
142        # a use case for it.
143        raise QAPISemError(info, "flags 'allow-oob' and 'coroutine' "
144                                 "are incompatible")
145
146
147def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
148
149    def check_if_str(ifcond: object) -> None:
150        if not isinstance(ifcond, str):
151            raise QAPISemError(
152                info,
153                "'if' condition of %s must be a string or a list of strings"
154                % source)
155        if ifcond.strip() == '':
156            raise QAPISemError(
157                info,
158                "'if' condition '%s' of %s makes no sense"
159                % (ifcond, source))
160
161    ifcond = expr.get('if')
162    if ifcond is None:
163        return
164    if isinstance(ifcond, list):
165        if ifcond == []:
166            raise QAPISemError(
167                info, "'if' condition [] of %s is useless" % source)
168        for elt in ifcond:
169            check_if_str(elt)
170    else:
171        check_if_str(ifcond)
172        expr['if'] = [ifcond]
173
174
175def normalize_members(members: object) -> None:
176    if isinstance(members, dict):
177        for key, arg in members.items():
178            if isinstance(arg, dict):
179                continue
180            members[key] = {'type': arg}
181
182
183def check_type(value: Optional[object],
184               info: QAPISourceInfo,
185               source: str,
186               allow_array: bool = False,
187               allow_dict: Union[bool, str] = False) -> None:
188    if value is None:
189        return
190
191    # Type name
192    if isinstance(value, str):
193        return
194
195    # Array type
196    if isinstance(value, list):
197        if not allow_array:
198            raise QAPISemError(info, "%s cannot be an array" % source)
199        if len(value) != 1 or not isinstance(value[0], str):
200            raise QAPISemError(info,
201                               "%s: array type must contain single type name" %
202                               source)
203        return
204
205    # Anonymous type
206
207    if not allow_dict:
208        raise QAPISemError(info, "%s should be a type name" % source)
209
210    if not isinstance(value, dict):
211        raise QAPISemError(info,
212                           "%s should be an object or type name" % source)
213
214    permissive = False
215    if isinstance(allow_dict, str):
216        permissive = allow_dict in info.pragma.member_name_exceptions
217
218    # value is a dictionary, check that each member is okay
219    for (key, arg) in value.items():
220        key_source = "%s member '%s'" % (source, key)
221        if key.startswith('*'):
222            key = key[1:]
223        check_name_lower(key, info, key_source,
224                         permit_upper=permissive,
225                         permit_underscore=permissive)
226        if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'):
227            raise QAPISemError(info, "%s uses reserved name" % key_source)
228        check_keys(arg, info, key_source, ['type'], ['if', 'features'])
229        check_if(arg, info, key_source)
230        check_features(arg.get('features'), info)
231        check_type(arg['type'], info, key_source, allow_array=True)
232
233
234def check_features(features: Optional[object],
235                   info: QAPISourceInfo) -> None:
236    if features is None:
237        return
238    if not isinstance(features, list):
239        raise QAPISemError(info, "'features' must be an array")
240    features[:] = [f if isinstance(f, dict) else {'name': f}
241                   for f in features]
242    for f in features:
243        source = "'features' member"
244        assert isinstance(f, dict)
245        check_keys(f, info, source, ['name'], ['if'])
246        check_name_is_str(f['name'], info, source)
247        source = "%s '%s'" % (source, f['name'])
248        check_name_lower(f['name'], info, source)
249        check_if(f, info, source)
250
251
252def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
253    name = expr['enum']
254    members = expr['data']
255    prefix = expr.get('prefix')
256
257    if not isinstance(members, list):
258        raise QAPISemError(info, "'data' must be an array")
259    if prefix is not None and not isinstance(prefix, str):
260        raise QAPISemError(info, "'prefix' must be a string")
261
262    permissive = name in info.pragma.member_name_exceptions
263
264    members[:] = [m if isinstance(m, dict) else {'name': m}
265                  for m in members]
266    for member in members:
267        source = "'data' member"
268        member_name = member['name']
269        check_keys(member, info, source, ['name'], ['if'])
270        check_name_is_str(member_name, info, source)
271        source = "%s '%s'" % (source, member_name)
272        # Enum members may start with a digit
273        if member_name[0].isdigit():
274            member_name = 'd' + member_name  # Hack: hide the digit
275        check_name_lower(member_name, info, source,
276                         permit_upper=permissive,
277                         permit_underscore=permissive)
278        check_if(member, info, source)
279
280
281def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
282    name = cast(str, expr['struct'])  # Checked in check_exprs
283    members = expr['data']
284
285    check_type(members, info, "'data'", allow_dict=name)
286    check_type(expr.get('base'), info, "'base'")
287
288
289def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
290    name = cast(str, expr['union'])  # Checked in check_exprs
291    base = expr.get('base')
292    discriminator = expr.get('discriminator')
293    members = expr['data']
294
295    if discriminator is None:   # simple union
296        if base is not None:
297            raise QAPISemError(info, "'base' requires 'discriminator'")
298    else:                       # flat union
299        check_type(base, info, "'base'", allow_dict=name)
300        if not base:
301            raise QAPISemError(info, "'discriminator' requires 'base'")
302        check_name_is_str(discriminator, info, "'discriminator'")
303
304    if not isinstance(members, dict):
305        raise QAPISemError(info, "'data' must be an object")
306
307    for (key, value) in members.items():
308        source = "'data' member '%s'" % key
309        if discriminator is None:
310            check_name_lower(key, info, source)
311        # else: name is in discriminator enum, which gets checked
312        check_keys(value, info, source, ['type'], ['if'])
313        check_if(value, info, source)
314        check_type(value['type'], info, source, allow_array=not base)
315
316
317def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
318    members = expr['data']
319
320    if not members:
321        raise QAPISemError(info, "'data' must not be empty")
322
323    if not isinstance(members, dict):
324        raise QAPISemError(info, "'data' must be an object")
325
326    for (key, value) in members.items():
327        source = "'data' member '%s'" % key
328        check_name_lower(key, info, source)
329        check_keys(value, info, source, ['type'], ['if'])
330        check_if(value, info, source)
331        check_type(value['type'], info, source)
332
333
334def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
335    args = expr.get('data')
336    rets = expr.get('returns')
337    boxed = expr.get('boxed', False)
338
339    if boxed and args is None:
340        raise QAPISemError(info, "'boxed': true requires 'data'")
341    check_type(args, info, "'data'", allow_dict=not boxed)
342    check_type(rets, info, "'returns'", allow_array=True)
343
344
345def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
346    args = expr.get('data')
347    boxed = expr.get('boxed', False)
348
349    if boxed and args is None:
350        raise QAPISemError(info, "'boxed': true requires 'data'")
351    check_type(args, info, "'data'", allow_dict=not boxed)
352
353
354def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
355    for expr_elem in exprs:
356        # Expression
357        assert isinstance(expr_elem['expr'], dict)
358        for key in expr_elem['expr'].keys():
359            assert isinstance(key, str)
360        expr: _JSONObject = expr_elem['expr']
361
362        # QAPISourceInfo
363        assert isinstance(expr_elem['info'], QAPISourceInfo)
364        info: QAPISourceInfo = expr_elem['info']
365
366        # Optional[QAPIDoc]
367        tmp = expr_elem.get('doc')
368        assert tmp is None or isinstance(tmp, QAPIDoc)
369        doc: Optional[QAPIDoc] = tmp
370
371        if 'include' in expr:
372            continue
373
374        if 'enum' in expr:
375            meta = 'enum'
376        elif 'union' in expr:
377            meta = 'union'
378        elif 'alternate' in expr:
379            meta = 'alternate'
380        elif 'struct' in expr:
381            meta = 'struct'
382        elif 'command' in expr:
383            meta = 'command'
384        elif 'event' in expr:
385            meta = 'event'
386        else:
387            raise QAPISemError(info, "expression is missing metatype")
388
389        check_name_is_str(expr[meta], info, "'%s'" % meta)
390        name = cast(str, expr[meta])
391        info.set_defn(meta, name)
392        check_defn_name_str(name, info, meta)
393
394        if doc:
395            if doc.symbol != name:
396                raise QAPISemError(
397                    info, "documentation comment is for '%s'" % doc.symbol)
398            doc.check_expr(expr)
399        elif info.pragma.doc_required:
400            raise QAPISemError(info,
401                               "documentation comment required")
402
403        if meta == 'enum':
404            check_keys(expr, info, meta,
405                       ['enum', 'data'], ['if', 'features', 'prefix'])
406            check_enum(expr, info)
407        elif meta == 'union':
408            check_keys(expr, info, meta,
409                       ['union', 'data'],
410                       ['base', 'discriminator', 'if', 'features'])
411            normalize_members(expr.get('base'))
412            normalize_members(expr['data'])
413            check_union(expr, info)
414        elif meta == 'alternate':
415            check_keys(expr, info, meta,
416                       ['alternate', 'data'], ['if', 'features'])
417            normalize_members(expr['data'])
418            check_alternate(expr, info)
419        elif meta == 'struct':
420            check_keys(expr, info, meta,
421                       ['struct', 'data'], ['base', 'if', 'features'])
422            normalize_members(expr['data'])
423            check_struct(expr, info)
424        elif meta == 'command':
425            check_keys(expr, info, meta,
426                       ['command'],
427                       ['data', 'returns', 'boxed', 'if', 'features',
428                        'gen', 'success-response', 'allow-oob',
429                        'allow-preconfig', 'coroutine'])
430            normalize_members(expr.get('data'))
431            check_command(expr, info)
432        elif meta == 'event':
433            check_keys(expr, info, meta,
434                       ['event'], ['data', 'boxed', 'if', 'features'])
435            normalize_members(expr.get('data'))
436            check_event(expr, info)
437        else:
438            assert False, 'unexpected meta type'
439
440        check_if(expr, info, meta)
441        check_features(expr.get('features'), info)
442        check_flags(expr, info)
443
444    return exprs
445