| # 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 inehritance | |
| 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) | |
| # Handloe 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 metod {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 inheritence | |
| 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) |