1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function, unicode_literals 6 7import inspect 8import re 9import types 10from dis import Bytecode 11from functools import wraps 12from io import StringIO 13from . import ( 14 CombinedDependsFunction, 15 ConfigureError, 16 ConfigureSandbox, 17 DependsFunction, 18 SandboxedGlobal, 19 TrivialDependsFunction, 20 SandboxDependsFunction, 21) 22from .help import HelpFormatter 23from mozbuild.util import memoize 24 25 26class LintSandbox(ConfigureSandbox): 27 def __init__(self, environ=None, argv=None, stdout=None, stderr=None): 28 out = StringIO() 29 stdout = stdout or out 30 stderr = stderr or out 31 environ = environ or {} 32 argv = argv or [] 33 self._wrapped = {} 34 self._has_imports = set() 35 self._bool_options = [] 36 self._bool_func_options = [] 37 self.LOG = "" 38 super(LintSandbox, self).__init__( 39 {}, environ=environ, argv=argv, stdout=stdout, stderr=stderr 40 ) 41 42 def run(self, path=None): 43 if path: 44 self.include_file(path) 45 46 for dep in self._depends.values(): 47 self._check_dependencies(dep) 48 49 def _raise_from(self, exception, obj, line=0): 50 """ 51 Raises the given exception as if it were emitted from the given 52 location. 53 54 The location is determined from the values of obj and line. 55 - `obj` can be a function or DependsFunction, in which case 56 `line` corresponds to the line within the function the exception 57 will be raised from (as an offset from the function's firstlineno). 58 - `obj` can be a stack frame, in which case `line` is ignored. 59 """ 60 61 def thrower(e): 62 raise e 63 64 if isinstance(obj, DependsFunction): 65 obj, _ = self.unwrap(obj._func) 66 67 if inspect.isfunction(obj): 68 funcname = obj.__name__ 69 filename = obj.__code__.co_filename 70 firstline = obj.__code__.co_firstlineno 71 line += firstline 72 elif inspect.isframe(obj): 73 funcname = obj.f_code.co_name 74 filename = obj.f_code.co_filename 75 firstline = obj.f_code.co_firstlineno 76 line = obj.f_lineno 77 else: 78 # Don't know how to handle the given location, still raise the 79 # exception. 80 raise exception 81 82 # Create a new function from the above thrower that pretends 83 # the `def` line is on the first line of the function given as 84 # argument, and the `raise` line is on the line given as argument. 85 86 offset = line - firstline 87 # co_lnotab is a string where each pair of consecutive character is 88 # (chr(byte_increment), chr(line_increment)), mapping bytes in co_code 89 # to line numbers relative to co_firstlineno. 90 # If the offset we need to encode is larger than what fits in a 8-bit 91 # signed integer, we need to split it. 92 co_lnotab = bytes([0, 127] * (offset // 127) + [0, offset % 127]) 93 code = thrower.__code__ 94 codetype_args = [ 95 code.co_argcount, 96 code.co_kwonlyargcount, 97 code.co_nlocals, 98 code.co_stacksize, 99 code.co_flags, 100 code.co_code, 101 code.co_consts, 102 code.co_names, 103 code.co_varnames, 104 filename, 105 funcname, 106 firstline, 107 co_lnotab, 108 ] 109 if hasattr(code, "co_posonlyargcount"): 110 # co_posonlyargcount was introduced in Python 3.8. 111 codetype_args.insert(1, code.co_posonlyargcount) 112 113 code = types.CodeType(*codetype_args) 114 thrower = types.FunctionType( 115 code, 116 thrower.__globals__, 117 funcname, 118 thrower.__defaults__, 119 thrower.__closure__, 120 ) 121 thrower(exception) 122 123 def _check_dependencies(self, obj): 124 if isinstance(obj, CombinedDependsFunction) or obj in ( 125 self._always, 126 self._never, 127 ): 128 return 129 func, glob = self.unwrap(obj._func) 130 func_args = inspect.getfullargspec(func) 131 if func_args.varkw: 132 e = ConfigureError( 133 "Keyword arguments are not allowed in @depends functions" 134 ) 135 self._raise_from(e, func) 136 137 all_args = list(func_args.args) 138 if func_args.varargs: 139 all_args.append(func_args.varargs) 140 used_args = set() 141 142 for instr in Bytecode(func): 143 if instr.opname in ("LOAD_FAST", "LOAD_CLOSURE"): 144 if instr.argval in all_args: 145 used_args.add(instr.argval) 146 147 for num, arg in enumerate(all_args): 148 if arg not in used_args: 149 dep = obj.dependencies[num] 150 if dep != self._help_option or not self._need_help_dependency(obj): 151 if isinstance(dep, DependsFunction): 152 dep = dep.name 153 else: 154 dep = dep.option 155 e = ConfigureError("The dependency on `%s` is unused" % dep) 156 self._raise_from(e, func) 157 158 def _need_help_dependency(self, obj): 159 if isinstance(obj, (CombinedDependsFunction, TrivialDependsFunction)): 160 return False 161 if isinstance(obj, DependsFunction): 162 if obj in (self._always, self._never): 163 return False 164 func, glob = self.unwrap(obj._func) 165 # We allow missing --help dependencies for functions that: 166 # - don't use @imports 167 # - don't have a closure 168 # - don't use global variables 169 if func in self._has_imports or func.__closure__: 170 return True 171 for instr in Bytecode(func): 172 if instr.opname in ("LOAD_GLOBAL", "STORE_GLOBAL"): 173 # There is a fake os module when one is not imported, 174 # and it's allowed for functions without a --help 175 # dependency. 176 if instr.argval == "os" and glob.get("os") is self.OS: 177 continue 178 if instr.argval in self.BUILTINS: 179 continue 180 if instr.argval in "namespace": 181 continue 182 return True 183 return False 184 185 def _missing_help_dependency(self, obj): 186 if isinstance(obj, DependsFunction) and self._help_option in obj.dependencies: 187 return False 188 return self._need_help_dependency(obj) 189 190 @memoize 191 def _value_for_depends(self, obj): 192 with_help = self._help_option in obj.dependencies 193 if with_help: 194 for arg in obj.dependencies: 195 if self._missing_help_dependency(arg): 196 e = ConfigureError( 197 "Missing '--help' dependency because `%s` depends on " 198 "'--help' and `%s`" % (obj.name, arg.name) 199 ) 200 self._raise_from(e, arg) 201 elif self._missing_help_dependency(obj): 202 e = ConfigureError("Missing '--help' dependency") 203 self._raise_from(e, obj) 204 return super(LintSandbox, self)._value_for_depends(obj) 205 206 def option_impl(self, *args, **kwargs): 207 result = super(LintSandbox, self).option_impl(*args, **kwargs) 208 when = self._conditions.get(result) 209 if when: 210 self._value_for(when) 211 212 self._check_option(result, *args, **kwargs) 213 214 return result 215 216 def _check_option(self, option, *args, **kwargs): 217 if "default" not in kwargs: 218 return 219 if len(args) == 0: 220 return 221 222 self._check_prefix_for_bool_option(*args, **kwargs) 223 self._check_help_for_option_with_func_default(option, *args, **kwargs) 224 225 def _check_prefix_for_bool_option(self, *args, **kwargs): 226 name = args[0] 227 default = kwargs["default"] 228 229 if type(default) != bool: 230 return 231 232 table = { 233 True: { 234 "enable": "disable", 235 "with": "without", 236 }, 237 False: { 238 "disable": "enable", 239 "without": "with", 240 }, 241 } 242 for prefix, replacement in table[default].items(): 243 if name.startswith("--{}-".format(prefix)): 244 frame = inspect.currentframe() 245 while frame and frame.f_code.co_name != self.option_impl.__name__: 246 frame = frame.f_back 247 e = ConfigureError( 248 "{} should be used instead of " 249 "{} with default={}".format( 250 name.replace( 251 "--{}-".format(prefix), "--{}-".format(replacement) 252 ), 253 name, 254 default, 255 ) 256 ) 257 self._raise_from(e, frame.f_back if frame else None) 258 259 def _check_help_for_option_with_func_default(self, option, *args, **kwargs): 260 default = kwargs["default"] 261 262 if not isinstance(default, SandboxDependsFunction): 263 return 264 265 if not option.prefix: 266 return 267 268 default = self._resolve(default) 269 if type(default) is str: 270 return 271 272 help = kwargs["help"] 273 match = re.search(HelpFormatter.RE_FORMAT, help) 274 if match: 275 return 276 277 if option.prefix in ("enable", "disable"): 278 rule = "{Enable|Disable}" 279 else: 280 rule = "{With|Without}" 281 282 frame = inspect.currentframe() 283 while frame and frame.f_code.co_name != self.option_impl.__name__: 284 frame = frame.f_back 285 e = ConfigureError( 286 '`help` should contain "{}" because of non-constant default'.format(rule) 287 ) 288 self._raise_from(e, frame.f_back if frame else None) 289 290 def unwrap(self, func): 291 glob = func.__globals__ 292 while func in self._wrapped: 293 if isinstance(func.__globals__, SandboxedGlobal): 294 glob = func.__globals__ 295 func = self._wrapped[func] 296 return func, glob 297 298 def wraps(self, func): 299 def do_wraps(wrapper): 300 self._wrapped[wrapper] = func 301 return wraps(func)(wrapper) 302 303 return do_wraps 304 305 def imports_impl(self, _import, _from=None, _as=None): 306 wrapper = super(LintSandbox, self).imports_impl(_import, _from=_from, _as=_as) 307 308 def decorator(func): 309 self._has_imports.add(func) 310 return wrapper(func) 311 312 return decorator 313 314 def _prepare_function(self, func, update_globals=None): 315 wrapped = super(LintSandbox, self)._prepare_function(func, update_globals) 316 _, glob = self.unwrap(wrapped) 317 imports = set() 318 for _from, _import, _as in self._imports.get(func, ()): 319 if _as: 320 imports.add(_as) 321 else: 322 what = _import.split(".")[0] 323 imports.add(what) 324 if _from == "__builtin__" and _import in glob["__builtins__"]: 325 e = NameError( 326 "builtin '{}' doesn't need to be imported".format(_import) 327 ) 328 self._raise_from(e, func) 329 for instr in Bytecode(func): 330 code = func.__code__ 331 if ( 332 instr.opname == "LOAD_GLOBAL" 333 and instr.argval not in glob 334 and instr.argval not in imports 335 and instr.argval not in glob["__builtins__"] 336 and instr.argval not in code.co_varnames[: code.co_argcount] 337 ): 338 # Raise the same kind of error as what would happen during 339 # execution. 340 e = NameError("global name '{}' is not defined".format(instr.argval)) 341 if instr.starts_line is None: 342 self._raise_from(e, func) 343 else: 344 self._raise_from(e, func, instr.starts_line - code.co_firstlineno) 345 346 return wrapped 347