diff --git a/.github/workflows/lint_mypy.yml b/.github/workflows/lint_mypy.yml index d74043bec..8f6d917eb 100644 --- a/.github/workflows/lint_mypy.yml +++ b/.github/workflows/lint_mypy.yml @@ -30,4 +30,4 @@ jobs: with: python-version: '3.x' - run: python -m pip install mypy - - run: mypy --follow-imports=skip mesonbuild/mtest.py mesonbuild/minit.py mesonbuild/mintro.py mesonbuild/msetup.py mesonbuild/wrap tools/ mesonbuild/modules/fs.py mesonbuild/dependencies/boost.py mesonbuild/dependencies/mpi.py mesonbuild/dependencies/hdf5.py mesonbuild/compilers/mixins/intel.py mesonbuild/mlog.py + - run: mypy --follow-imports=skip mesonbuild/interpreterbase.py mesonbuild/mtest.py mesonbuild/minit.py mesonbuild/mintro.py mesonbuild/mparser.py mesonbuild/msetup.py mesonbuild/ast mesonbuild/wrap tools/ mesonbuild/modules/fs.py mesonbuild/dependencies/boost.py mesonbuild/dependencies/mpi.py mesonbuild/dependencies/hdf5.py mesonbuild/compilers/mixins/intel.py mesonbuild/mlog.py diff --git a/mesonbuild/ast/interpreter.py b/mesonbuild/ast/interpreter.py index 5f47ad340..25dfb8030 100644 --- a/mesonbuild/ast/interpreter.py +++ b/mesonbuild/ast/interpreter.py @@ -19,19 +19,27 @@ from .visitor import AstVisitor from .. import interpreterbase, mparser, mesonlib from .. import environment -from ..interpreterbase import InvalidArguments, BreakRequest, ContinueRequest +from ..interpreterbase import InvalidArguments, BreakRequest, ContinueRequest, TYPE_nvar, TYPE_nkwargs from ..mparser import ( + AndNode, ArgumentNode, ArithmeticNode, ArrayNode, AssignmentNode, BaseNode, + ComparisonNode, ElementaryNode, EmptyNode, + ForeachClauseNode, IdNode, + IfClauseNode, + IndexNode, MethodNode, + OrNode, PlusAssignmentNode, + StringNode, TernaryNode, + UMinusNode, ) import os, sys @@ -59,13 +67,13 @@ ADD_SOURCE = 0 REMOVE_SOURCE = 1 class AstInterpreter(interpreterbase.InterpreterBase): - def __init__(self, source_root: str, subdir: str, visitors: T.Optional[T.List[AstVisitor]] = None): - super().__init__(source_root, subdir) + def __init__(self, source_root: str, subdir: str, subproject: str, visitors: T.Optional[T.List[AstVisitor]] = None): + super().__init__(source_root, subdir, subproject) self.visitors = visitors if visitors is not None else [] - self.visited_subdirs = {} - self.assignments = {} - self.assign_vals = {} - self.reverse_assignment = {} + self.visited_subdirs = {} # type: T.Dict[str, bool] + self.assignments = {} # type: T.Dict[str, BaseNode] + self.assign_vals = {} # type: T.Dict[str, T.Any] + self.reverse_assignment = {} # type: T.Dict[str, BaseNode] self.funcs.update({'project': self.func_do_nothing, 'test': self.func_do_nothing, 'benchmark': self.func_do_nothing, @@ -122,15 +130,15 @@ class AstInterpreter(interpreterbase.InterpreterBase): 'summary': self.func_do_nothing, }) - def func_do_nothing(self, node, args, kwargs): + def func_do_nothing(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> bool: return True - def load_root_meson_file(self): + def load_root_meson_file(self) -> None: super().load_root_meson_file() for i in self.visitors: self.ast.accept(i) - def func_subdir(self, node, args, kwargs): + def func_subdir(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None: args = self.flatten_args(args) if len(args) != 1 or not isinstance(args[0], str): sys.stderr.write('Unable to evaluate subdir({}) in AstInterpreter --> Skipping\n'.format(args)) @@ -165,64 +173,69 @@ class AstInterpreter(interpreterbase.InterpreterBase): self.evaluate_codeblock(codeblock) self.subdir = prev_subdir - def method_call(self, node): + def method_call(self, node: BaseNode) -> bool: return True - def evaluate_arithmeticstatement(self, cur): + def evaluate_arithmeticstatement(self, cur: ArithmeticNode) -> int: self.evaluate_statement(cur.left) self.evaluate_statement(cur.right) return 0 - def evaluate_uminusstatement(self, cur): + def evaluate_uminusstatement(self, cur: UMinusNode) -> int: self.evaluate_statement(cur.value) return 0 - def evaluate_ternary(self, node): + def evaluate_ternary(self, node: TernaryNode) -> None: assert(isinstance(node, TernaryNode)) self.evaluate_statement(node.condition) self.evaluate_statement(node.trueblock) self.evaluate_statement(node.falseblock) - def evaluate_plusassign(self, node): + def evaluate_plusassign(self, node: PlusAssignmentNode) -> None: assert(isinstance(node, PlusAssignmentNode)) - if node.var_name not in self.assignments: - self.assignments[node.var_name] = [] - self.assign_vals[node.var_name] = [] - self.assignments[node.var_name] += [node.value] # Save a reference to the value node - if hasattr(node.value, 'ast_id'): + # Cheat by doing a reassignment + self.assignments[node.var_name] = node.value # Save a reference to the value node + if node.value.ast_id: self.reverse_assignment[node.value.ast_id] = node - self.assign_vals[node.var_name] += [self.evaluate_statement(node.value)] + self.assign_vals[node.var_name] = self.evaluate_statement(node.value) - def evaluate_indexing(self, node): + def evaluate_indexing(self, node: IndexNode) -> int: return 0 - def unknown_function_called(self, func_name): + def unknown_function_called(self, func_name: str) -> None: pass - def reduce_arguments(self, args): + def reduce_arguments(self, args: ArgumentNode, resolve_key_nodes: bool = True) -> T.Tuple[T.List[TYPE_nvar], TYPE_nkwargs]: if isinstance(args, ArgumentNode): + kwargs = {} # type: T.Dict[T.Union[str, BaseNode], TYPE_nvar] + for key, val in args.kwargs.items(): + if isinstance(key, (StringNode, IdNode)): + assert isinstance(key.value, str) + kwargs[key.value] = val + else: + kwargs[key] = val if args.incorrect_order(): raise InvalidArguments('All keyword arguments must be after positional arguments.') - return self.flatten_args(args.arguments), args.kwargs + return self.flatten_args(args.arguments), kwargs else: return self.flatten_args(args), {} - def evaluate_comparison(self, node): + def evaluate_comparison(self, node: ComparisonNode) -> bool: self.evaluate_statement(node.left) self.evaluate_statement(node.right) return False - def evaluate_andstatement(self, cur): + def evaluate_andstatement(self, cur: AndNode) -> bool: self.evaluate_statement(cur.left) self.evaluate_statement(cur.right) return False - def evaluate_orstatement(self, cur): + def evaluate_orstatement(self, cur: OrNode) -> bool: self.evaluate_statement(cur.left) self.evaluate_statement(cur.right) return False - def evaluate_foreach(self, node): + def evaluate_foreach(self, node: ForeachClauseNode) -> None: try: self.evaluate_codeblock(node.block) except ContinueRequest: @@ -230,30 +243,31 @@ class AstInterpreter(interpreterbase.InterpreterBase): except BreakRequest: pass - def evaluate_if(self, node): + def evaluate_if(self, node: IfClauseNode) -> None: for i in node.ifs: self.evaluate_codeblock(i.block) if not isinstance(node.elseblock, EmptyNode): self.evaluate_codeblock(node.elseblock) - def get_variable(self, varname): + def get_variable(self, varname: str) -> int: return 0 - def assignment(self, node): + def assignment(self, node: AssignmentNode) -> None: assert(isinstance(node, AssignmentNode)) - self.assignments[node.var_name] = [node.value] # Save a reference to the value node - if hasattr(node.value, 'ast_id'): + self.assignments[node.var_name] = node.value # Save a reference to the value node + if node.value.ast_id: self.reverse_assignment[node.value.ast_id] = node - self.assign_vals[node.var_name] = [self.evaluate_statement(node.value)] # Evaluate the value just in case + self.assign_vals[node.var_name] = self.evaluate_statement(node.value) # Evaluate the value just in case def resolve_node(self, node: BaseNode, include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.Optional[T.Any]: def quick_resolve(n: BaseNode, loop_detect: T.Optional[T.List[str]] = None) -> T.Any: if loop_detect is None: loop_detect = [] if isinstance(n, IdNode): + assert isinstance(n.value, str) if n.value in loop_detect or n.value not in self.assignments: return [] - return quick_resolve(self.assignments[n.value][0], loop_detect = loop_detect + [n.value]) + return quick_resolve(self.assignments[n.value], loop_detect = loop_detect + [n.value]) elif isinstance(n, ElementaryNode): return n.value else: @@ -266,7 +280,7 @@ class AstInterpreter(interpreterbase.InterpreterBase): if not isinstance(node, BaseNode): return None - assert(hasattr(node, 'ast_id')) + assert node.ast_id if node.ast_id in id_loop_detect: return None # Loop detected id_loop_detect += [node.ast_id] @@ -296,7 +310,7 @@ class AstInterpreter(interpreterbase.InterpreterBase): elif isinstance(node, MethodNode): src = quick_resolve(node.source_object) - margs = self.flatten_args(node.args, include_unknown_args, id_loop_detect) + margs = self.flatten_args(node.args.arguments, include_unknown_args, id_loop_detect) try: if isinstance(src, str): result = self.string_method_call(src, node.name, margs) @@ -315,7 +329,7 @@ class AstInterpreter(interpreterbase.InterpreterBase): if isinstance(result, BaseNode): result = self.resolve_node(result, include_unknown_args, id_loop_detect) elif isinstance(result, list): - new_res = [] + new_res = [] # type: T.List[TYPE_nvar] for i in result: if isinstance(i, BaseNode): resolved = self.resolve_node(i, include_unknown_args, id_loop_detect) @@ -327,12 +341,14 @@ class AstInterpreter(interpreterbase.InterpreterBase): return result - def flatten_args(self, args: T.Any, include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.List[T.Any]: + def flatten_args(self, args_raw: T.Union[TYPE_nvar, T.Sequence[TYPE_nvar]], include_unknown_args: bool = False, id_loop_detect: T.Optional[T.List[str]] = None) -> T.List[TYPE_nvar]: # Make sure we are always dealing with lists - if not isinstance(args, list): - args = [args] + if isinstance(args_raw, list): + args = args_raw + else: + args = [args_raw] - flattend_args = [] + flattend_args = [] # type: T.List[TYPE_nvar] # Resolve the contents of args for i in args: @@ -346,7 +362,7 @@ class AstInterpreter(interpreterbase.InterpreterBase): flattend_args += [i] return flattend_args - def flatten_kwargs(self, kwargs: object, include_unknown_args: bool = False): + def flatten_kwargs(self, kwargs: T.Dict[str, TYPE_nvar], include_unknown_args: bool = False) -> T.Dict[str, TYPE_nvar]: flattend_kwargs = {} for key, val in kwargs.items(): if isinstance(val, BaseNode): diff --git a/mesonbuild/ast/introspection.py b/mesonbuild/ast/introspection.py index 709dbac4e..c9a51bf5a 100644 --- a/mesonbuild/ast/introspection.py +++ b/mesonbuild/ast/introspection.py @@ -15,30 +15,40 @@ # This class contains the basic functionality needed to run any interpreter # or an interpreter-based tool -from . import AstInterpreter +from .interpreter import AstInterpreter +from .visitor import AstVisitor from .. import compilers, environment, mesonlib, optinterpreter from .. import coredata as cdata from ..mesonlib import MachineChoice -from ..interpreterbase import InvalidArguments +from ..interpreterbase import InvalidArguments, TYPE_nvar from ..build import Executable, Jar, SharedLibrary, SharedModule, StaticLibrary from ..mparser import BaseNode, ArithmeticNode, ArrayNode, ElementaryNode, IdNode, FunctionNode, StringNode +import typing as T import os build_target_functions = ['executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries'] class IntrospectionHelper: # mimic an argparse namespace - def __init__(self, cross_file): - self.cross_file = cross_file - self.native_file = None - self.cmd_line_options = {} + def __init__(self, cross_file: str): + self.cross_file = cross_file # type: str + self.native_file = None # type: str + self.cmd_line_options = {} # type: T.Dict[str, str] class IntrospectionInterpreter(AstInterpreter): # Interpreter to detect the options without a build directory # Most of the code is stolen from interpreter.Interpreter - def __init__(self, source_root, subdir, backend, visitors=None, cross_file=None, subproject='', subproject_dir='subprojects', env=None): + def __init__(self, + source_root: str, + subdir: str, + backend: str, + visitors: T.Optional[T.List[AstVisitor]] = None, + cross_file: T.Optional[str] = None, + subproject: str = '', + subproject_dir: str = 'subprojects', + env: T.Optional[environment.Environment] = None): visitors = visitors if visitors is not None else [] - super().__init__(source_root, subdir, visitors=visitors) + super().__init__(source_root, subdir, subproject, visitors=visitors) options = IntrospectionHelper(cross_file) self.cross_file = cross_file @@ -46,16 +56,15 @@ class IntrospectionInterpreter(AstInterpreter): self.environment = environment.Environment(source_root, None, options) else: self.environment = env - self.subproject = subproject self.subproject_dir = subproject_dir self.coredata = self.environment.get_coredata() self.option_file = os.path.join(self.source_root, self.subdir, 'meson_options.txt') self.backend = backend self.default_options = {'backend': self.backend} - self.project_data = {} - self.targets = [] - self.dependencies = [] - self.project_node = None + self.project_data = {} # type: T.Dict[str, T.Any] + self.targets = [] # type: T.List[T.Dict[str, T.Any]] + self.dependencies = [] # type: T.List[T.Dict[str, T.Any]] + self.project_node = None # type: BaseNode self.funcs.update({ 'add_languages': self.func_add_languages, @@ -70,7 +79,7 @@ class IntrospectionInterpreter(AstInterpreter): 'both_libraries': self.func_both_lib, }) - def func_project(self, node, args, kwargs): + def func_project(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None: if self.project_node: raise InvalidArguments('Second call to project()') self.project_node = node @@ -99,7 +108,8 @@ class IntrospectionInterpreter(AstInterpreter): if not self.is_subproject() and 'subproject_dir' in kwargs: spdirname = kwargs['subproject_dir'] - if isinstance(spdirname, ElementaryNode): + if isinstance(spdirname, StringNode): + assert isinstance(spdirname.value, str) self.subproject_dir = spdirname.value if not self.is_subproject(): self.project_data['subprojects'] = [] @@ -115,7 +125,7 @@ class IntrospectionInterpreter(AstInterpreter): self.coredata.set_options(options) self.func_add_languages(None, proj_langs, None) - def do_subproject(self, dirname): + def do_subproject(self, dirname: str) -> None: subproject_dir_abs = os.path.join(self.environment.get_source_dir(), self.subproject_dir) subpr = os.path.join(subproject_dir_abs, dirname) try: @@ -126,15 +136,20 @@ class IntrospectionInterpreter(AstInterpreter): except (mesonlib.MesonException, RuntimeError): return - def func_add_languages(self, node, args, kwargs): + def func_add_languages(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None: args = self.flatten_args(args) for for_machine in [MachineChoice.BUILD, MachineChoice.HOST]: for lang in sorted(args, key=compilers.sort_clink): + if isinstance(lang, StringNode): + assert isinstance(lang.value, str) + lang = lang.value + if not isinstance(lang, str): + continue lang = lang.lower() if lang not in self.coredata.compilers[for_machine]: self.environment.detect_compiler_for(lang, for_machine) - def func_dependency(self, node, args, kwargs): + def func_dependency(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> None: args = self.flatten_args(args) kwargs = self.flatten_kwargs(kwargs) if not args: @@ -145,7 +160,6 @@ class IntrospectionInterpreter(AstInterpreter): version = kwargs.get('version', []) if not isinstance(version, list): version = [version] - condition_level = node.condition_level if hasattr(node, 'condition_level') else 0 if isinstance(required, ElementaryNode): required = required.value if not isinstance(required, bool): @@ -155,24 +169,24 @@ class IntrospectionInterpreter(AstInterpreter): 'required': required, 'version': version, 'has_fallback': has_fallback, - 'conditional': condition_level > 0, - 'node': node, + 'conditional': node.condition_level > 0, + 'node': node }] - def build_target(self, node, args, kwargs, targetclass): + def build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs_raw: T.Dict[str, TYPE_nvar], targetclass) -> T.Optional[T.Dict[str, T.Any]]: args = self.flatten_args(args) if not args or not isinstance(args[0], str): - return + return None name = args[0] srcqueue = [node] # Process the sources BEFORE flattening the kwargs, to preserve the original nodes - if 'sources' in kwargs: - srcqueue += mesonlib.listify(kwargs['sources']) + if 'sources' in kwargs_raw: + srcqueue += mesonlib.listify(kwargs_raw['sources']) - kwargs = self.flatten_kwargs(kwargs, True) + kwargs = self.flatten_kwargs(kwargs_raw, True) - source_nodes = [] + source_nodes = [] # type: T.List[BaseNode] while srcqueue: curr = srcqueue.pop(0) arg_node = None @@ -183,9 +197,10 @@ class IntrospectionInterpreter(AstInterpreter): arg_node = curr.args elif isinstance(curr, IdNode): # Try to resolve the ID and append the node to the queue + assert isinstance(curr.value, str) var_name = curr.value - if var_name in self.assignments and self.assignments[var_name]: - tmp_node = self.assignments[var_name][0] + if var_name in self.assignments: + tmp_node = self.assignments[var_name] if isinstance(tmp_node, (ArrayNode, IdNode, FunctionNode)): srcqueue += [tmp_node] elif isinstance(curr, ArithmeticNode): @@ -206,8 +221,9 @@ class IntrospectionInterpreter(AstInterpreter): kwargs_reduced = {k: v.value if isinstance(v, ElementaryNode) else v for k, v in kwargs_reduced.items()} kwargs_reduced = {k: v for k, v in kwargs_reduced.items() if not isinstance(v, BaseNode)} for_machine = MachineChoice.HOST - objects = [] - empty_sources = [] # Passing the unresolved sources list causes errors + objects = [] # type: T.List[T.Any] + empty_sources = [] # type: T.List[T.Any] + # Passing the unresolved sources list causes errors target = targetclass(name, self.subdir, self.subproject, for_machine, empty_sources, objects, self.environment, kwargs_reduced) new_target = { @@ -227,7 +243,7 @@ class IntrospectionInterpreter(AstInterpreter): self.targets += [new_target] return new_target - def build_library(self, node, args, kwargs): + def build_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: default_library = self.coredata.get_builtin_option('default_library') if default_library == 'shared': return self.build_target(node, args, kwargs, SharedLibrary) @@ -235,31 +251,32 @@ class IntrospectionInterpreter(AstInterpreter): return self.build_target(node, args, kwargs, StaticLibrary) elif default_library == 'both': return self.build_target(node, args, kwargs, SharedLibrary) + return None - def func_executable(self, node, args, kwargs): + def func_executable(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, Executable) - def func_static_lib(self, node, args, kwargs): + def func_static_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, StaticLibrary) - def func_shared_lib(self, node, args, kwargs): + def func_shared_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, SharedLibrary) - def func_both_lib(self, node, args, kwargs): + def func_both_lib(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, SharedLibrary) - def func_shared_module(self, node, args, kwargs): + def func_shared_module(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, SharedModule) - def func_library(self, node, args, kwargs): + def func_library(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_library(node, args, kwargs) - def func_jar(self, node, args, kwargs): + def func_jar(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: return self.build_target(node, args, kwargs, Jar) - def func_build_target(self, node, args, kwargs): + def func_build_target(self, node: BaseNode, args: T.List[TYPE_nvar], kwargs: T.Dict[str, TYPE_nvar]) -> T.Optional[T.Dict[str, T.Any]]: if 'target_type' not in kwargs: - return + return None target_type = kwargs.pop('target_type') if isinstance(target_type, ElementaryNode): target_type = target_type.value @@ -275,11 +292,12 @@ class IntrospectionInterpreter(AstInterpreter): return self.build_library(node, args, kwargs) elif target_type == 'jar': return self.build_target(node, args, kwargs, Jar) + return None - def is_subproject(self): + def is_subproject(self) -> bool: return self.subproject != '' - def analyze(self): + def analyze(self) -> None: self.load_root_meson_file() self.sanity_check_ast() self.parse_project() diff --git a/mesonbuild/ast/postprocess.py b/mesonbuild/ast/postprocess.py index 8e8732f3e..35fe1d317 100644 --- a/mesonbuild/ast/postprocess.py +++ b/mesonbuild/ast/postprocess.py @@ -17,48 +17,49 @@ from . import AstVisitor from .. import mparser +import typing as T class AstIndentationGenerator(AstVisitor): def __init__(self): self.level = 0 - def visit_default_func(self, node: mparser.BaseNode): + def visit_default_func(self, node: mparser.BaseNode) -> None: # Store the current level in the node node.level = self.level - def visit_ArrayNode(self, node: mparser.ArrayNode): + def visit_ArrayNode(self, node: mparser.ArrayNode) -> None: self.visit_default_func(node) self.level += 1 node.args.accept(self) self.level -= 1 - def visit_DictNode(self, node: mparser.DictNode): + def visit_DictNode(self, node: mparser.DictNode) -> None: self.visit_default_func(node) self.level += 1 node.args.accept(self) self.level -= 1 - def visit_MethodNode(self, node: mparser.MethodNode): + def visit_MethodNode(self, node: mparser.MethodNode) -> None: self.visit_default_func(node) node.source_object.accept(self) self.level += 1 node.args.accept(self) self.level -= 1 - def visit_FunctionNode(self, node: mparser.FunctionNode): + def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: self.visit_default_func(node) self.level += 1 node.args.accept(self) self.level -= 1 - def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode): + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: self.visit_default_func(node) self.level += 1 node.items.accept(self) node.block.accept(self) self.level -= 1 - def visit_IfClauseNode(self, node: mparser.IfClauseNode): + def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: self.visit_default_func(node) for i in node.ifs: i.accept(self) @@ -67,7 +68,7 @@ class AstIndentationGenerator(AstVisitor): node.elseblock.accept(self) self.level -= 1 - def visit_IfNode(self, node: mparser.IfNode): + def visit_IfNode(self, node: mparser.IfNode) -> None: self.visit_default_func(node) self.level += 1 node.condition.accept(self) @@ -76,9 +77,9 @@ class AstIndentationGenerator(AstVisitor): class AstIDGenerator(AstVisitor): def __init__(self): - self.counter = {} + self.counter = {} # type: T.Dict[str, int] - def visit_default_func(self, node: mparser.BaseNode): + def visit_default_func(self, node: mparser.BaseNode) -> None: name = type(node).__name__ if name not in self.counter: self.counter[name] = 0 @@ -89,17 +90,17 @@ class AstConditionLevel(AstVisitor): def __init__(self): self.condition_level = 0 - def visit_default_func(self, node: mparser.BaseNode): + def visit_default_func(self, node: mparser.BaseNode) -> None: node.condition_level = self.condition_level - def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode): + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: self.visit_default_func(node) self.condition_level += 1 node.items.accept(self) node.block.accept(self) self.condition_level -= 1 - def visit_IfClauseNode(self, node: mparser.IfClauseNode): + def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: self.visit_default_func(node) for i in node.ifs: i.accept(self) @@ -108,7 +109,7 @@ class AstConditionLevel(AstVisitor): node.elseblock.accept(self) self.condition_level -= 1 - def visit_IfNode(self, node: mparser.IfNode): + def visit_IfNode(self, node: mparser.IfNode) -> None: self.visit_default_func(node) self.condition_level += 1 node.condition.accept(self) diff --git a/mesonbuild/ast/printer.py b/mesonbuild/ast/printer.py index f245a3629..39e2ccae4 100644 --- a/mesonbuild/ast/printer.py +++ b/mesonbuild/ast/printer.py @@ -36,114 +36,111 @@ class AstPrinter(AstVisitor): self.is_newline = True self.last_level = 0 - def post_process(self): + def post_process(self) -> None: self.result = re.sub(r'\s+\n', '\n', self.result) - def append(self, data: str, node: mparser.BaseNode): - level = 0 - if node and hasattr(node, 'level'): - level = node.level - else: - level = self.last_level - self.last_level = level + def append(self, data: str, node: mparser.BaseNode) -> None: + self.last_level = node.level if self.is_newline: - self.result += ' ' * (level * self.indent) + self.result += ' ' * (node.level * self.indent) self.result += data self.is_newline = False - def append_padded(self, data: str, node: mparser.BaseNode): + def append_padded(self, data: str, node: mparser.BaseNode) -> None: if self.result[-1] not in [' ', '\n']: data = ' ' + data self.append(data + ' ', node) - def newline(self): + def newline(self) -> None: self.result += '\n' self.is_newline = True - def visit_BooleanNode(self, node: mparser.BooleanNode): + def visit_BooleanNode(self, node: mparser.BooleanNode) -> None: self.append('true' if node.value else 'false', node) - def visit_IdNode(self, node: mparser.IdNode): + def visit_IdNode(self, node: mparser.IdNode) -> None: + assert isinstance(node.value, str) self.append(node.value, node) - def visit_NumberNode(self, node: mparser.NumberNode): + def visit_NumberNode(self, node: mparser.NumberNode) -> None: self.append(str(node.value), node) - def visit_StringNode(self, node: mparser.StringNode): + def visit_StringNode(self, node: mparser.StringNode) -> None: + assert isinstance(node.value, str) self.append("'" + node.value + "'", node) - def visit_ContinueNode(self, node: mparser.ContinueNode): + def visit_ContinueNode(self, node: mparser.ContinueNode) -> None: self.append('continue', node) - def visit_BreakNode(self, node: mparser.BreakNode): + def visit_BreakNode(self, node: mparser.BreakNode) -> None: self.append('break', node) - def visit_ArrayNode(self, node: mparser.ArrayNode): + def visit_ArrayNode(self, node: mparser.ArrayNode) -> None: self.append('[', node) node.args.accept(self) self.append(']', node) - def visit_DictNode(self, node: mparser.DictNode): + def visit_DictNode(self, node: mparser.DictNode) -> None: self.append('{', node) node.args.accept(self) self.append('}', node) - def visit_OrNode(self, node: mparser.OrNode): + def visit_OrNode(self, node: mparser.OrNode) -> None: node.left.accept(self) self.append_padded('or', node) node.right.accept(self) - def visit_AndNode(self, node: mparser.AndNode): + def visit_AndNode(self, node: mparser.AndNode) -> None: node.left.accept(self) self.append_padded('and', node) node.right.accept(self) - def visit_ComparisonNode(self, node: mparser.ComparisonNode): + def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None: node.left.accept(self) self.append_padded(node.ctype, node) node.right.accept(self) - def visit_ArithmeticNode(self, node: mparser.ArithmeticNode): + def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None: node.left.accept(self) self.append_padded(arithmic_map[node.operation], node) node.right.accept(self) - def visit_NotNode(self, node: mparser.NotNode): + def visit_NotNode(self, node: mparser.NotNode) -> None: self.append_padded('not', node) node.value.accept(self) - def visit_CodeBlockNode(self, node: mparser.CodeBlockNode): + def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None: for i in node.lines: i.accept(self) self.newline() - def visit_IndexNode(self, node: mparser.IndexNode): + def visit_IndexNode(self, node: mparser.IndexNode) -> None: node.iobject.accept(self) self.append('[', node) node.index.accept(self) self.append(']', node) - def visit_MethodNode(self, node: mparser.MethodNode): + def visit_MethodNode(self, node: mparser.MethodNode) -> None: node.source_object.accept(self) self.append('.' + node.name + '(', node) node.args.accept(self) self.append(')', node) - def visit_FunctionNode(self, node: mparser.FunctionNode): + def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: self.append(node.func_name + '(', node) node.args.accept(self) self.append(')', node) - def visit_AssignmentNode(self, node: mparser.AssignmentNode): + def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: self.append(node.var_name + ' = ', node) node.value.accept(self) - def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode): + def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: self.append(node.var_name + ' += ', node) node.value.accept(self) - def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode): - varnames = [x.value for x in node.varnames] + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: + varnames = [x for x in node.varnames] self.append_padded('foreach', node) self.append_padded(', '.join(varnames), node) self.append_padded(':', node) @@ -152,7 +149,7 @@ class AstPrinter(AstVisitor): node.block.accept(self) self.append('endforeach', node) - def visit_IfClauseNode(self, node: mparser.IfClauseNode): + def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: prefix = '' for i in node.ifs: self.append_padded(prefix + 'if', node) @@ -163,23 +160,23 @@ class AstPrinter(AstVisitor): node.elseblock.accept(self) self.append('endif', node) - def visit_UMinusNode(self, node: mparser.UMinusNode): + def visit_UMinusNode(self, node: mparser.UMinusNode) -> None: self.append_padded('-', node) node.value.accept(self) - def visit_IfNode(self, node: mparser.IfNode): + def visit_IfNode(self, node: mparser.IfNode) -> None: node.condition.accept(self) self.newline() node.block.accept(self) - def visit_TernaryNode(self, node: mparser.TernaryNode): + def visit_TernaryNode(self, node: mparser.TernaryNode) -> None: node.condition.accept(self) self.append_padded('?', node) node.trueblock.accept(self) self.append_padded(':', node) node.falseblock.accept(self) - def visit_ArgumentNode(self, node: mparser.ArgumentNode): + def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None: break_args = (len(node.arguments) + len(node.kwargs)) > self.arg_newline_cutoff for i in node.arguments + list(node.kwargs.values()): if not isinstance(i, (mparser.ElementaryNode, mparser.IndexNode)): @@ -192,10 +189,7 @@ class AstPrinter(AstVisitor): if break_args: self.newline() for key, val in node.kwargs.items(): - if isinstance(key, str): - self.append(key, node) - else: - key.accept(self) + key.accept(self) self.append_padded(':', node) val.accept(self) self.append(', ', node) diff --git a/mesonbuild/ast/visitor.py b/mesonbuild/ast/visitor.py index de13daeac..37be4632a 100644 --- a/mesonbuild/ast/visitor.py +++ b/mesonbuild/ast/visitor.py @@ -21,119 +21,120 @@ class AstVisitor: def __init__(self): pass - def visit_default_func(self, node: mparser.BaseNode): + def visit_default_func(self, node: mparser.BaseNode) -> None: pass - def visit_BooleanNode(self, node: mparser.BooleanNode): + def visit_BooleanNode(self, node: mparser.BooleanNode) -> None: self.visit_default_func(node) - def visit_IdNode(self, node: mparser.IdNode): + def visit_IdNode(self, node: mparser.IdNode) -> None: self.visit_default_func(node) - def visit_NumberNode(self, node: mparser.NumberNode): + def visit_NumberNode(self, node: mparser.NumberNode) -> None: self.visit_default_func(node) - def visit_StringNode(self, node: mparser.StringNode): + def visit_StringNode(self, node: mparser.StringNode) -> None: self.visit_default_func(node) - def visit_ContinueNode(self, node: mparser.ContinueNode): + def visit_ContinueNode(self, node: mparser.ContinueNode) -> None: self.visit_default_func(node) - def visit_BreakNode(self, node: mparser.BreakNode): + def visit_BreakNode(self, node: mparser.BreakNode) -> None: self.visit_default_func(node) - def visit_ArrayNode(self, node: mparser.ArrayNode): + def visit_ArrayNode(self, node: mparser.ArrayNode) -> None: self.visit_default_func(node) node.args.accept(self) - def visit_DictNode(self, node: mparser.DictNode): + def visit_DictNode(self, node: mparser.DictNode) -> None: self.visit_default_func(node) node.args.accept(self) - def visit_EmptyNode(self, node: mparser.EmptyNode): + def visit_EmptyNode(self, node: mparser.EmptyNode) -> None: self.visit_default_func(node) - def visit_OrNode(self, node: mparser.OrNode): + def visit_OrNode(self, node: mparser.OrNode) -> None: self.visit_default_func(node) node.left.accept(self) node.right.accept(self) - def visit_AndNode(self, node: mparser.AndNode): + def visit_AndNode(self, node: mparser.AndNode) -> None: self.visit_default_func(node) node.left.accept(self) node.right.accept(self) - def visit_ComparisonNode(self, node: mparser.ComparisonNode): + def visit_ComparisonNode(self, node: mparser.ComparisonNode) -> None: self.visit_default_func(node) node.left.accept(self) node.right.accept(self) - def visit_ArithmeticNode(self, node: mparser.ArithmeticNode): + def visit_ArithmeticNode(self, node: mparser.ArithmeticNode) -> None: self.visit_default_func(node) node.left.accept(self) node.right.accept(self) - def visit_NotNode(self, node: mparser.NotNode): + def visit_NotNode(self, node: mparser.NotNode) -> None: self.visit_default_func(node) node.value.accept(self) - def visit_CodeBlockNode(self, node: mparser.CodeBlockNode): + def visit_CodeBlockNode(self, node: mparser.CodeBlockNode) -> None: self.visit_default_func(node) for i in node.lines: i.accept(self) - def visit_IndexNode(self, node: mparser.IndexNode): + def visit_IndexNode(self, node: mparser.IndexNode) -> None: self.visit_default_func(node) node.iobject.accept(self) node.index.accept(self) - def visit_MethodNode(self, node: mparser.MethodNode): + def visit_MethodNode(self, node: mparser.MethodNode) -> None: self.visit_default_func(node) node.source_object.accept(self) node.args.accept(self) - def visit_FunctionNode(self, node: mparser.FunctionNode): + def visit_FunctionNode(self, node: mparser.FunctionNode) -> None: self.visit_default_func(node) node.args.accept(self) - def visit_AssignmentNode(self, node: mparser.AssignmentNode): + def visit_AssignmentNode(self, node: mparser.AssignmentNode) -> None: self.visit_default_func(node) node.value.accept(self) - def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode): + def visit_PlusAssignmentNode(self, node: mparser.PlusAssignmentNode) -> None: self.visit_default_func(node) node.value.accept(self) - def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode): + def visit_ForeachClauseNode(self, node: mparser.ForeachClauseNode) -> None: self.visit_default_func(node) node.items.accept(self) node.block.accept(self) - def visit_IfClauseNode(self, node: mparser.IfClauseNode): + def visit_IfClauseNode(self, node: mparser.IfClauseNode) -> None: self.visit_default_func(node) for i in node.ifs: i.accept(self) if node.elseblock: node.elseblock.accept(self) - def visit_UMinusNode(self, node: mparser.UMinusNode): + def visit_UMinusNode(self, node: mparser.UMinusNode) -> None: self.visit_default_func(node) node.value.accept(self) - def visit_IfNode(self, node: mparser.IfNode): + def visit_IfNode(self, node: mparser.IfNode) -> None: self.visit_default_func(node) node.condition.accept(self) node.block.accept(self) - def visit_TernaryNode(self, node: mparser.TernaryNode): + def visit_TernaryNode(self, node: mparser.TernaryNode) -> None: self.visit_default_func(node) node.condition.accept(self) node.trueblock.accept(self) node.falseblock.accept(self) - def visit_ArgumentNode(self, node: mparser.ArgumentNode): + def visit_ArgumentNode(self, node: mparser.ArgumentNode) -> None: self.visit_default_func(node) for i in node.arguments: i.accept(self) - for val in node.kwargs.values(): + for key, val in node.kwargs.items(): + key.accept(self) val.accept(self) diff --git a/mesonbuild/cmake/interpreter.py b/mesonbuild/cmake/interpreter.py index 941baeddf..efdb840b6 100644 --- a/mesonbuild/cmake/interpreter.py +++ b/mesonbuild/cmake/interpreter.py @@ -978,7 +978,7 @@ class CMakeInterpreter: if isinstance(value, str): return string(value) elif isinstance(value, bool): - return BooleanNode(token(), value) + return BooleanNode(token(val=value)) elif isinstance(value, int): return number(value) elif isinstance(value, list): @@ -1002,7 +1002,7 @@ class CMakeInterpreter: if not isinstance(args, list): args = [args] args_n.arguments = [nodeify(x) for x in args if x is not None] - args_n.kwargs = {k: nodeify(v) for k, v in kwargs.items() if v is not None} + args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None} func_n = FunctionNode(self.subdir, 0, 0, 0, 0, name, args_n) return func_n @@ -1013,7 +1013,7 @@ class CMakeInterpreter: if not isinstance(args, list): args = [args] args_n.arguments = [nodeify(x) for x in args if x is not None] - args_n.kwargs = {k: nodeify(v) for k, v in kwargs.items() if v is not None} + args_n.kwargs = {id_node(k): nodeify(v) for k, v in kwargs.items() if v is not None} return MethodNode(self.subdir, 0, 0, obj, name, args_n) def assign(var_name: str, value: BaseNode) -> AssignmentNode: diff --git a/mesonbuild/interpreter.py b/mesonbuild/interpreter.py index 2769c2d04..dad293358 100644 --- a/mesonbuild/interpreter.py +++ b/mesonbuild/interpreter.py @@ -2182,13 +2182,12 @@ class Interpreter(InterpreterBase): def __init__(self, build, backend=None, subproject='', subdir='', subproject_dir='subprojects', modules = None, default_project_options=None, mock=False, ast=None): - super().__init__(build.environment.get_source_dir(), subdir) + super().__init__(build.environment.get_source_dir(), subdir, subproject) self.an_unpicklable_object = mesonlib.an_unpicklable_object self.build = build self.environment = build.environment self.coredata = self.environment.get_coredata() self.backend = backend - self.subproject = subproject self.summary = {} if modules is None: self.modules = {} @@ -4426,15 +4425,15 @@ Try setting b_lundef to false instead.'''.format(self.coredata.base_options['b_s ef = extract_as_list(kwargs, 'extra_files') kwargs['extra_files'] = self.source_strings_to_files(ef) self.check_sources_exist(os.path.join(self.source_root, self.subdir), sources) - if targetholder is ExecutableHolder: + if targetholder == ExecutableHolder: targetclass = build.Executable - elif targetholder is SharedLibraryHolder: + elif targetholder == SharedLibraryHolder: targetclass = build.SharedLibrary - elif targetholder is SharedModuleHolder: + elif targetholder == SharedModuleHolder: targetclass = build.SharedModule - elif targetholder is StaticLibraryHolder: + elif targetholder == StaticLibraryHolder: targetclass = build.StaticLibrary - elif targetholder is JarHolder: + elif targetholder == JarHolder: targetclass = build.Jar else: mlog.debug('Unknown target type:', str(targetholder)) @@ -4500,7 +4499,7 @@ This will become a hard error in the future.''', location=self.current_node) raise InterpreterException('Tried to add non-existing source file %s.' % s) # Only permit object extraction from the same subproject - def validate_extraction(self, buildtarget): + def validate_extraction(self, buildtarget: InterpreterObject) -> None: if not self.subdir.startswith(self.subproject_dir): if buildtarget.subdir.startswith(self.subproject_dir): raise InterpreterException('Tried to extract objects from a subproject target.') @@ -4510,19 +4509,6 @@ This will become a hard error in the future.''', location=self.current_node) if self.subdir.split('/')[1] != buildtarget.subdir.split('/')[1]: raise InterpreterException('Tried to extract objects from a different subproject.') - def check_contains(self, obj, args): - if len(args) != 1: - raise InterpreterException('Contains method takes exactly one argument.') - item = args[0] - for element in obj: - if isinstance(element, list): - found = self.check_contains(element, args) - if found: - return True - if element == item: - return True - return False - def is_subproject(self): return self.subproject != '' diff --git a/mesonbuild/interpreterbase.py b/mesonbuild/interpreterbase.py index d723da554..807113677 100644 --- a/mesonbuild/interpreterbase.py +++ b/mesonbuild/interpreterbase.py @@ -19,19 +19,43 @@ from . import mparser, mesonlib, mlog from . import environment, dependencies import os, copy, re +import collections.abc from functools import wraps +import typing as T -class ObjectHolder: - def __init__(self, obj, subproject=None): - self.held_object = obj - self.subproject = subproject +class InterpreterObject: + def __init__(self): + self.methods = {} # type: T.Dict[str, T.Callable] + # Current node set during a method call. This can be used as location + # when printing a warning message during a method call. + self.current_node = None # type: mparser.BaseNode + + def method_call(self, method_name: str, args: T.List[T.Union[mparser.BaseNode, str, int, float, bool, list, dict, 'InterpreterObject', 'ObjectHolder']], kwargs: T.Dict[str, T.Union[mparser.BaseNode, str, int, float, bool, list, dict, 'InterpreterObject', 'ObjectHolder']]): + if method_name in self.methods: + method = self.methods[method_name] + if not getattr(method, 'no-args-flattening', False): + args = flatten(args) + return method(args, kwargs) + raise InvalidCode('Unknown method "%s" in object.' % method_name) + +TV_InterpreterObject = T.TypeVar('TV_InterpreterObject') + +class ObjectHolder(T.Generic[TV_InterpreterObject]): + def __init__(self, obj: InterpreterObject, subproject: T.Optional[str] = None): + self.held_object = obj # type: InterpreterObject + self.subproject = subproject # type: str def __repr__(self): return ''.format(self.held_object) +TYPE_elementary = T.Union[str, int, float, bool] +TYPE_var = T.Union[TYPE_elementary, list, dict, InterpreterObject, ObjectHolder] +TYPE_nvar = T.Union[TYPE_var, mparser.BaseNode] +TYPE_nkwargs = T.Dict[T.Union[mparser.BaseNode, str], TYPE_nvar] + # Decorators for method calls. -def check_stringlist(a, msg='Arguments must be strings.'): +def check_stringlist(a: T.Any, msg: str = 'Arguments must be strings.') -> None: if not isinstance(a, list): mlog.debug('Not a list:', str(a)) raise InvalidArguments('Argument not a list.') @@ -39,7 +63,7 @@ def check_stringlist(a, msg='Arguments must be strings.'): mlog.debug('Element not a string:', str(a)) raise InvalidArguments(msg) -def _get_callee_args(wrapped_args, want_subproject=False): +def _get_callee_args(wrapped_args, want_subproject: bool = False): s = wrapped_args[0] n = len(wrapped_args) # Raise an error if the codepaths are not there @@ -100,12 +124,13 @@ def _get_callee_args(wrapped_args, want_subproject=False): kwargs = kwargs if kwargs is not None else {} return s, node, args, kwargs, subproject -def flatten(args): +def flatten(args: T.Union[TYPE_nvar, T.List[TYPE_nvar]]) -> T.List[TYPE_nvar]: if isinstance(args, mparser.StringNode): - return args.value - if isinstance(args, (int, str, mesonlib.File, InterpreterObject)): - return args - result = [] + assert isinstance(args.value, str) + return [args.value] + if not isinstance(args, collections.abc.Sequence): + return [args] + result = [] # type: T.List[TYPE_nvar] for a in args: if isinstance(a, list): rest = flatten(a) @@ -160,8 +185,8 @@ def disablerIfNotFound(f): class permittedKwargs: - def __init__(self, permitted): - self.permitted = permitted + def __init__(self, permitted: T.Set[str]): + self.permitted = permitted # type: T.Set[str] def __call__(self, f): @wraps(f) @@ -178,18 +203,23 @@ class permittedKwargs: class FeatureCheckBase: "Base class for feature version checks" - def __init__(self, feature_name, version): - self.feature_name = feature_name - self.feature_version = version + # Class variable, shared across all instances + # + # Format: {subproject: {feature_version: set(feature_names)}} + feature_registry = {} # type: T.ClassVar[T.Dict[str, T.Dict[str, T.Set[str]]]] + + def __init__(self, feature_name: str, version: str): + self.feature_name = feature_name # type: str + self.feature_version = version # type: str @staticmethod - def get_target_version(subproject): + def get_target_version(subproject: str) -> str: # Don't do any checks if project() has not been parsed yet if subproject not in mesonlib.project_meson_versions: return '' return mesonlib.project_meson_versions[subproject] - def use(self, subproject): + def use(self, subproject: str) -> None: tv = self.get_target_version(subproject) # No target version if tv == '': @@ -212,7 +242,7 @@ class FeatureCheckBase: self.log_usage_warning(tv) @classmethod - def report(cls, subproject): + def report(cls, subproject: str) -> None: if subproject not in cls.feature_registry: return warning_str = cls.get_warning_str_prefix(cls.get_target_version(subproject)) @@ -221,6 +251,13 @@ class FeatureCheckBase: warning_str += '\n * {}: {}'.format(version, fv[version]) mlog.warning(warning_str) + def log_usage_warning(self, tv: str) -> None: + raise InterpreterException('log_usage_warning not implemented') + + @staticmethod + def get_warning_str_prefix(tv: str) -> str: + raise InterpreterException('get_warning_str_prefix not implemented') + def __call__(self, f): @wraps(f) def wrapped(*wrapped_args, **wrapped_kwargs): @@ -233,38 +270,30 @@ class FeatureCheckBase: class FeatureNew(FeatureCheckBase): """Checks for new features""" - # Class variable, shared across all instances - # - # Format: {subproject: {feature_version: set(feature_names)}} - feature_registry = {} @staticmethod - def get_warning_str_prefix(tv): + def get_warning_str_prefix(tv: str) -> str: return 'Project specifies a minimum meson_version \'{}\' but uses features which were added in newer versions:'.format(tv) - def log_usage_warning(self, tv): + def log_usage_warning(self, tv: str) -> None: mlog.warning('Project targeting \'{}\' but tried to use feature introduced ' 'in \'{}\': {}'.format(tv, self.feature_version, self.feature_name)) class FeatureDeprecated(FeatureCheckBase): """Checks for deprecated features""" - # Class variable, shared across all instances - # - # Format: {subproject: {feature_version: set(feature_names)}} - feature_registry = {} @staticmethod - def get_warning_str_prefix(tv): + def get_warning_str_prefix(tv: str) -> str: return 'Deprecated features used:' - def log_usage_warning(self, tv): + def log_usage_warning(self, tv: str) -> None: mlog.deprecation('Project targeting \'{}\' but tried to use feature ' 'deprecated since \'{}\': {}' ''.format(tv, self.feature_version, self.feature_name)) class FeatureCheckKwargsBase: - def __init__(self, feature_name, feature_version, kwargs): + def __init__(self, feature_name: str, feature_version: str, kwargs: T.List[str]): self.feature_name = feature_name self.feature_version = feature_version self.kwargs = kwargs @@ -310,21 +339,6 @@ class ContinueRequest(BaseException): class BreakRequest(BaseException): pass -class InterpreterObject: - def __init__(self): - self.methods = {} - # Current node set during a method call. This can be used as location - # when printing a warning message during a method call. - self.current_node = None - - def method_call(self, method_name, args, kwargs): - if method_name in self.methods: - method = self.methods[method_name] - if not getattr(method, 'no-args-flattening', False): - args = flatten(args) - return method(args, kwargs) - raise InvalidCode('Unknown method "%s" in object.' % method_name) - class MutableInterpreterObject(InterpreterObject): def __init__(self): super().__init__() @@ -359,19 +373,22 @@ def is_disabled(args, kwargs) -> bool: return False class InterpreterBase: - def __init__(self, source_root, subdir): + elementary_types = (int, float, str, bool, list) + + def __init__(self, source_root: str, subdir: str, subproject: str): self.source_root = source_root - self.funcs = {} - self.builtin = {} + self.funcs = {} # type: T.Dict[str, T.Callable[[mparser.BaseNode, T.List[TYPE_nvar], T.Dict[str, TYPE_nvar]], TYPE_var]] + self.builtin = {} # type: T.Dict[str, InterpreterObject] self.subdir = subdir - self.variables = {} + self.subproject = subproject + self.variables = {} # type: T.Dict[str, TYPE_var] self.argument_depth = 0 self.current_lineno = -1 # Current node set during a function call. This can be used as location # when printing a warning message during a method call. - self.current_node = None + self.current_node = None # type: mparser.BaseNode - def load_root_meson_file(self): + def load_root_meson_file(self) -> None: mesonfile = os.path.join(self.source_root, self.subdir, environment.build_filename) if not os.path.isfile(mesonfile): raise InvalidArguments('Missing Meson file in %s' % mesonfile) @@ -386,17 +403,17 @@ class InterpreterBase: me.file = mesonfile raise me - def join_path_strings(self, args): + def join_path_strings(self, args: T.Sequence[str]) -> str: return os.path.join(*args).replace('\\', '/') - def parse_project(self): + def parse_project(self) -> None: """ Parses project() and initializes languages, compilers etc. Do this early because we need this before we parse the rest of the AST. """ self.evaluate_codeblock(self.ast, end=1) - def sanity_check_ast(self): + def sanity_check_ast(self) -> None: if not isinstance(self.ast, mparser.CodeBlockNode): raise InvalidCode('AST is of invalid type. Possibly a bug in the parser.') if not self.ast.lines: @@ -405,7 +422,7 @@ class InterpreterBase: if not isinstance(first, mparser.FunctionNode) or first.func_name != 'project': raise InvalidCode('First statement must be a call to project') - def run(self): + def run(self) -> None: # Evaluate everything after the first line, which is project() because # we already parsed that in self.parse_project() try: @@ -413,7 +430,7 @@ class InterpreterBase: except SubdirDoneRequest: pass - def evaluate_codeblock(self, node, start=0, end=None): + def evaluate_codeblock(self, node: mparser.CodeBlockNode, start: int = 0, end: T.Optional[int] = None) -> None: if node is None: return if not isinstance(node, mparser.CodeBlockNode): @@ -430,17 +447,18 @@ class InterpreterBase: self.evaluate_statement(cur) except Exception as e: if not hasattr(e, 'lineno'): - e.lineno = cur.lineno - e.colno = cur.colno - e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) + # We are doing the equivalent to setattr here and mypy does not like it + e.lineno = cur.lineno # type: ignore + e.colno = cur.colno # type: ignore + e.file = os.path.join(self.source_root, self.subdir, environment.build_filename) # type: ignore raise e i += 1 # In THE FUTURE jump over blocks and stuff. - def evaluate_statement(self, cur): + def evaluate_statement(self, cur: mparser.BaseNode) -> T.Optional[TYPE_var]: if isinstance(cur, mparser.FunctionNode): return self.function_call(cur) elif isinstance(cur, mparser.AssignmentNode): - return self.assignment(cur) + self.assignment(cur) elif isinstance(cur, mparser.MethodNode): return self.method_call(cur) elif isinstance(cur, mparser.StringNode): @@ -470,9 +488,9 @@ class InterpreterBase: elif isinstance(cur, mparser.ArithmeticNode): return self.evaluate_arithmeticstatement(cur) elif isinstance(cur, mparser.ForeachClauseNode): - return self.evaluate_foreach(cur) + self.evaluate_foreach(cur) elif isinstance(cur, mparser.PlusAssignmentNode): - return self.evaluate_plusassign(cur) + self.evaluate_plusassign(cur) elif isinstance(cur, mparser.IndexNode): return self.evaluate_indexing(cur) elif isinstance(cur, mparser.TernaryNode): @@ -481,75 +499,78 @@ class InterpreterBase: raise ContinueRequest() elif isinstance(cur, mparser.BreakNode): raise BreakRequest() - elif self.is_elementary_type(cur): + elif isinstance(cur, self.elementary_types): return cur else: raise InvalidCode("Unknown statement.") + return None - def evaluate_arraystatement(self, cur): + def evaluate_arraystatement(self, cur: mparser.ArrayNode) -> list: (arguments, kwargs) = self.reduce_arguments(cur.args) if len(kwargs) > 0: raise InvalidCode('Keyword arguments are invalid in array construction.') return arguments @FeatureNew('dict', '0.47.0') - def evaluate_dictstatement(self, cur): - (arguments, kwargs) = self.reduce_arguments(cur.args) + def evaluate_dictstatement(self, cur: mparser.DictNode) -> T.Dict[str, T.Any]: + (arguments, kwargs) = self.reduce_arguments(cur.args, resolve_key_nodes=False) assert (not arguments) - result = {} + result = {} # type: T.Dict[str, T.Any] self.argument_depth += 1 for key, value in kwargs.items(): if not isinstance(key, mparser.StringNode): FeatureNew('Dictionary entry using non literal key', '0.53.0').use(self.subproject) - key = self.evaluate_statement(key) - if not isinstance(key, str): + assert isinstance(key, mparser.BaseNode) # All keys must be nodes due to resolve_key_nodes=False + str_key = self.evaluate_statement(key) + if not isinstance(str_key, str): raise InvalidArguments('Key must be a string') - if key in result: - raise InvalidArguments('Duplicate dictionary key: {}'.format(key)) - result[key] = value + if str_key in result: + raise InvalidArguments('Duplicate dictionary key: {}'.format(str_key)) + result[str_key] = value self.argument_depth -= 1 return result - def evaluate_notstatement(self, cur): + def evaluate_notstatement(self, cur: mparser.NotNode) -> T.Union[bool, Disabler]: v = self.evaluate_statement(cur.value) - if is_disabler(v): + if isinstance(v, Disabler): return v if not isinstance(v, bool): raise InterpreterException('Argument to "not" is not a boolean.') return not v - def evaluate_if(self, node): + def evaluate_if(self, node: mparser.IfClauseNode) -> T.Optional[Disabler]: assert(isinstance(node, mparser.IfClauseNode)) for i in node.ifs: result = self.evaluate_statement(i.condition) - if is_disabler(result): + if isinstance(result, Disabler): return result if not(isinstance(result, bool)): raise InvalidCode('If clause {!r} does not evaluate to true or false.'.format(result)) if result: self.evaluate_codeblock(i.block) - return + return None if not isinstance(node.elseblock, mparser.EmptyNode): self.evaluate_codeblock(node.elseblock) + return None - def validate_comparison_types(self, val1, val2): + def validate_comparison_types(self, val1: T.Any, val2: T.Any) -> bool: if type(val1) != type(val2): return False return True - def evaluate_in(self, val1, val2): + def evaluate_in(self, val1: T.Any, val2: T.Any) -> bool: if not isinstance(val1, (str, int, float, ObjectHolder)): raise InvalidArguments('lvalue of "in" operator must be a string, integer, float, or object') if not isinstance(val2, (list, dict)): raise InvalidArguments('rvalue of "in" operator must be an array or a dict') return val1 in val2 - def evaluate_comparison(self, node): + def evaluate_comparison(self, node: mparser.ComparisonNode) -> T.Union[bool, Disabler]: val1 = self.evaluate_statement(node.left) - if is_disabler(val1): + if isinstance(val1, Disabler): return val1 val2 = self.evaluate_statement(node.right) - if is_disabler(val2): + if isinstance(val2, Disabler): return val2 if node.ctype == 'in': return self.evaluate_in(val1, val2) @@ -572,68 +593,70 @@ The result of this is undefined and will become a hard error in a future Meson r 'Values of different types ({}, {}) cannot be compared using {}.'.format(type(val1).__name__, type(val2).__name__, node.ctype)) - elif not self.is_elementary_type(val1): - raise InterpreterException('{} can only be compared for equality.'.format(node.left.value)) - elif not self.is_elementary_type(val2): - raise InterpreterException('{} can only be compared for equality.'.format(node.right.value)) + elif not isinstance(val1, self.elementary_types): + raise InterpreterException('{} can only be compared for equality.'.format(getattr(node.left, 'value', ''))) + elif not isinstance(val2, self.elementary_types): + raise InterpreterException('{} can only be compared for equality.'.format(getattr(node.right, 'value', ''))) + # Use type: ignore because mypy will complain that we are comparing two Unions, + # but we actually guarantee earlier that both types are the same elif node.ctype == '<': - return val1 < val2 + return val1 < val2 # type: ignore elif node.ctype == '<=': - return val1 <= val2 + return val1 <= val2 # type: ignore elif node.ctype == '>': - return val1 > val2 + return val1 > val2 # type: ignore elif node.ctype == '>=': - return val1 >= val2 + return val1 >= val2 # type: ignore else: raise InvalidCode('You broke my compare eval.') - def evaluate_andstatement(self, cur): + def evaluate_andstatement(self, cur: mparser.AndNode) -> T.Union[bool, Disabler]: l = self.evaluate_statement(cur.left) - if is_disabler(l): + if isinstance(l, Disabler): return l if not isinstance(l, bool): raise InterpreterException('First argument to "and" is not a boolean.') if not l: return False r = self.evaluate_statement(cur.right) - if is_disabler(r): + if isinstance(r, Disabler): return r if not isinstance(r, bool): raise InterpreterException('Second argument to "and" is not a boolean.') return r - def evaluate_orstatement(self, cur): + def evaluate_orstatement(self, cur: mparser.OrNode) -> T.Union[bool, Disabler]: l = self.evaluate_statement(cur.left) - if is_disabler(l): + if isinstance(l, Disabler): return l if not isinstance(l, bool): raise InterpreterException('First argument to "or" is not a boolean.') if l: return True r = self.evaluate_statement(cur.right) - if is_disabler(r): + if isinstance(r, Disabler): return r if not isinstance(r, bool): raise InterpreterException('Second argument to "or" is not a boolean.') return r - def evaluate_uminusstatement(self, cur): + def evaluate_uminusstatement(self, cur) -> T.Union[int, Disabler]: v = self.evaluate_statement(cur.value) - if is_disabler(v): + if isinstance(v, Disabler): return v if not isinstance(v, int): raise InterpreterException('Argument to negation is not an integer.') return -v @FeatureNew('/ with string arguments', '0.49.0') - def evaluate_path_join(self, l, r): + def evaluate_path_join(self, l: str, r: str) -> str: if not isinstance(l, str): raise InvalidCode('The division operator can only append to a string.') if not isinstance(r, str): raise InvalidCode('The division operator can only append a string.') return self.join_path_strings((l, r)) - def evaluate_division(self, l, r): + def evaluate_division(self, l: T.Any, r: T.Any) -> T.Union[int, str]: if isinstance(l, str) or isinstance(r, str): return self.evaluate_path_join(l, r) if isinstance(l, int) and isinstance(r, int): @@ -642,19 +665,20 @@ The result of this is undefined and will become a hard error in a future Meson r return l // r raise InvalidCode('Division works only with strings or integers.') - def evaluate_arithmeticstatement(self, cur): + def evaluate_arithmeticstatement(self, cur: mparser.ArithmeticNode) -> T.Union[int, str, dict, list, Disabler]: l = self.evaluate_statement(cur.left) - if is_disabler(l): + if isinstance(l, Disabler): return l r = self.evaluate_statement(cur.right) - if is_disabler(r): + if isinstance(r, Disabler): return r if cur.operation == 'add': if isinstance(l, dict) and isinstance(r, dict): return {**l, **r} try: - return l + r + # MyPy error due to handling two Unions (we are catching all exceptions anyway) + return l + r # type: ignore except Exception as e: raise InvalidCode('Invalid use of addition: ' + str(e)) elif cur.operation == 'sub': @@ -674,10 +698,10 @@ The result of this is undefined and will become a hard error in a future Meson r else: raise InvalidCode('You broke me.') - def evaluate_ternary(self, node): + def evaluate_ternary(self, node: mparser.TernaryNode) -> TYPE_var: assert(isinstance(node, mparser.TernaryNode)) result = self.evaluate_statement(node.condition) - if is_disabler(result): + if isinstance(result, Disabler): return result if not isinstance(result, bool): raise InterpreterException('Ternary condition is not boolean.') @@ -686,14 +710,14 @@ The result of this is undefined and will become a hard error in a future Meson r else: return self.evaluate_statement(node.falseblock) - def evaluate_foreach(self, node): + def evaluate_foreach(self, node: mparser.ForeachClauseNode) -> None: assert(isinstance(node, mparser.ForeachClauseNode)) items = self.evaluate_statement(node.items) if isinstance(items, list): if len(node.varnames) != 1: raise InvalidArguments('Foreach on array does not unpack') - varname = node.varnames[0].value + varname = node.varnames[0] for item in items: self.set_variable(varname, item) try: @@ -706,8 +730,8 @@ The result of this is undefined and will become a hard error in a future Meson r if len(node.varnames) != 2: raise InvalidArguments('Foreach on dict unpacks key and value') for key, value in items.items(): - self.set_variable(node.varnames[0].value, key) - self.set_variable(node.varnames[1].value, value) + self.set_variable(node.varnames[0], key) + self.set_variable(node.varnames[1], value) try: self.evaluate_codeblock(node.block) except ContinueRequest: @@ -717,7 +741,7 @@ The result of this is undefined and will become a hard error in a future Meson r else: raise InvalidArguments('Items of foreach loop must be an array or a dict') - def evaluate_plusassign(self, node): + def evaluate_plusassign(self, node: mparser.PlusAssignmentNode) -> None: assert(isinstance(node, mparser.PlusAssignmentNode)) varname = node.var_name addition = self.evaluate_statement(node.value) @@ -727,6 +751,7 @@ The result of this is undefined and will become a hard error in a future Meson r # Remember that all variables are immutable. We must always create a # full new variable and then assign it. old_variable = self.get_variable(varname) + new_value = None # type: T.Union[str, int, float, bool, dict, list] if isinstance(old_variable, str): if not isinstance(addition, str): raise InvalidArguments('The += operator requires a string on the right hand side if the variable on the left is a string') @@ -749,10 +774,10 @@ The result of this is undefined and will become a hard error in a future Meson r raise InvalidArguments('The += operator currently only works with arrays, dicts, strings or ints ') self.set_variable(varname, new_value) - def evaluate_indexing(self, node): + def evaluate_indexing(self, node: mparser.IndexNode) -> TYPE_var: assert(isinstance(node, mparser.IndexNode)) iobject = self.evaluate_statement(node.iobject) - if is_disabler(iobject): + if isinstance(iobject, Disabler): return iobject if not hasattr(iobject, '__getitem__'): raise InterpreterException( @@ -770,26 +795,32 @@ The result of this is undefined and will become a hard error in a future Meson r if not isinstance(index, int): raise InterpreterException('Index value is not an integer.') try: - return iobject[index] + # Ignore the MyPy error, since we don't know all indexable types here + # and we handle non indexable types with an exception + # TODO maybe find a better solution + return iobject[index] # type: ignore except IndexError: - raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) + # We are already checking for the existance of __getitem__, so this should be save + raise InterpreterException('Index %d out of bounds of array of size %d.' % (index, len(iobject))) # type: ignore - def function_call(self, node): + def function_call(self, node: mparser.FunctionNode) -> T.Optional[TYPE_var]: func_name = node.func_name (posargs, kwargs) = self.reduce_arguments(node.args) if is_disabled(posargs, kwargs) and func_name != 'set_variable' and func_name != 'is_disabler': return Disabler() if func_name in self.funcs: func = self.funcs[func_name] + func_args = posargs # type: T.Any if not getattr(func, 'no-args-flattening', False): - posargs = flatten(posargs) + func_args = flatten(posargs) self.current_node = node - return func(node, posargs, kwargs) + return func(node, func_args, self.kwargs_string_keys(kwargs)) else: self.unknown_function_called(func_name) + return None - def method_call(self, node): + def method_call(self, node: mparser.MethodNode) -> TYPE_var: invokable = node.source_object if isinstance(invokable, mparser.IdNode): object_name = invokable.value @@ -797,7 +828,9 @@ The result of this is undefined and will become a hard error in a future Meson r else: obj = self.evaluate_statement(invokable) method_name = node.name - args = node.args + (args, kwargs) = self.reduce_arguments(node.args) + if is_disabled(args, kwargs): + return Disabler() if isinstance(obj, str): return self.string_method_call(obj, method_name, args) if isinstance(obj, bool): @@ -812,7 +845,6 @@ The result of this is undefined and will become a hard error in a future Meson r raise InvalidArguments('File object "%s" is not callable.' % obj) if not isinstance(obj, InterpreterObject): raise InvalidArguments('Variable "%s" is not callable.' % object_name) - (args, kwargs) = self.reduce_arguments(args) # Special case. This is the only thing you can do with a disabler # object. Every other use immediately returns the disabler object. if isinstance(obj, Disabler): @@ -820,17 +852,14 @@ The result of this is undefined and will become a hard error in a future Meson r return False else: return Disabler() - if is_disabled(args, kwargs): - return Disabler() if method_name == 'extract_objects': + if not isinstance(obj, ObjectHolder): + raise InvalidArguments('Invalid operation "extract_objects" on variable "{}"'.format(object_name)) self.validate_extraction(obj.held_object) obj.current_node = node - return obj.method_call(method_name, args, kwargs) + return obj.method_call(method_name, args, self.kwargs_string_keys(kwargs)) - def bool_method_call(self, obj, method_name, args): - (posargs, kwargs) = self.reduce_arguments(args) - if is_disabled(posargs, kwargs): - return Disabler() + def bool_method_call(self, obj: bool, method_name: str, posargs: T.List[TYPE_nvar]) -> T.Union[str, int]: if method_name == 'to_string': if not posargs: if obj: @@ -852,10 +881,7 @@ The result of this is undefined and will become a hard error in a future Meson r else: raise InterpreterException('Unknown method "%s" for a boolean.' % method_name) - def int_method_call(self, obj, method_name, args): - (posargs, kwargs) = self.reduce_arguments(args) - if is_disabled(posargs, kwargs): - return Disabler() + def int_method_call(self, obj: int, method_name: str, posargs: T.List[TYPE_nvar]) -> T.Union[str, bool]: if method_name == 'is_even': if not posargs: return obj % 2 == 0 @@ -875,7 +901,7 @@ The result of this is undefined and will become a hard error in a future Meson r raise InterpreterException('Unknown method "%s" for an integer.' % method_name) @staticmethod - def _get_one_string_posarg(posargs, method_name): + def _get_one_string_posarg(posargs: T.List[TYPE_nvar], method_name: str) -> str: if len(posargs) > 1: m = '{}() must have zero or one arguments' raise InterpreterException(m.format(method_name)) @@ -887,17 +913,14 @@ The result of this is undefined and will become a hard error in a future Meson r return s return None - def string_method_call(self, obj, method_name, args): - (posargs, kwargs) = self.reduce_arguments(args) - if is_disabled(posargs, kwargs): - return Disabler() + def string_method_call(self, obj: str, method_name: str, posargs: T.List[TYPE_nvar]) -> T.Union[str, int, bool, T.List[str]]: if method_name == 'strip': - s = self._get_one_string_posarg(posargs, 'strip') - if s is not None: - return obj.strip(s) + s1 = self._get_one_string_posarg(posargs, 'strip') + if s1 is not None: + return obj.strip(s1) return obj.strip() elif method_name == 'format': - return self.format_string(obj, args) + return self.format_string(obj, posargs) elif method_name == 'to_upper': return obj.upper() elif method_name == 'to_lower': @@ -905,19 +928,19 @@ The result of this is undefined and will become a hard error in a future Meson r elif method_name == 'underscorify': return re.sub(r'[^a-zA-Z0-9]', '_', obj) elif method_name == 'split': - s = self._get_one_string_posarg(posargs, 'split') - if s is not None: - return obj.split(s) + s2 = self._get_one_string_posarg(posargs, 'split') + if s2 is not None: + return obj.split(s2) return obj.split() elif method_name == 'startswith' or method_name == 'contains' or method_name == 'endswith': - s = posargs[0] - if not isinstance(s, str): + s3 = posargs[0] + if not isinstance(s3, str): raise InterpreterException('Argument must be a string.') if method_name == 'startswith': - return obj.startswith(s) + return obj.startswith(s3) elif method_name == 'contains': - return obj.find(s) >= 0 - return obj.endswith(s) + return obj.find(s3) >= 0 + return obj.endswith(s3) elif method_name == 'to_int': try: return int(obj) @@ -928,6 +951,7 @@ The result of this is undefined and will become a hard error in a future Meson r raise InterpreterException('Join() takes exactly one argument.') strlist = posargs[0] check_stringlist(strlist) + assert isinstance(strlist, list) # Required for mypy return obj.join(strlist) elif method_name == 'version_compare': if len(posargs) != 1: @@ -938,12 +962,11 @@ The result of this is undefined and will become a hard error in a future Meson r return mesonlib.version_compare(obj, cmpr) raise InterpreterException('Unknown method "%s" for a string.' % method_name) - def format_string(self, templ, args): - if isinstance(args, mparser.ArgumentNode): - args = args.arguments + def format_string(self, templ: str, args: T.List[TYPE_nvar]) -> str: arg_strings = [] for arg in args: - arg = self.evaluate_statement(arg) + if isinstance(arg, mparser.BaseNode): + arg = self.evaluate_statement(arg) if isinstance(arg, bool): # Python boolean is upper case. arg = str(arg).lower() arg_strings.append(str(arg)) @@ -956,15 +979,24 @@ The result of this is undefined and will become a hard error in a future Meson r return re.sub(r'@(\d+)@', arg_replace, templ) - def unknown_function_called(self, func_name): + def unknown_function_called(self, func_name: str) -> None: raise InvalidCode('Unknown function "%s".' % func_name) - def array_method_call(self, obj, method_name, args): - (posargs, kwargs) = self.reduce_arguments(args) - if is_disabled(posargs, kwargs): - return Disabler() + def array_method_call(self, obj: list, method_name: str, posargs: T.List[TYPE_nvar]) -> TYPE_var: if method_name == 'contains': - return self.check_contains(obj, posargs) + def check_contains(el: list) -> bool: + if len(posargs) != 1: + raise InterpreterException('Contains method takes exactly one argument.') + item = posargs[0] + for element in el: + if isinstance(element, list): + found = check_contains(element) + if found: + return True + if element == item: + return True + return False + return check_contains(obj) elif method_name == 'length': return len(obj) elif method_name == 'get': @@ -983,16 +1015,14 @@ The result of this is undefined and will become a hard error in a future Meson r if fallback is None: m = 'Array index {!r} is out of bounds for array of size {!r}.' raise InvalidArguments(m.format(index, len(obj))) + if isinstance(fallback, mparser.BaseNode): + return self.evaluate_statement(fallback) return fallback return obj[index] m = 'Arrays do not have a method called {!r}.' raise InterpreterException(m.format(method_name)) - def dict_method_call(self, obj, method_name, args): - (posargs, kwargs) = self.reduce_arguments(args) - if is_disabled(posargs, kwargs): - return Disabler() - + def dict_method_call(self, obj: dict, method_name: str, posargs: T.List[TYPE_nvar]) -> TYPE_var: if method_name in ('has_key', 'get'): if method_name == 'has_key': if len(posargs) != 1: @@ -1014,7 +1044,10 @@ The result of this is undefined and will become a hard error in a future Meson r return obj[key] if len(posargs) == 2: - return posargs[1] + fallback = posargs[1] + if isinstance(fallback, mparser.BaseNode): + return self.evaluate_statement(fallback) + return fallback raise InterpreterException('Key {!r} is not in the dictionary.'.format(key)) @@ -1025,21 +1058,27 @@ The result of this is undefined and will become a hard error in a future Meson r raise InterpreterException('Dictionaries do not have a method called "%s".' % method_name) - def reduce_arguments(self, args): + def reduce_arguments(self, args: mparser.ArgumentNode, resolve_key_nodes: bool = True) -> T.Tuple[T.List[TYPE_nvar], TYPE_nkwargs]: assert(isinstance(args, mparser.ArgumentNode)) if args.incorrect_order(): raise InvalidArguments('All keyword arguments must be after positional arguments.') self.argument_depth += 1 - reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] - reduced_kw = {} - for key in args.kwargs.keys(): - a = args.kwargs[key] - reduced_kw[key] = self.evaluate_statement(a) + reduced_pos = [self.evaluate_statement(arg) for arg in args.arguments] # type: T.List[TYPE_nvar] + reduced_kw = {} # type: TYPE_nkwargs + for key, val in args.kwargs.items(): + reduced_key = key # type: T.Union[str, mparser.BaseNode] + reduced_val = val # type: TYPE_nvar + if resolve_key_nodes and isinstance(key, mparser.IdNode): + assert isinstance(key.value, str) + reduced_key = key.value + if isinstance(reduced_val, mparser.BaseNode): + reduced_val = self.evaluate_statement(reduced_val) + reduced_kw[reduced_key] = reduced_val self.argument_depth -= 1 final_kw = self.expand_default_kwargs(reduced_kw) return reduced_pos, final_kw - def expand_default_kwargs(self, kwargs): + def expand_default_kwargs(self, kwargs: TYPE_nkwargs) -> TYPE_nkwargs: if 'kwargs' not in kwargs: return kwargs to_expand = kwargs.pop('kwargs') @@ -1053,7 +1092,15 @@ The result of this is undefined and will become a hard error in a future Meson r kwargs[k] = v return kwargs - def assignment(self, node): + def kwargs_string_keys(self, kwargs: TYPE_nkwargs) -> T.Dict[str, TYPE_nvar]: + kw = {} # type: T.Dict[str, TYPE_nvar] + for key, val in kwargs.items(): + if not isinstance(key, str): + raise InterpreterException('Key of kwargs is not a string') + kw[key] = val + return kw + + def assignment(self, node: mparser.AssignmentNode) -> None: assert(isinstance(node, mparser.AssignmentNode)) if self.argument_depth != 0: raise InvalidArguments('''Tried to assign values inside an argument list. @@ -1070,7 +1117,7 @@ To specify a keyword argument, use : instead of =.''') self.set_variable(var_name, value) return None - def set_variable(self, varname, variable): + def set_variable(self, varname: str, variable: TYPE_var) -> None: if variable is None: raise InvalidCode('Can not assign None to variable.') if not isinstance(varname, str): @@ -1083,16 +1130,16 @@ To specify a keyword argument, use : instead of =.''') raise InvalidCode('Tried to overwrite internal variable "%s"' % varname) self.variables[varname] = variable - def get_variable(self, varname): + def get_variable(self, varname) -> TYPE_var: if varname in self.builtin: return self.builtin[varname] if varname in self.variables: return self.variables[varname] raise InvalidCode('Unknown variable "%s".' % varname) - def is_assignable(self, value): + def is_assignable(self, value: T.Any) -> bool: return isinstance(value, (InterpreterObject, dependencies.Dependency, str, int, list, dict, mesonlib.File)) - def is_elementary_type(self, v): - return isinstance(v, (int, float, str, bool, list)) + def validate_extraction(self, buildtarget: InterpreterObject) -> None: + raise InterpreterException('validate_extraction is not implemented in this context (please file a bug)') diff --git a/mesonbuild/mesonlib.py b/mesonbuild/mesonlib.py index 854c934e3..2298c861b 100644 --- a/mesonbuild/mesonlib.py +++ b/mesonbuild/mesonlib.py @@ -400,7 +400,7 @@ class PerThreeMachine(PerMachine[_T]): class PerMachineDefaultable(PerMachine[T.Optional[_T]]): """Extends `PerMachine` with the ability to default from `None`s. """ - def __init__(self) -> None: + def __init__(self): super().__init__(None, None) def default_missing(self) -> "PerMachine[T.Optional[_T]]": @@ -418,7 +418,7 @@ class PerMachineDefaultable(PerMachine[T.Optional[_T]]): class PerThreeMachineDefaultable(PerMachineDefaultable, PerThreeMachine[T.Optional[_T]]): """Extends `PerThreeMachine` with the ability to default from `None`s. """ - def __init__(self) -> None: + def __init__(self): PerThreeMachine.__init__(self, None, None, None) def default_missing(self) -> "PerThreeMachine[T.Optional[_T]]": diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index cfa457436..cca437225 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -25,7 +25,7 @@ from . import mesonlib from .ast import IntrospectionInterpreter, build_target_functions, AstConditionLevel, AstIDGenerator, AstIndentationGenerator from . import mlog from .backend import backends -from .mparser import FunctionNode, ArrayNode, ArgumentNode, StringNode +from .mparser import BaseNode, FunctionNode, ArrayNode, ArgumentNode, StringNode from .interpreter import Interpreter from pathlib import PurePath import typing as T @@ -110,7 +110,7 @@ def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[st for i in intr.targets: sources = [] # type: T.List[str] for n in i['sources']: - args = [] # type: T.List[T.Union[str, StringNode]] + args = [] # type: T.List[BaseNode] if isinstance(n, FunctionNode): args = list(n.args.arguments) if n.func_name in build_target_functions: @@ -121,6 +121,7 @@ def list_targets_from_source(intr: IntrospectionInterpreter) -> T.List[T.Dict[st args = n.arguments for j in args: if isinstance(j, StringNode): + assert isinstance(j.value, str) sources += [j.value] elif isinstance(j, str): sources += [j] diff --git a/mesonbuild/mparser.py b/mesonbuild/mparser.py index 430c89e73..8753b4038 100644 --- a/mesonbuild/mparser.py +++ b/mesonbuild/mparser.py @@ -14,10 +14,15 @@ import re import codecs +import textwrap import types +import typing as T from .mesonlib import MesonException from . import mlog +if T.TYPE_CHECKING: + from .ast import AstVisitor + # This is the regex for the supported escape sequences of a regular string # literal, like 'abc\x00' ESCAPE_SEQUENCE_SINGLE_RE = re.compile(r''' @@ -71,23 +76,25 @@ class BlockParseException(MesonException): self.lineno = lineno self.colno = colno -class Token: - def __init__(self, tid, filename, line_start, lineno, colno, bytespan, value): - self.tid = tid - self.filename = filename - self.line_start = line_start - self.lineno = lineno - self.colno = colno - self.bytespan = bytespan - self.value = value +TV_TokenTypes = T.TypeVar('TV_TokenTypes', int, str, bool) - def __eq__(self, other): +class Token(T.Generic[TV_TokenTypes]): + def __init__(self, tid: str, filename: str, line_start: int, lineno: int, colno: int, bytespan: T.Tuple[int, int], value: TV_TokenTypes): + self.tid = tid # type: str + self.filename = filename # type: str + self.line_start = line_start # type: int + self.lineno = lineno # type: int + self.colno = colno # type: int + self.bytespan = bytespan # type: T.Tuple[int, int] + self.value = value # type: TV_TokenTypes + + def __eq__(self, other) -> bool: if isinstance(other, str): return self.tid == other return self.tid == other.tid class Lexer: - def __init__(self, code): + def __init__(self, code: str): self.code = code self.keywords = {'true', 'false', 'if', 'else', 'elif', 'endif', 'and', 'or', 'not', 'foreach', 'endforeach', @@ -129,10 +136,10 @@ class Lexer: ('questionmark', re.compile(r'\?')), ] - def getline(self, line_start): + def getline(self, line_start: int) -> str: return self.code[line_start:self.code.find('\n', line_start)] - def lex(self, filename): + def lex(self, filename: str) -> T.Generator[Token, None, None]: line_start = 0 lineno = 1 loc = 0 @@ -142,7 +149,7 @@ class Lexer: col = 0 while loc < len(self.code): matched = False - value = None + value = None # type: T.Union[str, bool, int] for (tid, reg) in self.token_specification: mo = reg.match(self.code, loc) if mo: @@ -174,8 +181,14 @@ class Lexer: elif tid == 'string': # Handle here and not on the regexp to give a better error message. if match_text.find("\n") != -1: - mlog.warning("""Newline character in a string detected, use ''' (three single quotes) for multiline strings instead. -This will become a hard error in a future Meson release.""", self.getline(line_start), lineno, col) + mlog.warning(textwrap.dedent("""\ + Newline character in a string detected, use ''' (three single quotes) for multiline strings instead. + This will become a hard error in a future Meson release.\ + """), + self.getline(line_start), + str(lineno), + str(col) + ) value = match_text[1:-1] try: value = ESCAPE_SEQUENCE_SINGLE_RE.sub(decode_match, value) @@ -213,44 +226,53 @@ This will become a hard error in a future Meson release.""", self.getline(line_s raise ParseException('lexer', self.getline(line_start), lineno, col) class BaseNode: - def accept(self, visitor): + def __init__(self, lineno: int, colno: int, filename: str, end_lineno: T.Optional[int] = None, end_colno: T.Optional[int] = None): + self.lineno = lineno # type: int + self.colno = colno # type: int + self.filename = filename # type: str + self.end_lineno = end_lineno if end_lineno is not None else self.lineno + self.end_colno = end_colno if end_colno is not None else self.colno + + # Attributes for the visitors + self.level = 0 # type: int + self.ast_id = '' # type: str + self.condition_level = 0 # type: int + + def accept(self, visitor: 'AstVisitor') -> None: fname = 'visit_{}'.format(type(self).__name__) if hasattr(visitor, fname): func = getattr(visitor, fname) if callable(func): func(self) -class ElementaryNode(BaseNode): - def __init__(self, token): - self.lineno = token.lineno - self.filename = token.filename - self.colno = token.colno - self.value = token.value - self.bytespan = token.bytespan +class ElementaryNode(T.Generic[TV_TokenTypes], BaseNode): + def __init__(self, token: Token[TV_TokenTypes]): + super().__init__(token.lineno, token.colno, token.filename) + self.value = token.value # type: TV_TokenTypes + self.bytespan = token.bytespan # type: T.Tuple[int, int] -class BooleanNode(ElementaryNode): - def __init__(self, token, value): +class BooleanNode(ElementaryNode[bool]): + def __init__(self, token: Token[bool]): super().__init__(token) - self.value = value - assert(isinstance(self.value, bool)) + assert isinstance(self.value, bool) -class IdNode(ElementaryNode): - def __init__(self, token): +class IdNode(ElementaryNode[str]): + def __init__(self, token: Token[str]): super().__init__(token) - assert(isinstance(self.value, str)) + assert isinstance(self.value, str) def __str__(self): return "Id node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) -class NumberNode(ElementaryNode): - def __init__(self, token): +class NumberNode(ElementaryNode[int]): + def __init__(self, token: Token[int]): super().__init__(token) - assert(isinstance(self.value, int)) + assert isinstance(self.value, int) -class StringNode(ElementaryNode): - def __init__(self, token): +class StringNode(ElementaryNode[str]): + def __init__(self, token: Token[str]): super().__init__(token) - assert(isinstance(self.value, str)) + assert isinstance(self.value, str) def __str__(self): return "String node: '%s' (%d, %d)." % (self.value, self.lineno, self.colno) @@ -261,203 +283,163 @@ class ContinueNode(ElementaryNode): class BreakNode(ElementaryNode): pass +class ArgumentNode(BaseNode): + def __init__(self, token: Token[TV_TokenTypes]): + super().__init__(token.lineno, token.colno, token.filename) + self.arguments = [] # type: T.List[BaseNode] + self.commas = [] # type: T.List[Token[TV_TokenTypes]] + self.kwargs = {} # type: T.Dict[BaseNode, BaseNode] + self.order_error = False + + def prepend(self, statement: BaseNode) -> None: + if self.num_kwargs() > 0: + self.order_error = True + if not isinstance(statement, EmptyNode): + self.arguments = [statement] + self.arguments + + def append(self, statement: BaseNode) -> None: + if self.num_kwargs() > 0: + self.order_error = True + if not isinstance(statement, EmptyNode): + self.arguments += [statement] + + def set_kwarg(self, name: IdNode, value: BaseNode) -> None: + if name.value in [x.value for x in self.kwargs.keys() if isinstance(x, IdNode)]: + mlog.warning('Keyword argument "{}" defined multiple times.'.format(name.value), location=self) + mlog.warning('This will be an error in future Meson releases.') + self.kwargs[name] = value + + def set_kwarg_no_check(self, name: BaseNode, value: BaseNode) -> None: + self.kwargs[name] = value + + def num_args(self) -> int: + return len(self.arguments) + + def num_kwargs(self) -> int: + return len(self.kwargs) + + def incorrect_order(self) -> bool: + return self.order_error + + def __len__(self) -> int: + return self.num_args() # Fixme + class ArrayNode(BaseNode): - def __init__(self, args, lineno, colno, end_lineno, end_colno): - self.filename = args.filename - self.lineno = lineno - self.colno = colno - self.end_lineno = end_lineno - self.end_colno = end_colno - self.args = args + def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): + super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) + self.args = args # type: ArgumentNode class DictNode(BaseNode): - def __init__(self, args, lineno, colno, end_lineno, end_colno): - self.filename = args.filename - self.lineno = lineno - self.colno = colno - self.end_lineno = end_lineno - self.end_colno = end_colno + def __init__(self, args: ArgumentNode, lineno: int, colno: int, end_lineno: int, end_colno: int): + super().__init__(lineno, colno, args.filename, end_lineno=end_lineno, end_colno=end_colno) self.args = args class EmptyNode(BaseNode): - def __init__(self, lineno, colno): - self.filename = '' - self.lineno = lineno - self.colno = colno + def __init__(self, lineno: int, colno: int, filename: str): + super().__init__(lineno, colno, filename) self.value = None class OrNode(BaseNode): - def __init__(self, left, right): - self.filename = left.filename - self.lineno = left.lineno - self.colno = left.colno - self.left = left - self.right = right + def __init__(self, left: BaseNode, right: BaseNode): + super().__init__(left.lineno, left.colno, left.filename) + self.left = left # type: BaseNode + self.right = right # type: BaseNode class AndNode(BaseNode): - def __init__(self, left, right): - self.filename = left.filename - self.lineno = left.lineno - self.colno = left.colno - self.left = left - self.right = right + def __init__(self, left: BaseNode, right: BaseNode): + super().__init__(left.lineno, left.colno, left.filename) + self.left = left # type: BaseNode + self.right = right # type: BaseNode class ComparisonNode(BaseNode): - def __init__(self, ctype, left, right): - self.lineno = left.lineno - self.colno = left.colno - self.filename = left.filename - self.left = left - self.right = right - self.ctype = ctype + def __init__(self, ctype: str, left: BaseNode, right: BaseNode): + super().__init__(left.lineno, left.colno, left.filename) + self.left = left # type: BaseNode + self.right = right # type: BaseNode + self.ctype = ctype # type: str class ArithmeticNode(BaseNode): - def __init__(self, operation, left, right): - self.filename = left.filename - self.lineno = left.lineno - self.colno = left.colno - self.left = left - self.right = right - self.operation = operation + def __init__(self, operation: str, left: BaseNode, right: BaseNode): + super().__init__(left.lineno, left.colno, left.filename) + self.left = left # type: BaseNode + self.right = right # type: BaseNode + self.operation = operation # type: str class NotNode(BaseNode): - def __init__(self, location_node, value): - self.filename = location_node.filename - self.lineno = location_node.lineno - self.colno = location_node.colno - self.value = value + def __init__(self, token: Token[TV_TokenTypes], value: BaseNode): + super().__init__(token.lineno, token.colno, token.filename) + self.value = value # type: BaseNode class CodeBlockNode(BaseNode): - def __init__(self, location_node): - self.filename = location_node.filename - self.lineno = location_node.lineno - self.colno = location_node.colno - self.lines = [] + def __init__(self, token: Token[TV_TokenTypes]): + super().__init__(token.lineno, token.colno, token.filename) + self.lines = [] # type: T.List[BaseNode] class IndexNode(BaseNode): - def __init__(self, iobject, index): - self.iobject = iobject - self.index = index - self.filename = iobject.filename - self.lineno = iobject.lineno - self.colno = iobject.colno + def __init__(self, iobject: BaseNode, index: BaseNode): + super().__init__(iobject.lineno, iobject.colno, iobject.filename) + self.iobject = iobject # type: BaseNode + self.index = index # type: BaseNode class MethodNode(BaseNode): - def __init__(self, filename, lineno, colno, source_object, name, args): - self.filename = filename - self.lineno = lineno - self.colno = colno - self.source_object = source_object - self.name = name + def __init__(self, filename: str, lineno: int, colno: int, source_object: BaseNode, name: str, args: ArgumentNode): + super().__init__(lineno, colno, filename) + self.source_object = source_object # type: BaseNode + self.name = name # type: str assert(isinstance(self.name, str)) - self.args = args + self.args = args # type: ArgumentNode class FunctionNode(BaseNode): - def __init__(self, filename, lineno, colno, end_lineno, end_colno, func_name, args): - self.filename = filename - self.lineno = lineno - self.colno = colno - self.end_lineno = end_lineno - self.end_colno = end_colno - self.func_name = func_name + def __init__(self, filename: str, lineno: int, colno: int, end_lineno: int, end_colno: int, func_name: str, args: ArgumentNode): + super().__init__(lineno, colno, filename, end_lineno=end_lineno, end_colno=end_colno) + self.func_name = func_name # type: str assert(isinstance(func_name, str)) - self.args = args + self.args = args # type: ArgumentNode class AssignmentNode(BaseNode): - def __init__(self, filename, lineno, colno, var_name, value): - self.filename = filename - self.lineno = lineno - self.colno = colno - self.var_name = var_name + def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): + super().__init__(lineno, colno, filename) + self.var_name = var_name # type: str assert(isinstance(var_name, str)) - self.value = value + self.value = value # type: BaseNode class PlusAssignmentNode(BaseNode): - def __init__(self, filename, lineno, colno, var_name, value): - self.filename = filename - self.lineno = lineno - self.colno = colno - self.var_name = var_name + def __init__(self, filename: str, lineno: int, colno: int, var_name: str, value: BaseNode): + super().__init__(lineno, colno, filename) + self.var_name = var_name # type: str assert(isinstance(var_name, str)) - self.value = value + self.value = value # type: BaseNode class ForeachClauseNode(BaseNode): - def __init__(self, lineno, colno, varnames, items, block): - self.lineno = lineno - self.colno = colno - self.varnames = varnames - self.items = items - self.block = block + def __init__(self, token: Token, varnames: T.List[str], items: BaseNode, block: CodeBlockNode): + super().__init__(token.lineno, token.colno, token.filename) + self.varnames = varnames # type: T.List[str] + self.items = items # type: BaseNode + self.block = block # type: CodeBlockNode + +class IfNode(BaseNode): + def __init__(self, linenode: BaseNode, condition: BaseNode, block: CodeBlockNode): + super().__init__(linenode.lineno, linenode.colno, linenode.filename) + self.condition = condition # type: BaseNode + self.block = block # type: CodeBlockNode class IfClauseNode(BaseNode): - def __init__(self, lineno, colno): - self.lineno = lineno - self.colno = colno - self.ifs = [] - self.elseblock = EmptyNode(lineno, colno) + def __init__(self, linenode: BaseNode): + super().__init__(linenode.lineno, linenode.colno, linenode.filename) + self.ifs = [] # type: T.List[IfNode] + self.elseblock = EmptyNode(linenode.lineno, linenode.colno, linenode.filename) # type: T.Union[EmptyNode, CodeBlockNode] class UMinusNode(BaseNode): - def __init__(self, current_location, value): - self.filename = current_location.filename - self.lineno = current_location.lineno - self.colno = current_location.colno - self.value = value - -class IfNode(BaseNode): - def __init__(self, lineno, colno, condition, block): - self.lineno = lineno - self.colno = colno - self.condition = condition - self.block = block + def __init__(self, current_location: Token, value: BaseNode): + super().__init__(current_location.lineno, current_location.colno, current_location.filename) + self.value = value # type: BaseNode class TernaryNode(BaseNode): - def __init__(self, filename, lineno, colno, condition, trueblock, falseblock): - self.filename = filename - self.lineno = lineno - self.colno = colno - self.condition = condition - self.trueblock = trueblock - self.falseblock = falseblock - -class ArgumentNode(BaseNode): - def __init__(self, token): - self.lineno = token.lineno - self.colno = token.colno - self.filename = token.filename - self.arguments = [] - self.commas = [] - self.kwargs = {} - self.order_error = False - - def prepend(self, statement): - if self.num_kwargs() > 0: - self.order_error = True - if not isinstance(statement, EmptyNode): - self.arguments = [statement] + self.arguments - - def append(self, statement): - if self.num_kwargs() > 0: - self.order_error = True - if not isinstance(statement, EmptyNode): - self.arguments += [statement] - - def set_kwarg(self, name, value): - if name in self.kwargs: - mlog.warning('Keyword argument "{}" defined multiple times.'.format(name), location=self) - mlog.warning('This will be an error in future Meson releases.') - self.kwargs[name] = value - - def num_args(self): - return len(self.arguments) - - def num_kwargs(self): - return len(self.kwargs) - - def incorrect_order(self): - return self.order_error - - def __len__(self): - return self.num_args() # Fixme + def __init__(self, condition: BaseNode, trueblock: BaseNode, falseblock: BaseNode): + super().__init__(condition.lineno, condition.colno, condition.filename) + self.condition = condition # type: BaseNode + self.trueblock = trueblock # type: BaseNode + self.falseblock = falseblock # type: BaseNode comparison_map = {'equal': '==', 'nequal': '!=', @@ -485,58 +467,60 @@ comparison_map = {'equal': '==', # 9 plain token class Parser: - def __init__(self, code, filename): + def __init__(self, code: str, filename: str): self.lexer = Lexer(code) self.stream = self.lexer.lex(filename) - self.current = Token('eof', '', 0, 0, 0, (0, 0), None) + self.current = Token('eof', '', 0, 0, 0, (0, 0), None) # type: Token self.getsym() self.in_ternary = False - def getsym(self): + def getsym(self) -> None: try: self.current = next(self.stream) except StopIteration: self.current = Token('eof', '', self.current.line_start, self.current.lineno, self.current.colno + self.current.bytespan[1] - self.current.bytespan[0], (0, 0), None) - def getline(self): + def getline(self) -> str: return self.lexer.getline(self.current.line_start) - def accept(self, s): + def accept(self, s: str) -> bool: if self.current.tid == s: self.getsym() return True return False - def expect(self, s): + def expect(self, s: str) -> bool: if self.accept(s): return True raise ParseException('Expecting %s got %s.' % (s, self.current.tid), self.getline(), self.current.lineno, self.current.colno) - def block_expect(self, s, block_start): + def block_expect(self, s: str, block_start: Token) -> bool: if self.accept(s): return True raise BlockParseException('Expecting %s got %s.' % (s, self.current.tid), self.getline(), self.current.lineno, self.current.colno, self.lexer.getline(block_start.line_start), block_start.lineno, block_start.colno) - def parse(self): + def parse(self) -> CodeBlockNode: block = self.codeblock() self.expect('eof') return block - def statement(self): + def statement(self) -> BaseNode: return self.e1() - def e1(self): + def e1(self) -> BaseNode: left = self.e2() if self.accept('plusassign'): value = self.e1() if not isinstance(left, IdNode): raise ParseException('Plusassignment target must be an id.', self.getline(), left.lineno, left.colno) + assert isinstance(left.value, str) return PlusAssignmentNode(left.filename, left.lineno, left.colno, left.value, value) elif self.accept('assign'): value = self.e1() if not isinstance(left, IdNode): raise ParseException('Assignment target must be an id.', self.getline(), left.lineno, left.colno) + assert isinstance(left.value, str) return AssignmentNode(left.filename, left.lineno, left.colno, left.value, value) elif self.accept('questionmark'): if self.in_ternary: @@ -547,10 +531,10 @@ class Parser: self.expect('colon') falseblock = self.e1() self.in_ternary = False - return TernaryNode(left.filename, left.lineno, left.colno, left, trueblock, falseblock) + return TernaryNode(left, trueblock, falseblock) return left - def e2(self): + def e2(self) -> BaseNode: left = self.e3() while self.accept('or'): if isinstance(left, EmptyNode): @@ -559,7 +543,7 @@ class Parser: left = OrNode(left, self.e3()) return left - def e3(self): + def e3(self) -> BaseNode: left = self.e4() while self.accept('and'): if isinstance(left, EmptyNode): @@ -568,7 +552,7 @@ class Parser: left = AndNode(left, self.e4()) return left - def e4(self): + def e4(self) -> BaseNode: left = self.e5() for nodename, operator_type in comparison_map.items(): if self.accept(nodename): @@ -577,47 +561,47 @@ class Parser: return ComparisonNode('notin', left, self.e5()) return left - def e5(self): + def e5(self) -> BaseNode: return self.e5add() - def e5add(self): + def e5add(self) -> BaseNode: left = self.e5sub() if self.accept('plus'): return ArithmeticNode('add', left, self.e5add()) return left - def e5sub(self): + def e5sub(self) -> BaseNode: left = self.e5mod() if self.accept('dash'): return ArithmeticNode('sub', left, self.e5sub()) return left - def e5mod(self): + def e5mod(self) -> BaseNode: left = self.e5mul() if self.accept('percent'): return ArithmeticNode('mod', left, self.e5mod()) return left - def e5mul(self): + def e5mul(self) -> BaseNode: left = self.e5div() if self.accept('star'): return ArithmeticNode('mul', left, self.e5mul()) return left - def e5div(self): + def e5div(self) -> BaseNode: left = self.e6() if self.accept('fslash'): return ArithmeticNode('div', left, self.e5div()) return left - def e6(self): + def e6(self) -> BaseNode: if self.accept('not'): return NotNode(self.current, self.e7()) if self.accept('dash'): return UMinusNode(self.current, self.e7()) return self.e7() - def e7(self): + def e7(self) -> BaseNode: left = self.e8() block_start = self.current if self.accept('lparen'): @@ -626,6 +610,7 @@ class Parser: if not isinstance(left, IdNode): raise ParseException('Function call must be applied to plain id', self.getline(), left.lineno, left.colno) + assert isinstance(left.value, str) left = FunctionNode(left.filename, left.lineno, left.colno, self.current.lineno, self.current.colno, left.value, args) go_again = True while go_again: @@ -638,7 +623,7 @@ class Parser: left = self.index_call(left) return left - def e8(self): + def e8(self) -> BaseNode: block_start = self.current if self.accept('lparen'): e = self.statement() @@ -655,27 +640,29 @@ class Parser: else: return self.e9() - def e9(self): + def e9(self) -> BaseNode: t = self.current if self.accept('true'): - return BooleanNode(t, True) + t.value = True + return BooleanNode(t) if self.accept('false'): - return BooleanNode(t, False) + t.value = False + return BooleanNode(t) if self.accept('id'): return IdNode(t) if self.accept('number'): return NumberNode(t) if self.accept('string'): return StringNode(t) - return EmptyNode(self.current.lineno, self.current.colno) + return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) - def key_values(self): - s = self.statement() - a = ArgumentNode(s) + def key_values(self) -> ArgumentNode: + s = self.statement() # type: BaseNode + a = ArgumentNode(self.current) while not isinstance(s, EmptyNode): if self.accept('colon'): - a.set_kwarg(s, self.statement()) + a.set_kwarg_no_check(s, self.statement()) potential = self.current if not self.accept('comma'): return a @@ -686,9 +673,9 @@ class Parser: s = self.statement() return a - def args(self): - s = self.statement() - a = ArgumentNode(s) + def args(self) -> ArgumentNode: + s = self.statement() # type: BaseNode + a = ArgumentNode(self.current) while not isinstance(s, EmptyNode): potential = self.current @@ -699,7 +686,7 @@ class Parser: if not isinstance(s, IdNode): raise ParseException('Dictionary key must be a plain identifier.', self.getline(), s.lineno, s.colno) - a.set_kwarg(s.value, self.statement()) + a.set_kwarg(s, self.statement()) potential = self.current if not self.accept('comma'): return a @@ -710,11 +697,12 @@ class Parser: s = self.statement() return a - def method_call(self, source_object): + def method_call(self, source_object) -> MethodNode: methodname = self.e9() if not(isinstance(methodname, IdNode)): raise ParseException('Method name must be plain id', self.getline(), self.current.lineno, self.current.colno) + assert isinstance(methodname.value, str) self.expect('lparen') args = self.args() self.expect('rparen') @@ -723,68 +711,73 @@ class Parser: return self.method_call(method) return method - def index_call(self, source_object): + def index_call(self, source_object) -> IndexNode: index_statement = self.statement() self.expect('rbracket') return IndexNode(source_object, index_statement) - def foreachblock(self): + def foreachblock(self) -> ForeachClauseNode: t = self.current self.expect('id') + assert isinstance(t.value, str) varname = t - varnames = [t] + varnames = [t.value] # type: T.List[str] if self.accept('comma'): t = self.current self.expect('id') - varnames.append(t) + assert isinstance(t.value, str) + varnames.append(t.value) self.expect('colon') items = self.statement() block = self.codeblock() - return ForeachClauseNode(varname.lineno, varname.colno, varnames, items, block) + return ForeachClauseNode(varname, varnames, items, block) - def ifblock(self): + def ifblock(self) -> IfClauseNode: condition = self.statement() - clause = IfClauseNode(condition.lineno, condition.colno) + clause = IfClauseNode(condition) self.expect('eol') block = self.codeblock() - clause.ifs.append(IfNode(clause.lineno, clause.colno, condition, block)) + clause.ifs.append(IfNode(clause, condition, block)) self.elseifblock(clause) - clause.elseblock = self.elseblock() + elseblock = self.elseblock() + if elseblock: + clause.elseblock = elseblock return clause - def elseifblock(self, clause): + def elseifblock(self, clause) -> None: while self.accept('elif'): s = self.statement() self.expect('eol') b = self.codeblock() - clause.ifs.append(IfNode(s.lineno, s.colno, s, b)) + clause.ifs.append(IfNode(s, s, b)) - def elseblock(self): + def elseblock(self) -> T.Optional[CodeBlockNode]: if self.accept('else'): self.expect('eol') return self.codeblock() + return None - def line(self): + def line(self) -> BaseNode: block_start = self.current if self.current == 'eol': - return EmptyNode(self.current.lineno, self.current.colno) + return EmptyNode(self.current.lineno, self.current.colno, self.current.filename) if self.accept('if'): - block = self.ifblock() + ifblock = self.ifblock() self.block_expect('endif', block_start) - return block + return ifblock if self.accept('foreach'): - block = self.foreachblock() + forblock = self.foreachblock() self.block_expect('endforeach', block_start) - return block + return forblock if self.accept('continue'): return ContinueNode(self.current) if self.accept('break'): return BreakNode(self.current) return self.statement() - def codeblock(self): + def codeblock(self) -> CodeBlockNode: block = CodeBlockNode(self.current) cond = True while cond: diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index fd0a8c95e..2695a269c 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -176,10 +176,10 @@ class OptionInterpreter: reduced_pos = [self.reduce_single(arg) for arg in args.arguments] reduced_kw = {} for key in args.kwargs.keys(): - if not isinstance(key, str): + if not isinstance(key, mparser.IdNode): raise OptionException('Keyword argument name is not a string.') a = args.kwargs[key] - reduced_kw[key] = self.reduce_single(a) + reduced_kw[key.value] = self.reduce_single(a) return reduced_pos, reduced_kw def evaluate_statement(self, node): diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index 39d8337d9..785451fd2 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -113,7 +113,7 @@ class MTypeBase: def _new_node(self): # Overwrite in derived class - return BaseNode() + raise RewriterException('Internal error: _new_node of MTypeBase was called') def can_modify(self): return self.node_type is not None @@ -159,7 +159,7 @@ class MTypeBool(MTypeBase): super().__init__(node) def _new_node(self): - return StringNode(Token('', '', 0, 0, 0, None, False)) + return BooleanNode(Token('', '', 0, 0, 0, None, False)) def supported_nodes(self): return [BooleanNode] @@ -172,7 +172,7 @@ class MTypeID(MTypeBase): super().__init__(node) def _new_node(self): - return StringNode(Token('', '', 0, 0, 0, None, '')) + return IdNode(Token('', '', 0, 0, 0, None, '')) def supported_nodes(self): return [IdNode] @@ -189,7 +189,7 @@ class MTypeList(MTypeBase): def _new_element_node(self, value): # Overwrite in derived class - return BaseNode() + raise RewriterException('Internal error: _new_element_node of MTypeList was called') def _ensure_array_node(self): if not isinstance(self.node, ArrayNode): @@ -414,10 +414,10 @@ class Rewriter: # Check the assignments tgt = None if target in self.interpreter.assignments: - node = self.interpreter.assignments[target][0] + node = self.interpreter.assignments[target] if isinstance(node, FunctionNode): if node.func_name in ['executable', 'jar', 'library', 'shared_library', 'shared_module', 'static_library', 'both_libraries']: - tgt = self.interpreter.assign_vals[target][0] + tgt = self.interpreter.assign_vals[target] return tgt @@ -434,7 +434,7 @@ class Rewriter: # Check the assignments if dependency in self.interpreter.assignments: - node = self.interpreter.assignments[dependency][0] + node = self.interpreter.assignments[dependency] if isinstance(node, FunctionNode): if node.func_name in ['dependency']: name = self.interpreter.flatten_args(node.args)[0] @@ -522,6 +522,8 @@ class Rewriter: mlog.error('Unable to find the function node') assert(isinstance(node, FunctionNode)) assert(isinstance(arg_node, ArgumentNode)) + # Transform the key nodes to plain strings + arg_node.kwargs = {k.value: v for k, v in arg_node.kwargs.items()} # Print kwargs info if cmd['operation'] == 'info': @@ -585,11 +587,13 @@ class Rewriter: arg_node.kwargs[key] = modifyer.get_node() num_changed += 1 + # Convert the keys back to IdNode's + arg_node.kwargs = {IdNode(Token('', '', 0, 0, 0, None, k)): v for k, v in arg_node.kwargs.items()} if num_changed > 0 and node not in self.modefied_nodes: self.modefied_nodes += [node] def find_assignment_node(self, node: BaseNode) -> AssignmentNode: - if hasattr(node, 'ast_id') and node.ast_id in self.interpreter.reverse_assignment: + if node.ast_id and node.ast_id in self.interpreter.reverse_assignment: return self.interpreter.reverse_assignment[node.ast_id] return None diff --git a/run_unittests.py b/run_unittests.py index 56551655e..3f5c87e45 100755 --- a/run_unittests.py +++ b/run_unittests.py @@ -1398,7 +1398,7 @@ class DataTests(unittest.TestCase): ''' env = get_fake_env() interp = Interpreter(FakeBuild(env), mock=True) - astint = AstInterpreter('.', '') + astint = AstInterpreter('.', '', '') self.assertEqual(set(interp.funcs.keys()), set(astint.funcs.keys()))