diff options
Diffstat (limited to 'python/mozbuild/mozbuild/configure/lint.py')
-rw-r--r-- | python/mozbuild/mozbuild/configure/lint.py | 348 |
1 files changed, 348 insertions, 0 deletions
diff --git a/python/mozbuild/mozbuild/configure/lint.py b/python/mozbuild/mozbuild/configure/lint.py new file mode 100644 index 0000000000..7ea379b1ef --- /dev/null +++ b/python/mozbuild/mozbuild/configure/lint.py @@ -0,0 +1,348 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import inspect +import re +import types +from dis import Bytecode +from functools import wraps +from io import StringIO + +from mozbuild.util import memoize + +from . import ( + CombinedDependsFunction, + ConfigureError, + ConfigureSandbox, + DependsFunction, + SandboxDependsFunction, + SandboxedGlobal, + TrivialDependsFunction, +) +from .help import HelpFormatter + + +class LintSandbox(ConfigureSandbox): + def __init__(self, environ=None, argv=None, stdout=None, stderr=None): + out = StringIO() + stdout = stdout or out + stderr = stderr or out + environ = environ or {} + argv = argv or [] + self._wrapped = {} + self._has_imports = set() + self._bool_options = [] + self._bool_func_options = [] + self.LOG = "" + super(LintSandbox, self).__init__( + {}, environ=environ, argv=argv, stdout=stdout, stderr=stderr + ) + + def run(self, path=None): + if path: + self.include_file(path) + + for dep in self._depends.values(): + self._check_dependencies(dep) + + def _raise_from(self, exception, obj, line=0): + """ + Raises the given exception as if it were emitted from the given + location. + + The location is determined from the values of obj and line. + - `obj` can be a function or DependsFunction, in which case + `line` corresponds to the line within the function the exception + will be raised from (as an offset from the function's firstlineno). + - `obj` can be a stack frame, in which case `line` is ignored. + """ + + def thrower(e): + raise e + + if isinstance(obj, DependsFunction): + obj, _ = self.unwrap(obj._func) + + if inspect.isfunction(obj): + funcname = obj.__name__ + filename = obj.__code__.co_filename + firstline = obj.__code__.co_firstlineno + line += firstline + elif inspect.isframe(obj): + funcname = obj.f_code.co_name + filename = obj.f_code.co_filename + firstline = obj.f_code.co_firstlineno + line = obj.f_lineno + else: + # Don't know how to handle the given location, still raise the + # exception. + raise exception + + # Create a new function from the above thrower that pretends + # the `def` line is on the first line of the function given as + # argument, and the `raise` line is on the line given as argument. + + offset = line - firstline + # co_lnotab is a string where each pair of consecutive character is + # (chr(byte_increment), chr(line_increment)), mapping bytes in co_code + # to line numbers relative to co_firstlineno. + # If the offset we need to encode is larger than what fits in a 8-bit + # signed integer, we need to split it. + co_lnotab = bytes([0, 127] * (offset // 127) + [0, offset % 127]) + code = thrower.__code__ + codetype_args = [ + code.co_argcount, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + filename, + funcname, + firstline, + co_lnotab, + ] + if hasattr(code, "co_posonlyargcount"): + # co_posonlyargcount was introduced in Python 3.8. + codetype_args.insert(1, code.co_posonlyargcount) + + code = types.CodeType(*codetype_args) + thrower = types.FunctionType( + code, + thrower.__globals__, + funcname, + thrower.__defaults__, + thrower.__closure__, + ) + thrower(exception) + + def _check_dependencies(self, obj): + if isinstance(obj, CombinedDependsFunction) or obj in ( + self._always, + self._never, + ): + return + if not inspect.isroutine(obj._func): + return + func, glob = self.unwrap(obj._func) + func_args = inspect.getfullargspec(func) + if func_args.varkw: + e = ConfigureError( + "Keyword arguments are not allowed in @depends functions" + ) + self._raise_from(e, func) + + all_args = list(func_args.args) + if func_args.varargs: + all_args.append(func_args.varargs) + used_args = set() + + for instr in Bytecode(func): + if instr.opname in ("LOAD_FAST", "LOAD_CLOSURE"): + if instr.argval in all_args: + used_args.add(instr.argval) + + for num, arg in enumerate(all_args): + if arg not in used_args: + dep = obj.dependencies[num] + if dep != self._help_option or not self._need_help_dependency(obj): + if isinstance(dep, DependsFunction): + dep = dep.name + else: + dep = dep.option + e = ConfigureError("The dependency on `%s` is unused" % dep) + self._raise_from(e, func) + + def _need_help_dependency(self, obj): + if isinstance(obj, (CombinedDependsFunction, TrivialDependsFunction)): + return False + if isinstance(obj, DependsFunction): + if obj in (self._always, self._never) or not inspect.isroutine(obj._func): + return False + func, glob = self.unwrap(obj._func) + # We allow missing --help dependencies for functions that: + # - don't use @imports + # - don't have a closure + # - don't use global variables + if func in self._has_imports or func.__closure__: + return True + for instr in Bytecode(func): + if instr.opname in ("LOAD_GLOBAL", "STORE_GLOBAL"): + # There is a fake os module when one is not imported, + # and it's allowed for functions without a --help + # dependency. + if instr.argval == "os" and glob.get("os") is self.OS: + continue + if instr.argval in self.BUILTINS: + continue + if instr.argval in "namespace": + continue + return True + return False + + def _missing_help_dependency(self, obj): + if isinstance(obj, DependsFunction) and self._help_option in obj.dependencies: + return False + return self._need_help_dependency(obj) + + @memoize + def _value_for_depends(self, obj): + with_help = self._help_option in obj.dependencies + if with_help: + for arg in obj.dependencies: + if self._missing_help_dependency(arg): + e = ConfigureError( + "Missing '--help' dependency because `%s` depends on " + "'--help' and `%s`" % (obj.name, arg.name) + ) + self._raise_from(e, arg) + elif self._missing_help_dependency(obj): + e = ConfigureError("Missing '--help' dependency") + self._raise_from(e, obj) + return super(LintSandbox, self)._value_for_depends(obj) + + def option_impl(self, *args, **kwargs): + result = super(LintSandbox, self).option_impl(*args, **kwargs) + when = self._conditions.get(result) + if when: + self._value_for(when) + + self._check_option(result, *args, **kwargs) + + return result + + def _check_option(self, option, *args, **kwargs): + if "default" not in kwargs: + return + if len(args) == 0: + return + + self._check_prefix_for_bool_option(*args, **kwargs) + self._check_help_for_option_with_func_default(option, *args, **kwargs) + + def _check_prefix_for_bool_option(self, *args, **kwargs): + name = args[0] + default = kwargs["default"] + + if type(default) != bool: + return + + table = { + True: { + "enable": "disable", + "with": "without", + }, + False: { + "disable": "enable", + "without": "with", + }, + } + for prefix, replacement in table[default].items(): + if name.startswith("--{}-".format(prefix)): + frame = inspect.currentframe() + while frame and frame.f_code.co_name != self.option_impl.__name__: + frame = frame.f_back + e = ConfigureError( + "{} should be used instead of " + "{} with default={}".format( + name.replace( + "--{}-".format(prefix), "--{}-".format(replacement) + ), + name, + default, + ) + ) + self._raise_from(e, frame.f_back if frame else None) + + def _check_help_for_option_with_func_default(self, option, *args, **kwargs): + default = kwargs["default"] + + if not isinstance(default, SandboxDependsFunction): + return + + if not option.prefix: + return + + default = self._resolve(default) + if type(default) is str: + return + + help = kwargs["help"] + match = re.search(HelpFormatter.RE_FORMAT, help) + if match: + return + + if option.prefix in ("enable", "disable"): + rule = "{Enable|Disable}" + else: + rule = "{With|Without}" + + frame = inspect.currentframe() + while frame and frame.f_code.co_name != self.option_impl.__name__: + frame = frame.f_back + e = ConfigureError( + '`help` should contain "{}" because of non-constant default'.format(rule) + ) + self._raise_from(e, frame.f_back if frame else None) + + def unwrap(self, func): + glob = func.__globals__ + while func in self._wrapped: + if isinstance(func.__globals__, SandboxedGlobal): + glob = func.__globals__ + func = self._wrapped[func] + return func, glob + + def wraps(self, func): + def do_wraps(wrapper): + self._wrapped[wrapper] = func + return wraps(func)(wrapper) + + return do_wraps + + def imports_impl(self, _import, _from=None, _as=None): + wrapper = super(LintSandbox, self).imports_impl(_import, _from=_from, _as=_as) + + def decorator(func): + self._has_imports.add(func) + return wrapper(func) + + return decorator + + def _prepare_function(self, func, update_globals=None): + wrapped = super(LintSandbox, self)._prepare_function(func, update_globals) + _, glob = self.unwrap(wrapped) + imports = set() + for _from, _import, _as in self._imports.get(func, ()): + if _as: + imports.add(_as) + else: + what = _import.split(".")[0] + imports.add(what) + if _from == "__builtin__" and _import in glob["__builtins__"]: + e = NameError( + "builtin '{}' doesn't need to be imported".format(_import) + ) + self._raise_from(e, func) + for instr in Bytecode(func): + code = func.__code__ + if ( + instr.opname == "LOAD_GLOBAL" + and instr.argval not in glob + and instr.argval not in imports + and instr.argval not in glob["__builtins__"] + and instr.argval not in code.co_varnames[: code.co_argcount] + ): + # Raise the same kind of error as what would happen during + # execution. + e = NameError("global name '{}' is not defined".format(instr.argval)) + if instr.starts_line is None: + self._raise_from(e, func) + else: + self._raise_from(e, func, instr.starts_line - code.co_firstlineno) + + return wrapped |