summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/configure/lint.py
blob: 7ea379b1eff7abf57a9b81a40578ce7b3f3f70ae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
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