# Copyright 2021 The Meson development team # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from abc import ABCMeta, abstractmethod from pathlib import Path import re import typing as T from .model import ( NamedObject, FetureCheck, ArgBase, PosArg, DataTypeInfo, Type, Function, Method, Object, ObjectType, ReferenceManual, ) from mesonbuild import mlog class _Resolver: def __init__(self) -> None: self.type_map: T.Dict[str, Object] = {} self.func_map: T.Dict[str, T.Union[Function, Method]] = {} self.processed_funcs: T.Set[str] = set() def _validate_named_object(self, obj: NamedObject) -> None: name_regex = re.compile(r'[a-zA-Z0-9_]+') obj.name = obj.name.strip() obj.description = obj.description.strip() assert obj.name and obj.description, 'Both name and description must be set' assert obj.name.islower(), f'Object names must be lower case ({obj.name})' assert name_regex.match(obj.name) or obj.name == '[index]', f'Invalid name {obj.name}' def _validate_feature_check(self, obj: FetureCheck) -> None: meson_version_reg = re.compile(r'[0-9]+\.[0-9]+\.[0-9]+') obj.since = obj.since.strip() obj.deprecated = obj.deprecated.strip() if obj.since: assert meson_version_reg.match(obj.since) if obj.deprecated: assert meson_version_reg.match(obj.deprecated) def _resolve_type(self, raw: str) -> Type: typ = Type(raw) # We can't use `types = raw.split('|')`, because of `list[str | env]` types: T.List[str] = [''] stack = 0 for c in raw: if stack == 0 and c == '|': types += [''] continue if c == '[': stack += 1 if c == ']': stack -= 1 types[-1] += c types = [x.strip() for x in types] for t in types: t = t.strip() idx = t.find('[') base_type = t held_type = None if idx > 0: base_type = t[:idx] held_type = self._resolve_type(t[idx+1:-1]) assert base_type in self.type_map, f'No known object {t}' obj = self.type_map[base_type] typ.resolved += [DataTypeInfo(obj, held_type)] return typ def _validate_func(self, func: T.Union[Function, Method]) -> None: # Always run basic checks, since they also slightly post-process (strip) some strings self._validate_named_object(func) self._validate_feature_check(func) func_id = f'{func.obj.name}.{func.name}' if isinstance(func, Method) else func.name if func_id in self.processed_funcs: return func.returns = self._resolve_type(func.returns.raw) all_args: T.List[ArgBase] = [] all_args += func.posargs all_args += func.optargs all_args += func.kwargs.values() all_args += [func.varargs] if func.varargs else [] for arg in all_args: arg.type = self._resolve_type(arg.type.raw) # Handle returned_by for obj in func.returns.resolved: obj.data_type.returned_by += [func] # Handle kwargs inheritance for base_name in func.kwargs_inherit: base_name = base_name.strip() assert base_name in self.func_map, f'Unknown base function `{base_name}` for {func.name}' base = self.func_map[base_name] if base_name not in self.processed_funcs: self._validate_func(base) curr_keys = set(func.kwargs.keys()) base_keys = set(base.kwargs.keys()) # Calculate the missing kwargs from the current set missing = {k: v for k, v in base.kwargs.items() if k in base_keys - curr_keys} func.kwargs.update(missing) # Handle other args inheritance _T = T.TypeVar('_T', bound=T.Union[ArgBase, T.List[PosArg]]) def resolve_inherit(name: str, curr: _T, resolver: T.Callable[[Function], _T]) -> _T: if name and not curr: name = name.strip() assert name in self.func_map, f'Unknown base function `{name}` for {func.name}' if name not in self.processed_funcs: self._validate_func(self.func_map[name]) ref_args = resolver(self.func_map[name]) assert ref_args is not None, f'Inherited function `{name}` does not have inherited args set' return ref_args return curr func.posargs = resolve_inherit(func.posargs_inherit, func.posargs, lambda x: x.posargs) func.optargs = resolve_inherit(func.optargs_inherit, func.optargs, lambda x: x.optargs) func.varargs = resolve_inherit(func.varargs_inherit, func.varargs, lambda x: x.varargs) self.processed_funcs.add(func_id) def validate_and_resolve(self, manual: ReferenceManual) -> ReferenceManual: mlog.log('Validating loaded manual...') # build type map and func map for methods for obj in manual.objects: assert obj.name not in self.type_map, f'Duplicate object name {obj.name}' self.type_map[obj.name] = obj for m in obj.methods: mid = f'{obj.name}.{m.name}' assert mid not in self.type_map, f'Duplicate method {mid}' self.func_map[mid] = m # Build func map for functions for func in manual.functions: assert func.name not in [*self.func_map.keys()], f'Duplicate function {func.name}' self.func_map[func.name] = func mlog.log('Validating functions...') for func in manual.functions: mlog.log(' -- validating', mlog.bold(func.name)) self._validate_func(func) mlog.log('Validating objects...') for obj in manual.objects: mlog.log(' -- validating', mlog.bold(obj.name)) self._validate_named_object(obj) self._validate_feature_check(obj) # Resolve and validate inheritance if obj.extends: assert obj.extends in self.type_map, f'Unknown extends object {obj.extends} in {obj.name}' obj.extends_obj = self.type_map[obj.extends] obj.extends_obj.extended_by += [obj] # Only returned objects can be associated with module if obj.obj_type is not ObjectType.RETURNED: assert obj.defined_by_module is None for m in obj.methods: assert m.obj is obj self._validate_func(m) # Resolve inherited methods for obj in manual.objects: inherited_methods = obj.inherited_methods curr = obj.extends_obj while curr is not None: inherited_methods += curr.methods curr = curr.extends_obj return manual class LoaderBase(metaclass=ABCMeta): def __init__(self) -> None: self._input_files: T.List[Path] = [] @property def input_files(self) -> T.List[Path]: return list(self._input_files) def read_file(self, f: Path) -> str: assert f.exists() assert f.is_file() self._input_files += [f.resolve()] return f.read_text(encoding='utf-8') @abstractmethod def load_impl(self) -> ReferenceManual: pass def load(self) -> ReferenceManual: self._input_files = [] # Reset input files manual = self.load_impl() resolver = _Resolver() with mlog.nested(): return resolver.validate_and_resolve(manual)