xref: /openbmc/qemu/scripts/qapi/expr.py (revision e42648dccdd1defe8f35f247966cd7283f865cd6)
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    ifcond = expr.get('if')
150    if ifcond is None:
151        return
152
153    if isinstance(ifcond, list):
154        if not ifcond:
155            raise QAPISemError(
156                info, "'if' condition [] of %s is useless" % source)
157    else:
158        # Normalize to a list
159        ifcond = expr['if'] = [ifcond]
160
161    for elt in ifcond:
162        if not isinstance(elt, str):
163            raise QAPISemError(
164                info,
165                "'if' condition of %s must be a string or a list of strings"
166                % source)
167        if not elt.strip():
168            raise QAPISemError(
169                info,
170                "'if' condition '%s' of %s makes no sense"
171                % (elt, source))
172
173
174def normalize_members(members: object) -> None:
175    if isinstance(members, dict):
176        for key, arg in members.items():
177            if isinstance(arg, dict):
178                continue
179            members[key] = {'type': arg}
180
181
182def check_type(value: Optional[object],
183               info: QAPISourceInfo,
184               source: str,
185               allow_array: bool = False,
186               allow_dict: Union[bool, str] = False) -> None:
187    if value is None:
188        return
189
190    # Type name
191    if isinstance(value, str):
192        return
193
194    # Array type
195    if isinstance(value, list):
196        if not allow_array:
197            raise QAPISemError(info, "%s cannot be an array" % source)
198        if len(value) != 1 or not isinstance(value[0], str):
199            raise QAPISemError(info,
200                               "%s: array type must contain single type name" %
201                               source)
202        return
203
204    # Anonymous type
205
206    if not allow_dict:
207        raise QAPISemError(info, "%s should be a type name" % source)
208
209    if not isinstance(value, dict):
210        raise QAPISemError(info,
211                           "%s should be an object or type name" % source)
212
213    permissive = False
214    if isinstance(allow_dict, str):
215        permissive = allow_dict in info.pragma.member_name_exceptions
216
217    # value is a dictionary, check that each member is okay
218    for (key, arg) in value.items():
219        key_source = "%s member '%s'" % (source, key)
220        if key.startswith('*'):
221            key = key[1:]
222        check_name_lower(key, info, key_source,
223                         permit_upper=permissive,
224                         permit_underscore=permissive)
225        if c_name(key, False) == 'u' or c_name(key, False).startswith('has_'):
226            raise QAPISemError(info, "%s uses reserved name" % key_source)
227        check_keys(arg, info, key_source, ['type'], ['if', 'features'])
228        check_if(arg, info, key_source)
229        check_features(arg.get('features'), info)
230        check_type(arg['type'], info, key_source, allow_array=True)
231
232
233def check_features(features: Optional[object],
234                   info: QAPISourceInfo) -> None:
235    if features is None:
236        return
237    if not isinstance(features, list):
238        raise QAPISemError(info, "'features' must be an array")
239    features[:] = [f if isinstance(f, dict) else {'name': f}
240                   for f in features]
241    for feat in features:
242        source = "'features' member"
243        assert isinstance(feat, dict)
244        check_keys(feat, info, source, ['name'], ['if'])
245        check_name_is_str(feat['name'], info, source)
246        source = "%s '%s'" % (source, feat['name'])
247        check_name_str(feat['name'], info, source)
248        check_if(feat, info, source)
249
250
251def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
252    name = expr['enum']
253    members = expr['data']
254    prefix = expr.get('prefix')
255
256    if not isinstance(members, list):
257        raise QAPISemError(info, "'data' must be an array")
258    if prefix is not None and not isinstance(prefix, str):
259        raise QAPISemError(info, "'prefix' must be a string")
260
261    permissive = name in info.pragma.member_name_exceptions
262
263    members[:] = [m if isinstance(m, dict) else {'name': m}
264                  for m in members]
265    for member in members:
266        source = "'data' member"
267        member_name = member['name']
268        check_keys(member, info, source, ['name'], ['if'])
269        check_name_is_str(member_name, info, source)
270        source = "%s '%s'" % (source, member_name)
271        # Enum members may start with a digit
272        if member_name[0].isdigit():
273            member_name = 'd' + member_name  # Hack: hide the digit
274        check_name_lower(member_name, info, source,
275                         permit_upper=permissive,
276                         permit_underscore=permissive)
277        check_if(member, info, source)
278
279
280def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
281    name = cast(str, expr['struct'])  # Checked in check_exprs
282    members = expr['data']
283
284    check_type(members, info, "'data'", allow_dict=name)
285    check_type(expr.get('base'), info, "'base'")
286
287
288def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
289    name = cast(str, expr['union'])  # Checked in check_exprs
290    base = expr.get('base')
291    discriminator = expr.get('discriminator')
292    members = expr['data']
293
294    if discriminator is None:   # simple union
295        if base is not None:
296            raise QAPISemError(info, "'base' requires 'discriminator'")
297    else:                       # flat union
298        check_type(base, info, "'base'", allow_dict=name)
299        if not base:
300            raise QAPISemError(info, "'discriminator' requires 'base'")
301        check_name_is_str(discriminator, info, "'discriminator'")
302
303    if not isinstance(members, dict):
304        raise QAPISemError(info, "'data' must be an object")
305
306    for (key, value) in members.items():
307        source = "'data' member '%s'" % key
308        if discriminator is None:
309            check_name_lower(key, info, source)
310        # else: name is in discriminator enum, which gets checked
311        check_keys(value, info, source, ['type'], ['if'])
312        check_if(value, info, source)
313        check_type(value['type'], info, source, allow_array=not base)
314
315
316def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
317    members = expr['data']
318
319    if not members:
320        raise QAPISemError(info, "'data' must not be empty")
321
322    if not isinstance(members, dict):
323        raise QAPISemError(info, "'data' must be an object")
324
325    for (key, value) in members.items():
326        source = "'data' member '%s'" % key
327        check_name_lower(key, info, source)
328        check_keys(value, info, source, ['type'], ['if'])
329        check_if(value, info, source)
330        check_type(value['type'], info, source)
331
332
333def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
334    args = expr.get('data')
335    rets = expr.get('returns')
336    boxed = expr.get('boxed', False)
337
338    if boxed and args is None:
339        raise QAPISemError(info, "'boxed': true requires 'data'")
340    check_type(args, info, "'data'", allow_dict=not boxed)
341    check_type(rets, info, "'returns'", allow_array=True)
342
343
344def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
345    args = expr.get('data')
346    boxed = expr.get('boxed', False)
347
348    if boxed and args is None:
349        raise QAPISemError(info, "'boxed': true requires 'data'")
350    check_type(args, info, "'data'", allow_dict=not boxed)
351
352
353def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
354    for expr_elem in exprs:
355        # Expression
356        assert isinstance(expr_elem['expr'], dict)
357        for key in expr_elem['expr'].keys():
358            assert isinstance(key, str)
359        expr: _JSONObject = expr_elem['expr']
360
361        # QAPISourceInfo
362        assert isinstance(expr_elem['info'], QAPISourceInfo)
363        info: QAPISourceInfo = expr_elem['info']
364
365        # Optional[QAPIDoc]
366        tmp = expr_elem.get('doc')
367        assert tmp is None or isinstance(tmp, QAPIDoc)
368        doc: Optional[QAPIDoc] = tmp
369
370        if 'include' in expr:
371            continue
372
373        if 'enum' in expr:
374            meta = 'enum'
375        elif 'union' in expr:
376            meta = 'union'
377        elif 'alternate' in expr:
378            meta = 'alternate'
379        elif 'struct' in expr:
380            meta = 'struct'
381        elif 'command' in expr:
382            meta = 'command'
383        elif 'event' in expr:
384            meta = 'event'
385        else:
386            raise QAPISemError(info, "expression is missing metatype")
387
388        check_name_is_str(expr[meta], info, "'%s'" % meta)
389        name = cast(str, expr[meta])
390        info.set_defn(meta, name)
391        check_defn_name_str(name, info, meta)
392
393        if doc:
394            if doc.symbol != name:
395                raise QAPISemError(
396                    info, "documentation comment is for '%s'" % doc.symbol)
397            doc.check_expr(expr)
398        elif info.pragma.doc_required:
399            raise QAPISemError(info,
400                               "documentation comment required")
401
402        if meta == 'enum':
403            check_keys(expr, info, meta,
404                       ['enum', 'data'], ['if', 'features', 'prefix'])
405            check_enum(expr, info)
406        elif meta == 'union':
407            check_keys(expr, info, meta,
408                       ['union', 'data'],
409                       ['base', 'discriminator', 'if', 'features'])
410            normalize_members(expr.get('base'))
411            normalize_members(expr['data'])
412            check_union(expr, info)
413        elif meta == 'alternate':
414            check_keys(expr, info, meta,
415                       ['alternate', 'data'], ['if', 'features'])
416            normalize_members(expr['data'])
417            check_alternate(expr, info)
418        elif meta == 'struct':
419            check_keys(expr, info, meta,
420                       ['struct', 'data'], ['base', 'if', 'features'])
421            normalize_members(expr['data'])
422            check_struct(expr, info)
423        elif meta == 'command':
424            check_keys(expr, info, meta,
425                       ['command'],
426                       ['data', 'returns', 'boxed', 'if', 'features',
427                        'gen', 'success-response', 'allow-oob',
428                        'allow-preconfig', 'coroutine'])
429            normalize_members(expr.get('data'))
430            check_command(expr, info)
431        elif meta == 'event':
432            check_keys(expr, info, meta,
433                       ['event'], ['data', 'boxed', 'if', 'features'])
434            normalize_members(expr.get('data'))
435            check_event(expr, info)
436        else:
437            assert False, 'unexpected meta type'
438
439        check_if(expr, info, meta)
440        check_features(expr.get('features'), info)
441        check_flags(expr, info)
442
443    return exprs
444