1# Copyright 2013-2021 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from .. import mesonlib, mlog
16from .baseobjects import TV_func, TYPE_var, TYPE_kwargs
17from .disabler import Disabler
18from .exceptions import InterpreterException, InvalidArguments
19from .operator import MesonOperator
20from ._unholder import _unholder
21
22from functools import wraps
23import abc
24import itertools
25import copy
26import typing as T
27if T.TYPE_CHECKING:
28    from .. import mparser
29
30def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', str]:
31    # First argument could be InterpreterBase, InterpreterObject or ModuleObject.
32    # In the case of a ModuleObject it is the 2nd argument (ModuleState) that
33    # contains the needed information.
34    s = wrapped_args[0]
35    if not hasattr(s, 'current_node'):
36        s = wrapped_args[1]
37    node = s.current_node
38    subproject = s.subproject
39    args = kwargs = None
40    if len(wrapped_args) >= 3:
41        args = wrapped_args[-2]
42        kwargs = wrapped_args[-1]
43    return node, args, kwargs, subproject
44
45def noPosargs(f: TV_func) -> TV_func:
46    @wraps(f)
47    def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
48        args = get_callee_args(wrapped_args)[1]
49        if args:
50            raise InvalidArguments('Function does not take positional arguments.')
51        return f(*wrapped_args, **wrapped_kwargs)
52    return T.cast(TV_func, wrapped)
53
54def noKwargs(f: TV_func) -> TV_func:
55    @wraps(f)
56    def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
57        kwargs = get_callee_args(wrapped_args)[2]
58        if kwargs:
59            raise InvalidArguments('Function does not take keyword arguments.')
60        return f(*wrapped_args, **wrapped_kwargs)
61    return T.cast(TV_func, wrapped)
62
63def stringArgs(f: TV_func) -> TV_func:
64    @wraps(f)
65    def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
66        args = get_callee_args(wrapped_args)[1]
67        if not isinstance(args, list):
68            mlog.debug('Not a list:', str(args))
69            raise InvalidArguments('Argument not a list.')
70        if not all(isinstance(s, str) for s in args):
71            mlog.debug('Element not a string:', str(args))
72            raise InvalidArguments('Arguments must be strings.')
73        return f(*wrapped_args, **wrapped_kwargs)
74    return T.cast(TV_func, wrapped)
75
76def noArgsFlattening(f: TV_func) -> TV_func:
77    setattr(f, 'no-args-flattening', True)  # noqa: B010
78    return f
79
80def noSecondLevelHolderResolving(f: TV_func) -> TV_func:
81    setattr(f, 'no-second-level-holder-flattening', True)  # noqa: B010
82    return f
83
84def unholder_return(f: TV_func) -> T.Callable[..., TYPE_var]:
85    @wraps(f)
86    def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
87        res = f(*wrapped_args, **wrapped_kwargs)
88        return _unholder(res)
89    return T.cast(T.Callable[..., TYPE_var], wrapped)
90
91def disablerIfNotFound(f: TV_func) -> TV_func:
92    @wraps(f)
93    def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
94        kwargs = get_callee_args(wrapped_args)[2]
95        disabler = kwargs.pop('disabler', False)
96        ret = f(*wrapped_args, **wrapped_kwargs)
97        if disabler and not ret.found():
98            return Disabler()
99        return ret
100    return T.cast(TV_func, wrapped)
101
102class permittedKwargs:
103
104    def __init__(self, permitted: T.Set[str]):
105        self.permitted = permitted  # type: T.Set[str]
106
107    def __call__(self, f: TV_func) -> TV_func:
108        @wraps(f)
109        def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
110            kwargs = get_callee_args(wrapped_args)[2]
111            unknowns = set(kwargs).difference(self.permitted)
112            if unknowns:
113                ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)])
114                raise InvalidArguments(f'Got unknown keyword arguments {ustr}')
115            return f(*wrapped_args, **wrapped_kwargs)
116        return T.cast(TV_func, wrapped)
117
118if T.TYPE_CHECKING:
119    from .baseobjects import InterpreterObject
120    from typing_extensions import Protocol
121
122    _TV_IntegerObject = T.TypeVar('_TV_IntegerObject', bound=InterpreterObject, contravariant=True)
123    _TV_ARG1 = T.TypeVar('_TV_ARG1', bound=TYPE_var, contravariant=True)
124
125    class FN_Operator(Protocol[_TV_IntegerObject, _TV_ARG1]):
126        def __call__(s, self: _TV_IntegerObject, other: _TV_ARG1) -> TYPE_var: ...
127    _TV_FN_Operator = T.TypeVar('_TV_FN_Operator', bound=FN_Operator)
128
129def typed_operator(operator: MesonOperator,
130                   types: T.Union[T.Type, T.Tuple[T.Type, ...]]) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']:
131    """Decorator that does type checking for operator calls.
132
133    The principle here is similar to typed_pos_args, however much simpler
134    since only one other object ever is passed
135    """
136    def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator':
137        @wraps(f)
138        def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var:
139            if not isinstance(other, types):
140                raise InvalidArguments(f'The `{operator.value}` of {self.display_name()} does not accept objects of type {type(other).__name__} ({other})')
141            return f(self, other)
142        return T.cast('_TV_FN_Operator', wrapper)
143    return inner
144
145def unary_operator(operator: MesonOperator) -> T.Callable[['_TV_FN_Operator'], '_TV_FN_Operator']:
146    """Decorator that does type checking for unary operator calls.
147
148    This decorator is for unary operators that do not take any other objects.
149    It should be impossible for a user to accidentally break this. Triggering
150    this check always indicates a bug in the Meson interpreter.
151    """
152    def inner(f: '_TV_FN_Operator') -> '_TV_FN_Operator':
153        @wraps(f)
154        def wrapper(self: 'InterpreterObject', other: TYPE_var) -> TYPE_var:
155            if other is not None:
156                raise mesonlib.MesonBugException(f'The unary operator `{operator.value}` of {self.display_name()} was passed the object {other} of type {type(other).__name__}')
157            return f(self, other)
158        return T.cast('_TV_FN_Operator', wrapper)
159    return inner
160
161
162def typed_pos_args(name: str, *types: T.Union[T.Type, T.Tuple[T.Type, ...]],
163                   varargs: T.Optional[T.Union[T.Type, T.Tuple[T.Type, ...]]] = None,
164                   optargs: T.Optional[T.List[T.Union[T.Type, T.Tuple[T.Type, ...]]]] = None,
165                   min_varargs: int = 0, max_varargs: int = 0) -> T.Callable[..., T.Any]:
166    """Decorator that types type checking of positional arguments.
167
168    This supports two different models of optional arguments, the first is the
169    variadic argument model. Variadic arguments are a possibly bounded,
170    possibly unbounded number of arguments of the same type (unions are
171    supported). The second is the standard default value model, in this case
172    a number of optional arguments may be provided, but they are still
173    ordered, and they may have different types.
174
175    This function does not support mixing variadic and default arguments.
176
177    :name: The name of the decorated function (as displayed in error messages)
178    :varargs: They type(s) of any variadic arguments the function takes. If
179        None the function takes no variadic args
180    :min_varargs: the minimum number of variadic arguments taken
181    :max_varargs: the maximum number of variadic arguments taken. 0 means unlimited
182    :optargs: The types of any optional arguments parameters taken. If None
183        then no optional parameters are taken.
184
185    Some examples of usage blow:
186    >>> @typed_pos_args('mod.func', str, (str, int))
187    ... def func(self, state: ModuleState, args: T.Tuple[str, T.Union[str, int]], kwargs: T.Dict[str, T.Any]) -> T.Any:
188    ...     pass
189
190    >>> @typed_pos_args('method', str, varargs=str)
191    ... def method(self, node: BaseNode, args: T.Tuple[str, T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
192    ...     pass
193
194    >>> @typed_pos_args('method', varargs=str, min_varargs=1)
195    ... def method(self, node: BaseNode, args: T.Tuple[T.List[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
196    ...     pass
197
198    >>> @typed_pos_args('method', str, optargs=[(str, int), str])
199    ... def method(self, node: BaseNode, args: T.Tuple[str, T.Optional[T.Union[str, int]], T.Optional[str]], kwargs: T.Dict[str, T.Any]) -> T.Any:
200    ...     pass
201
202    When should you chose `typed_pos_args('name', varargs=str,
203    min_varargs=1)` vs `typed_pos_args('name', str, varargs=str)`?
204
205    The answer has to do with the semantics of the function, if all of the
206    inputs are the same type (such as with `files()`) then the former is
207    correct, all of the arguments are string names of files. If the first
208    argument is something else the it should be separated.
209    """
210    def inner(f: TV_func) -> TV_func:
211
212        @wraps(f)
213        def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
214            args = get_callee_args(wrapped_args)[1]
215
216            # These are implementation programming errors, end users should never see them.
217            assert isinstance(args, list), args
218            assert max_varargs >= 0, 'max_varags cannot be negative'
219            assert min_varargs >= 0, 'min_varags cannot be negative'
220            assert optargs is None or varargs is None, \
221                'varargs and optargs not supported together as this would be ambiguous'
222
223            num_args = len(args)
224            num_types = len(types)
225            a_types = types
226
227            if varargs:
228                min_args = num_types + min_varargs
229                max_args = num_types + max_varargs
230                if max_varargs == 0 and num_args < min_args:
231                    raise InvalidArguments(f'{name} takes at least {min_args} arguments, but got {num_args}.')
232                elif max_varargs != 0 and (num_args < min_args or num_args > max_args):
233                    raise InvalidArguments(f'{name} takes between {min_args} and {max_args} arguments, but got {num_args}.')
234            elif optargs:
235                if num_args < num_types:
236                    raise InvalidArguments(f'{name} takes at least {num_types} arguments, but got {num_args}.')
237                elif num_args > num_types + len(optargs):
238                    raise InvalidArguments(f'{name} takes at most {num_types + len(optargs)} arguments, but got {num_args}.')
239                # Add the number of positional arguments required
240                if num_args > num_types:
241                    diff = num_args - num_types
242                    a_types = tuple(list(types) + list(optargs[:diff]))
243            elif num_args != num_types:
244                raise InvalidArguments(f'{name} takes exactly {num_types} arguments, but got {num_args}.')
245
246            for i, (arg, type_) in enumerate(itertools.zip_longest(args, a_types, fillvalue=varargs), start=1):
247                if not isinstance(arg, type_):
248                    if isinstance(type_, tuple):
249                        shouldbe = 'one of: {}'.format(", ".join(f'"{t.__name__}"' for t in type_))
250                    else:
251                        shouldbe = f'"{type_.__name__}"'
252                    raise InvalidArguments(f'{name} argument {i} was of type "{type(arg).__name__}" but should have been {shouldbe}')
253
254            # Ensure that we're actually passing a tuple.
255            # Depending on what kind of function we're calling the length of
256            # wrapped_args can vary.
257            nargs = list(wrapped_args)
258            i = nargs.index(args)
259            if varargs:
260                # if we have varargs we need to split them into a separate
261                # tuple, as python's typing doesn't understand tuples with
262                # fixed elements and variadic elements, only one or the other.
263                # so in that case we need T.Tuple[int, str, float, T.Tuple[str, ...]]
264                pos = args[:len(types)]
265                var = list(args[len(types):])
266                pos.append(var)
267                nargs[i] = tuple(pos)
268            elif optargs:
269                if num_args < num_types + len(optargs):
270                    diff = num_types + len(optargs) - num_args
271                    nargs[i] = tuple(list(args) + [None] * diff)
272                else:
273                    nargs[i] = args
274            else:
275                nargs[i] = tuple(args)
276            return f(*nargs, **wrapped_kwargs)
277
278        return T.cast(TV_func, wrapper)
279    return inner
280
281
282class ContainerTypeInfo:
283
284    """Container information for keyword arguments.
285
286    For keyword arguments that are containers (list or dict), this class encodes
287    that information.
288
289    :param container: the type of container
290    :param contains: the types the container holds
291    :param pairs: if the container is supposed to be of even length.
292        This is mainly used for interfaces that predate the addition of dictionaries, and use
293        `[key, value, key2, value2]` format.
294    :param allow_empty: Whether this container is allowed to be empty
295        There are some cases where containers not only must be passed, but must
296        not be empty, and other cases where an empty container is allowed.
297    """
298
299    def __init__(self, container: T.Type, contains: T.Union[T.Type, T.Tuple[T.Type, ...]], *,
300                 pairs: bool = False, allow_empty: bool = True):
301        self.container = container
302        self.contains = contains
303        self.pairs = pairs
304        self.allow_empty = allow_empty
305
306    def check(self, value: T.Any) -> bool:
307        """Check that a value is valid.
308
309        :param value: A value to check
310        :return: True if it is valid, False otherwise
311        """
312        if not isinstance(value, self.container):
313            return False
314        iter_ = iter(value.values()) if isinstance(value, dict) else iter(value)
315        for each in iter_:
316            if not isinstance(each, self.contains):
317                return False
318        if self.pairs and len(value) % 2 != 0:
319            return False
320        if not value and not self.allow_empty:
321            return False
322        return True
323
324    def description(self) -> str:
325        """Human readable description of this container type.
326
327        :return: string to be printed
328        """
329        container = 'dict' if self.container is dict else 'list'
330        if isinstance(self.contains, tuple):
331            contains = ','.join([t.__name__ for t in self.contains])
332        else:
333            contains = self.contains.__name__
334        s = f'{container}[{contains}]'
335        if self.pairs:
336            s += ' that has even size'
337        if not self.allow_empty:
338            s += ' that cannot be empty'
339        return s
340
341_T = T.TypeVar('_T')
342
343class _NULL_T:
344    """Special null type for evolution, this is an implementation detail."""
345
346
347_NULL = _NULL_T()
348
349class KwargInfo(T.Generic[_T]):
350
351    """A description of a keyword argument to a meson function
352
353    This is used to describe a value to the :func:typed_kwargs function.
354
355    :param name: the name of the parameter
356    :param types: A type or tuple of types that are allowed, or a :class:ContainerType
357    :param required: Whether this is a required keyword argument. defaults to False
358    :param listify: If true, then the argument will be listified before being
359        checked. This is useful for cases where the Meson DSL allows a scalar or
360        a container, but internally we only want to work with containers
361    :param default: A default value to use if this isn't set. defaults to None,
362        this may be safely set to a mutable type, as long as that type does not
363        itself contain mutable types, typed_kwargs will copy the default
364    :param since: Meson version in which this argument has been added. defaults to None
365    :param deprecated: Meson version in which this argument has been deprecated. defaults to None
366    :param validator: A callable that does additional validation. This is mainly
367        intended for cases where a string is expected, but only a few specific
368        values are accepted. Must return None if the input is valid, or a
369        message if the input is invalid
370    :param convertor: A callable that converts the raw input value into a
371        different type. This is intended for cases such as the meson DSL using a
372        string, but the implementation using an Enum. This should not do
373        validation, just conversion.
374    :param deprecated_values: a dictionary mapping a value to the version of
375        meson it was deprecated in.
376    :param since_values: a dictionary mapping a value to the version of meson it was
377        added in.
378    :param not_set_warning: A warning message that is logged if the kwarg is not
379        set by the user.
380    """
381    def __init__(self, name: str,
382                 types: T.Union[T.Type[_T], T.Tuple[T.Union[T.Type[_T], ContainerTypeInfo], ...], ContainerTypeInfo],
383                 *, required: bool = False, listify: bool = False,
384                 default: T.Optional[_T] = None,
385                 since: T.Optional[str] = None,
386                 since_values: T.Optional[T.Dict[str, str]] = None,
387                 deprecated: T.Optional[str] = None,
388                 deprecated_values: T.Optional[T.Dict[str, str]] = None,
389                 validator: T.Optional[T.Callable[[T.Any], T.Optional[str]]] = None,
390                 convertor: T.Optional[T.Callable[[_T], object]] = None,
391                 not_set_warning: T.Optional[str] = None):
392        self.name = name
393        self.types = types
394        self.required = required
395        self.listify = listify
396        self.default = default
397        self.since_values = since_values
398        self.since = since
399        self.deprecated = deprecated
400        self.deprecated_values = deprecated_values
401        self.validator = validator
402        self.convertor = convertor
403        self.not_set_warning = not_set_warning
404
405    def evolve(self, *,
406               name: T.Union[str, _NULL_T] = _NULL,
407               required: T.Union[bool, _NULL_T] = _NULL,
408               listify: T.Union[bool, _NULL_T] = _NULL,
409               default: T.Union[_T, None, _NULL_T] = _NULL,
410               since: T.Union[str, None, _NULL_T] = _NULL,
411               since_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL,
412               deprecated: T.Union[str, None, _NULL_T] = _NULL,
413               deprecated_values: T.Union[T.Dict[str, str], None, _NULL_T] = _NULL,
414               validator: T.Union[T.Callable[[_T], T.Optional[str]], None, _NULL_T] = _NULL,
415               convertor: T.Union[T.Callable[[_T], TYPE_var], None, _NULL_T] = _NULL) -> 'KwargInfo':
416        """Create a shallow copy of this KwargInfo, with modifications.
417
418        This allows us to create a new copy of a KwargInfo with modifications.
419        This allows us to use a shared kwarg that implements complex logic, but
420        has slight differences in usage, such as being added to different
421        functions in different versions of Meson.
422
423        The use the _NULL special value here allows us to pass None, which has
424        meaning in many of these cases. _NULL itself is never stored, always
425        being replaced by either the copy in self, or the provided new version.
426        """
427        return type(self)(
428            name if not isinstance(name, _NULL_T) else self.name,
429            self.types,
430            listify=listify if not isinstance(listify, _NULL_T) else self.listify,
431            required=required if not isinstance(required, _NULL_T) else self.required,
432            default=default if not isinstance(default, _NULL_T) else self.default,
433            since=since if not isinstance(since, _NULL_T) else self.since,
434            since_values=since_values if not isinstance(since_values, _NULL_T) else self.since_values,
435            deprecated=deprecated if not isinstance(deprecated, _NULL_T) else self.deprecated,
436            deprecated_values=deprecated_values if not isinstance(deprecated_values, _NULL_T) else self.deprecated_values,
437            validator=validator if not isinstance(validator, _NULL_T) else self.validator,
438            convertor=convertor if not isinstance(convertor, _NULL_T) else self.convertor,
439        )
440
441
442def typed_kwargs(name: str, *types: KwargInfo) -> T.Callable[..., T.Any]:
443    """Decorator for type checking keyword arguments.
444
445    Used to wrap a meson DSL implementation function, where it checks various
446    things about keyword arguments, including the type, and various other
447    information. For non-required values it sets the value to a default, which
448    means the value will always be provided.
449
450    If type tyhpe is a :class:ContainerTypeInfo, then the default value will be
451    passed as an argument to the container initializer, making a shallow copy
452
453    :param name: the name of the function, including the object it's attached to
454        (if applicable)
455    :param *types: KwargInfo entries for each keyword argument.
456    """
457    def inner(f: TV_func) -> TV_func:
458
459        @wraps(f)
460        def wrapper(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
461            _kwargs, subproject = get_callee_args(wrapped_args)[2:4]
462            # Cast here, as the convertor function may place something other than a TYPE_var in the kwargs
463            kwargs = T.cast(T.Dict[str, object], _kwargs)
464
465            all_names = {t.name for t in types}
466            unknowns = set(kwargs).difference(all_names)
467            if unknowns:
468                ustr = ', '.join([f'"{u}"' for u in sorted(unknowns)])
469                raise InvalidArguments(f'{name} got unknown keyword arguments {ustr}')
470
471            for info in types:
472                types_tuple = info.types if isinstance(info.types, tuple) else (info.types,)
473                def check_value_type(value: T.Any) -> bool:
474                    for t in types_tuple:
475                        if isinstance(t, ContainerTypeInfo):
476                            if t.check(value):
477                                return True
478                        elif isinstance(value, t):
479                            return True
480                    return False
481                def types_description() -> str:
482                    candidates = []
483                    for t in types_tuple:
484                        if isinstance(t, ContainerTypeInfo):
485                            candidates.append(t.description())
486                        else:
487                            candidates.append(t.__name__)
488                    shouldbe = 'one of: ' if len(candidates) > 1 else ''
489                    shouldbe += ', '.join(candidates)
490                    return shouldbe
491                value = kwargs.get(info.name)
492                if value is not None:
493                    if info.since:
494                        feature_name = info.name + ' arg in ' + name
495                        FeatureNew.single_use(feature_name, info.since, subproject)
496                    if info.deprecated:
497                        feature_name = info.name + ' arg in ' + name
498                        FeatureDeprecated.single_use(feature_name, info.deprecated, subproject)
499                    if info.listify:
500                        kwargs[info.name] = value = mesonlib.listify(value)
501                    if not check_value_type(value):
502                        shouldbe = types_description()
503                        raise InvalidArguments(f'{name} keyword argument {info.name!r} was of type {type(value).__name__!r} but should have been {shouldbe}')
504
505                    if info.validator is not None:
506                        msg = info.validator(value)
507                        if msg is not None:
508                            raise InvalidArguments(f'{name} keyword argument "{info.name}" {msg}')
509
510                    warn: bool
511                    if info.deprecated_values is not None:
512                        for n, version in info.deprecated_values.items():
513                            if isinstance(value, (dict, list)):
514                                warn = n in value
515                            else:
516                                warn = n == value
517
518                            if warn:
519                                FeatureDeprecated.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject)
520
521                    if info.since_values is not None:
522                        for n, version in info.since_values.items():
523                            if isinstance(value, (dict, list)):
524                                warn = n in value
525                            else:
526                                warn = n == value
527
528                            if warn:
529                                FeatureNew.single_use(f'"{name}" keyword argument "{info.name}" value "{n}"', version, subproject)
530
531                elif info.required:
532                    raise InvalidArguments(f'{name} is missing required keyword argument "{info.name}"')
533                else:
534                    # set the value to the default, this ensuring all kwargs are present
535                    # This both simplifies the typing checking and the usage
536                    assert check_value_type(info.default), f'In funcion {name} default value of {info.name} is not a valid type, got {type(info.default)} expected {types_description()}'
537                    # Create a shallow copy of the container. This allows mutable
538                    # types to be used safely as default values
539                    kwargs[info.name] = copy.copy(info.default)
540                    if info.not_set_warning:
541                        mlog.warning(info.not_set_warning)
542
543                if info.convertor:
544                    kwargs[info.name] = info.convertor(kwargs[info.name])
545
546            return f(*wrapped_args, **wrapped_kwargs)
547        return T.cast(TV_func, wrapper)
548    return inner
549
550
551class FeatureCheckBase(metaclass=abc.ABCMeta):
552    "Base class for feature version checks"
553
554    # In python 3.6 we can just forward declare this, but in 3.5 we can't
555    # This will be overwritten by the subclasses by necessity
556    feature_registry = {}  # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]]
557
558    def __init__(self, feature_name: str, version: str, extra_message: T.Optional[str] = None):
559        self.feature_name = feature_name  # type: str
560        self.feature_version = version    # type: str
561        self.extra_message = extra_message or ''  # type: str
562
563    @staticmethod
564    def get_target_version(subproject: str) -> str:
565        # Don't do any checks if project() has not been parsed yet
566        if subproject not in mesonlib.project_meson_versions:
567            return ''
568        return mesonlib.project_meson_versions[subproject]
569
570    @staticmethod
571    @abc.abstractmethod
572    def check_version(target_version: str, feature_Version: str) -> bool:
573        pass
574
575    def use(self, subproject: str) -> None:
576        tv = self.get_target_version(subproject)
577        # No target version
578        if tv == '':
579            return
580        # Target version is new enough
581        if self.check_version(tv, self.feature_version):
582            return
583        # Feature is too new for target version, register it
584        if subproject not in self.feature_registry:
585            self.feature_registry[subproject] = {self.feature_version: set()}
586        register = self.feature_registry[subproject]
587        if self.feature_version not in register:
588            register[self.feature_version] = set()
589        if self.feature_name in register[self.feature_version]:
590            # Don't warn about the same feature multiple times
591            # FIXME: This is needed to prevent duplicate warnings, but also
592            # means we won't warn about a feature used in multiple places.
593            return
594        register[self.feature_version].add(self.feature_name)
595        self.log_usage_warning(tv)
596
597    @classmethod
598    def report(cls, subproject: str) -> None:
599        if subproject not in cls.feature_registry:
600            return
601        warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject))
602        fv = cls.feature_registry[subproject]
603        for version in sorted(fv.keys()):
604            warning_str += '\n * {}: {}'.format(version, fv[version])
605        mlog.warning(warning_str)
606
607    def log_usage_warning(self, tv: str) -> None:
608        raise InterpreterException('log_usage_warning not implemented')
609
610    @staticmethod
611    def get_warning_str_prefix(tv: str) -> str:
612        raise InterpreterException('get_warning_str_prefix not implemented')
613
614    def __call__(self, f: TV_func) -> TV_func:
615        @wraps(f)
616        def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
617            subproject = get_callee_args(wrapped_args)[3]
618            if subproject is None:
619                raise AssertionError(f'{wrapped_args!r}')
620            self.use(subproject)
621            return f(*wrapped_args, **wrapped_kwargs)
622        return T.cast(TV_func, wrapped)
623
624    @classmethod
625    def single_use(cls, feature_name: str, version: str, subproject: str,
626                   extra_message: T.Optional[str] = None) -> None:
627        """Oneline version that instantiates and calls use()."""
628        cls(feature_name, version, extra_message).use(subproject)
629
630
631class FeatureNew(FeatureCheckBase):
632    """Checks for new features"""
633
634    # Class variable, shared across all instances
635    #
636    # Format: {subproject: {feature_version: set(feature_names)}}
637    feature_registry = {}  # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]]
638
639    @staticmethod
640    def check_version(target_version: str, feature_version: str) -> bool:
641        return mesonlib.version_compare_condition_with_min(target_version, feature_version)
642
643    @staticmethod
644    def get_warning_str_prefix(tv: str) -> str:
645        return f'Project specifies a minimum meson_version \'{tv}\' but uses features which were added in newer versions:'
646
647    def log_usage_warning(self, tv: str) -> None:
648        args = [
649            'Project targeting', f"'{tv}'",
650            'but tried to use feature introduced in',
651            f"'{self.feature_version}':",
652            f'{self.feature_name}.',
653        ]
654        if self.extra_message:
655            args.append(self.extra_message)
656        mlog.warning(*args)
657
658class FeatureDeprecated(FeatureCheckBase):
659    """Checks for deprecated features"""
660
661    # Class variable, shared across all instances
662    #
663    # Format: {subproject: {feature_version: set(feature_names)}}
664    feature_registry = {}  # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]]
665
666    @staticmethod
667    def check_version(target_version: str, feature_version: str) -> bool:
668        # For deprecation checks we need to return the inverse of FeatureNew checks
669        return not mesonlib.version_compare_condition_with_min(target_version, feature_version)
670
671    @staticmethod
672    def get_warning_str_prefix(tv: str) -> str:
673        return 'Deprecated features used:'
674
675    def log_usage_warning(self, tv: str) -> None:
676        args = [
677            'Project targeting', f"'{tv}'",
678            'but tried to use feature deprecated since',
679            f"'{self.feature_version}':",
680            f'{self.feature_name}.',
681        ]
682        if self.extra_message:
683            args.append(self.extra_message)
684        mlog.warning(*args)
685
686
687class FeatureCheckKwargsBase(metaclass=abc.ABCMeta):
688
689    @property
690    @abc.abstractmethod
691    def feature_check_class(self) -> T.Type[FeatureCheckBase]:
692        pass
693
694    def __init__(self, feature_name: str, feature_version: str,
695                 kwargs: T.List[str], extra_message: T.Optional[str] = None):
696        self.feature_name = feature_name
697        self.feature_version = feature_version
698        self.kwargs = kwargs
699        self.extra_message = extra_message
700
701    def __call__(self, f: TV_func) -> TV_func:
702        @wraps(f)
703        def wrapped(*wrapped_args: T.Any, **wrapped_kwargs: T.Any) -> T.Any:
704            kwargs, subproject = get_callee_args(wrapped_args)[2:4]
705            if subproject is None:
706                raise AssertionError(f'{wrapped_args!r}')
707            for arg in self.kwargs:
708                if arg not in kwargs:
709                    continue
710                name = arg + ' arg in ' + self.feature_name
711                self.feature_check_class.single_use(
712                        name, self.feature_version, subproject, self.extra_message)
713            return f(*wrapped_args, **wrapped_kwargs)
714        return T.cast(TV_func, wrapped)
715
716class FeatureNewKwargs(FeatureCheckKwargsBase):
717    feature_check_class = FeatureNew
718
719class FeatureDeprecatedKwargs(FeatureCheckKwargsBase):
720    feature_check_class = FeatureDeprecated
721