diff options
Diffstat (limited to 'tools/lint/python')
-rw-r--r-- | tools/lint/python/__init__.py | 3 | ||||
-rw-r--r-- | tools/lint/python/black.py | 141 | ||||
-rw-r--r-- | tools/lint/python/black_requirements.in | 1 | ||||
-rw-r--r-- | tools/lint/python/black_requirements.txt | 87 | ||||
-rwxr-xr-x | tools/lint/python/check_compat.py | 87 | ||||
-rw-r--r-- | tools/lint/python/compat.py | 91 | ||||
-rw-r--r-- | tools/lint/python/flake8.py | 191 | ||||
-rw-r--r-- | tools/lint/python/flake8_requirements.txt | 28 | ||||
-rw-r--r-- | tools/lint/python/l10n_lint.py | 158 | ||||
-rw-r--r-- | tools/lint/python/pylint.py | 137 | ||||
-rw-r--r-- | tools/lint/python/pylint_requirements.txt | 64 |
11 files changed, 988 insertions, 0 deletions
diff --git a/tools/lint/python/__init__.py b/tools/lint/python/__init__.py new file mode 100644 index 0000000000..c580d191c1 --- /dev/null +++ b/tools/lint/python/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/tools/lint/python/black.py b/tools/lint/python/black.py new file mode 100644 index 0000000000..2fe58a0ea5 --- /dev/null +++ b/tools/lint/python/black.py @@ -0,0 +1,141 @@ +# 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/. + +from __future__ import absolute_import, print_function + +import os +import platform +import re +import signal +import subprocess +import sys + +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler + +here = os.path.abspath(os.path.dirname(__file__)) +BLACK_REQUIREMENTS_PATH = os.path.join(here, "black_requirements.txt") + +BLACK_INSTALL_ERROR = """ +Unable to install correct version of black +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + BLACK_REQUIREMENTS_PATH +) + + +def default_bindir(): + # We use sys.prefix to find executables as that gets modified with + # virtualenv's activate_this.py, whereas sys.executable doesn't. + if platform.system() == "Windows": + return os.path.join(sys.prefix, "Scripts") + else: + return os.path.join(sys.prefix, "bin") + + +def get_black_version(binary): + """ + Returns found binary's version + """ + try: + output = subprocess.check_output( + [binary, "--version"], + stderr=subprocess.STDOUT, + universal_newlines=True, + ) + except subprocess.CalledProcessError as e: + output = e.output + + return re.match(r"black, version (.*)$", output)[1] + + +def parse_issues(config, output, paths, *, log): + would_reformat = re.compile("^would reformat (.*)$", re.I) + reformatted = re.compile("^reformatted (.*)$", re.I) + cannot_reformat = re.compile("^error: cannot format (.*?): (.*)$", re.I) + results = [] + for line in output: + line = line.decode("utf-8") + if line.startswith("All done!") or line.startswith("Oh no!"): + break + + match = would_reformat.match(line) + if match: + res = {"path": match.group(1), "level": "error"} + results.append(result.from_config(config, **res)) + continue + + match = reformatted.match(line) + if match: + res = {"path": match.group(1), "level": "warning", "message": "reformatted"} + results.append(result.from_config(config, **res)) + continue + + match = cannot_reformat.match(line) + if match: + res = {"path": match.group(1), "level": "error", "message": match.group(2)} + results.append(result.from_config(config, **res)) + continue + + log.debug("Unhandled line", line) + return results + + +class BlackProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def run_process(config, cmd): + proc = BlackProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements(BLACK_REQUIREMENTS_PATH, quiet=True) + except subprocess.CalledProcessError: + print(BLACK_INSTALL_ERROR) + return 1 + + +def run_black(config, paths, fix=None, *, log, virtualenv_bin_path): + binary = os.path.join(virtualenv_bin_path or default_bindir(), "black") + + log.debug("Black version {}".format(get_black_version(binary))) + + cmd_args = [binary] + if not fix: + cmd_args.append("--check") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(base_command))) + return parse_issues(config, run_process(config, base_command), paths, log=log) + + +def lint(paths, config, fix=None, **lintargs): + files = list(expand_exclusions(paths, config, lintargs["root"])) + + return run_black( + config, + files, + fix=fix, + log=lintargs["log"], + virtualenv_bin_path=lintargs.get("virtualenv_bin_path"), + ) diff --git a/tools/lint/python/black_requirements.in b/tools/lint/python/black_requirements.in new file mode 100644 index 0000000000..52ee1d9aa1 --- /dev/null +++ b/tools/lint/python/black_requirements.in @@ -0,0 +1 @@ +black==20.8b1 diff --git a/tools/lint/python/black_requirements.txt b/tools/lint/python/black_requirements.txt new file mode 100644 index 0000000000..16f6e00fe4 --- /dev/null +++ b/tools/lint/python/black_requirements.txt @@ -0,0 +1,87 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --generate-hashes --output-file=tools/lint/python/black_requirements.txt tools/lint/python/black_requirements.in +# +# MANUAL EDIT - for some reasons, dataclasses isn't added. Do it by hand: +# hashin -r tools/lint/python/black_requirements.txt dataclasses==0.6 +# +appdirs==1.4.4 \ + --hash=sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41 \ + --hash=sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128 \ + # via black +black==20.8b1 \ + --hash=sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea \ + --hash=sha256:70b62ef1527c950db59062cda342ea224d772abdf6adc58b86a45421bab20a6b \ + # via -r tools/lint/python/black_requirements.in +click==7.1.2 \ + --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \ + # via black +mypy-extensions==0.4.3 \ + --hash=sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d \ + --hash=sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8 \ + # via black +pathspec==0.8.0 \ + --hash=sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0 \ + --hash=sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061 \ + # via black +regex==2020.7.14 \ + --hash=sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204 \ + --hash=sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162 \ + --hash=sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f \ + --hash=sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb \ + --hash=sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6 \ + --hash=sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7 \ + --hash=sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88 \ + --hash=sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99 \ + --hash=sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644 \ + --hash=sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a \ + --hash=sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840 \ + --hash=sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067 \ + --hash=sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd \ + --hash=sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4 \ + --hash=sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e \ + --hash=sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89 \ + --hash=sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e \ + --hash=sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc \ + --hash=sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf \ + --hash=sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341 \ + --hash=sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7 \ + # via black +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 \ + # via black +typed-ast==1.4.1 \ + --hash=sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355 \ + --hash=sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919 \ + --hash=sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa \ + --hash=sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652 \ + --hash=sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75 \ + --hash=sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01 \ + --hash=sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d \ + --hash=sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1 \ + --hash=sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907 \ + --hash=sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c \ + --hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \ + --hash=sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b \ + --hash=sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614 \ + --hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \ + --hash=sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b \ + --hash=sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41 \ + --hash=sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6 \ + --hash=sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34 \ + --hash=sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe \ + --hash=sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4 \ + --hash=sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7 \ + # via black +typing-extensions==3.7.4.3 \ + --hash=sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918 \ + --hash=sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c \ + --hash=sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f \ + # via black +dataclasses==0.6 \ + --hash=sha256:454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f \ + --hash=sha256:6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84 diff --git a/tools/lint/python/check_compat.py b/tools/lint/python/check_compat.py new file mode 100755 index 0000000000..25a15fcedc --- /dev/null +++ b/tools/lint/python/check_compat.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# 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 ast +import json +import sys + + +def parse_file(f): + with open(f, "rb") as fh: + content = fh.read() + try: + return ast.parse(content) + except SyntaxError as e: + err = { + "path": f, + "message": e.msg, + "lineno": e.lineno, + "column": e.offset, + "source": e.text, + "rule": "is-parseable", + } + print(json.dumps(err)) + + +def check_compat_py2(f): + """Check Python 2 and Python 3 compatibility for a file with Python 2""" + root = parse_file(f) + + # Ignore empty or un-parseable files. + if not root or not root.body: + return + + futures = set() + haveprint = False + future_lineno = 1 + may_have_relative_imports = False + for node in ast.walk(root): + if isinstance(node, ast.ImportFrom): + if node.module == "__future__": + future_lineno = node.lineno + futures |= set(n.name for n in node.names) + else: + may_have_relative_imports = True + elif isinstance(node, ast.Import): + may_have_relative_imports = True + elif isinstance(node, ast.Print): + haveprint = True + + err = { + "path": f, + "lineno": future_lineno, + "column": 1, + } + + if "absolute_import" not in futures and may_have_relative_imports: + err["rule"] = "require absolute_import" + err["message"] = "Missing from __future__ import absolute_import" + print(json.dumps(err)) + + if haveprint and "print_function" not in futures: + err["rule"] = "require print_function" + err["message"] = "Missing from __future__ import print_function" + print(json.dumps(err)) + + +def check_compat_py3(f): + """Check Python 3 compatibility of a file with Python 3.""" + parse_file(f) + + +if __name__ == "__main__": + if sys.version_info[0] == 2: + fn = check_compat_py2 + else: + fn = check_compat_py3 + + manifest = sys.argv[1] + with open(manifest, "r") as fh: + files = fh.read().splitlines() + + for f in files: + fn(f) + + sys.exit(0) diff --git a/tools/lint/python/compat.py b/tools/lint/python/compat.py new file mode 100644 index 0000000000..dd541bd66e --- /dev/null +++ b/tools/lint/python/compat.py @@ -0,0 +1,91 @@ +# 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 json +import os +from distutils.spawn import find_executable + +import mozfile +from mozprocess import ProcessHandlerMixin + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) + +results = [] + + +class PyCompatProcess(ProcessHandlerMixin): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["processOutputLine"] = [self.process_line] + ProcessHandlerMixin.__init__(self, *args, **kwargs) + + def process_line(self, line): + try: + res = json.loads(line) + except ValueError: + print( + "Non JSON output from {} linter: {}".format(self.config["name"], line) + ) + return + + res["level"] = "error" + results.append(result.from_config(self.config, **res)) + + +def setup(python): + """Setup doesn't currently do any bootstrapping. For now, this function + is only used to print the warning message. + """ + binary = find_executable(python) + if not binary: + # TODO Bootstrap python2/python3 if not available + print("warning: {} not detected, skipping py-compat check".format(python)) + + +def run_linter(python, paths, config, **lintargs): + log = lintargs["log"] + binary = find_executable(python) + if not binary: + # If we're in automation, this is fatal. Otherwise, the warning in the + # setup method was already printed. + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + files = expand_exclusions(paths, config, lintargs["root"]) + + with mozfile.NamedTemporaryFile(mode="w") as fh: + fh.write("\n".join(files)) + fh.flush() + + cmd = [binary, os.path.join(here, "check_compat.py"), fh.name] + log.debug("Command: {}".format(" ".join(cmd))) + + proc = PyCompatProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return results + + +def setuppy2(**lintargs): + return setup("python2") + + +def lintpy2(*args, **kwargs): + return run_linter("python2", *args, **kwargs) + + +def setuppy3(**lintargs): + return setup("python3") + + +def lintpy3(*args, **kwargs): + return run_linter("python3", *args, **kwargs) diff --git a/tools/lint/python/flake8.py b/tools/lint/python/flake8.py new file mode 100644 index 0000000000..49ab04b019 --- /dev/null +++ b/tools/lint/python/flake8.py @@ -0,0 +1,191 @@ +# 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 json +import os +import platform +import subprocess +import sys + +import mozfile +import mozpack.path as mozpath + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +FLAKE8_REQUIREMENTS_PATH = os.path.join(here, "flake8_requirements.txt") + +FLAKE8_NOT_FOUND = """ +Could not find flake8! Install flake8 and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + FLAKE8_REQUIREMENTS_PATH +) + + +FLAKE8_INSTALL_ERROR = """ +Unable to install correct version of flake8 +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + FLAKE8_REQUIREMENTS_PATH +) + +LINE_OFFSETS = { + # continuation line under-indented for hanging indent + "E121": (-1, 2), + # continuation line missing indentation or outdented + "E122": (-1, 2), + # continuation line over-indented for hanging indent + "E126": (-1, 2), + # continuation line over-indented for visual indent + "E127": (-1, 2), + # continuation line under-indented for visual indent + "E128": (-1, 2), + # continuation line unaligned for hanging indend + "E131": (-1, 2), + # expected 1 blank line, found 0 + "E301": (-1, 2), + # expected 2 blank lines, found 1 + "E302": (-2, 3), +} +"""Maps a flake8 error to a lineoffset tuple. + +The offset is of the form (lineno_offset, num_lines) and is passed +to the lineoffset property of an `Issue`. +""" + + +def default_bindir(): + # We use sys.prefix to find executables as that gets modified with + # virtualenv's activate_this.py, whereas sys.executable doesn't. + if platform.system() == "Windows": + return os.path.join(sys.prefix, "Scripts") + else: + return os.path.join(sys.prefix, "bin") + + +class NothingToLint(Exception): + """Exception used to bail out of flake8's internals if all the specified + files were excluded. + """ + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + FLAKE8_REQUIREMENTS_PATH, quiet=True + ) + except subprocess.CalledProcessError: + print(FLAKE8_INSTALL_ERROR) + return 1 + + +def lint(paths, config, **lintargs): + from flake8.main.application import Application + + log = lintargs["log"] + root = lintargs["root"] + virtualenv_bin_path = lintargs.get("virtualenv_bin_path") + config_path = os.path.join(root, ".flake8") + + if lintargs.get("fix"): + fix_cmd = [ + os.path.join(virtualenv_bin_path or default_bindir(), "autopep8"), + "--global-config", + config_path, + "--in-place", + "--recursive", + ] + + if config.get("exclude"): + fix_cmd.extend(["--exclude", ",".join(config["exclude"])]) + + subprocess.call(fix_cmd + paths) + + # Run flake8. + app = Application() + log.debug("flake8 version={}".format(app.version)) + + output_file = mozfile.NamedTemporaryFile(mode="r") + flake8_cmd = [ + "--config", + config_path, + "--output-file", + output_file.name, + "--format", + '{"path":"%(path)s","lineno":%(row)s,' + '"column":%(col)s,"rule":"%(code)s","message":"%(text)s"}', + "--filename", + ",".join(["*.{}".format(e) for e in config["extensions"]]), + ] + log.debug("Command: {}".format(" ".join(flake8_cmd))) + + orig_make_file_checker_manager = app.make_file_checker_manager + + def wrap_make_file_checker_manager(self): + """Flake8 is very inefficient when it comes to applying exclusion + rules, using `expand_exclusions` to turn directories into a list of + relevant python files is an order of magnitude faster. + + Hooking into flake8 here also gives us a convenient place to merge the + `exclude` rules specified in the root .flake8 with the ones added by + tools/lint/mach_commands.py. + """ + # Ignore exclude rules if `--no-filter` was passed in. + config.setdefault("exclude", []) + if lintargs.get("use_filters", True): + config["exclude"].extend(map(mozpath.normpath, self.options.exclude)) + + # Since we use the root .flake8 file to store exclusions, we haven't + # properly filtered the paths through mozlint's `filterpaths` function + # yet. This mimics that though there could be other edge cases that are + # different. Maybe we should call `filterpaths` directly, though for + # now that doesn't appear to be necessary. + filtered = [ + p for p in paths if not any(p.startswith(e) for e in config["exclude"]) + ] + + self.args = self.args + list(expand_exclusions(filtered, config, root)) + + if not self.args: + raise NothingToLint + return orig_make_file_checker_manager() + + app.make_file_checker_manager = wrap_make_file_checker_manager.__get__( + app, Application + ) + + # Make sure to run from repository root so exclusions are joined to the + # repository root and not the current working directory. + oldcwd = os.getcwd() + os.chdir(root) + try: + app.run(flake8_cmd) + except NothingToLint: + pass + finally: + os.chdir(oldcwd) + + results = [] + + def process_line(line): + # Escape slashes otherwise JSON conversion will not work + line = line.replace("\\", "\\\\") + try: + res = json.loads(line) + except ValueError: + print("Non JSON output from linter, will not be processed: {}".format(line)) + return + + if res.get("code") in LINE_OFFSETS: + res["lineoffset"] = LINE_OFFSETS[res["code"]] + + results.append(result.from_config(config, **res)) + + list(map(process_line, output_file.readlines())) + return results diff --git a/tools/lint/python/flake8_requirements.txt b/tools/lint/python/flake8_requirements.txt new file mode 100644 index 0000000000..2a691e6edf --- /dev/null +++ b/tools/lint/python/flake8_requirements.txt @@ -0,0 +1,28 @@ +flake8==3.8.4 \ + --hash=sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839 \ + --hash=sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f +pyflakes==2.2.0 \ + --hash=sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92 \ + --hash=sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8 +pycodestyle==2.6.0 \ + --hash=sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367 \ + --hash=sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e +setuptools==47.3.1 \ + --hash=sha256:4ba6f9789ea243a6b8ba57da81f75a53494456117810436fd9277a74d1c915d1 \ + --hash=sha256:843037738d1e34e8b326b5e061f474aca6ef9d7ece41329afbc8aac6195a3920 +autopep8==1.5.3 \ + --hash=sha256:60fd8c4341bab59963dafd5d2a566e94f547e660b9b396f772afe67d8481dbf0 +entrypoints==0.3 \ + --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \ + --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 +toml==0.10.1 \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +importlib-metadata==1.6.1 \ + --hash=sha256:0505dd08068cfec00f53a74a0ad927676d7757da81b7436a6eefe4c7cf75c545 \ + --hash=sha256:15ec6c0fd909e893e3a08b3a7c76ecb149122fb14b7efe1199ddd4c7c57ea958 +zipp==3.1.0 \ + --hash=sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b \ + --hash=sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96 diff --git a/tools/lint/python/l10n_lint.py b/tools/lint/python/l10n_lint.py new file mode 100644 index 0000000000..fb79d41a2f --- /dev/null +++ b/tools/lint/python/l10n_lint.py @@ -0,0 +1,158 @@ +# 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/. + +from datetime import datetime, timedelta +import os + +from mozboot import util as mb_util +from mozlint import result, pathutils +from mozpack import path as mozpath +import mozversioncontrol.repoupdate + +from compare_locales.lint.linter import L10nLinter +from compare_locales.lint.util import l10n_base_reference_and_tests +from compare_locales import parser +from compare_locales.paths import TOMLParser, ProjectFiles + + +LOCALE = "gecko-strings" + + +PULL_AFTER = timedelta(days=2) + + +def lint(paths, lintconfig, **lintargs): + l10n_base = mb_util.get_state_dir() + root = lintargs["root"] + exclude = lintconfig.get("exclude") + extensions = lintconfig.get("extensions") + + # Load l10n.toml configs + l10nconfigs = load_configs(lintconfig, root, l10n_base) + + # Check include paths in l10n.yml if it's in our given paths + # Only the l10n.yml will show up here, but if the l10n.toml files + # change, we also get the l10n.yml as the toml files are listed as + # support files. + if lintconfig["path"] in paths: + results = validate_linter_includes(lintconfig, l10nconfigs, lintargs) + paths.remove(lintconfig["path"]) + else: + results = [] + + all_files = [] + for p in paths: + fp = pathutils.FilterPath(p) + if fp.isdir: + for _, fileobj in fp.finder: + all_files.append(fileobj.path) + if fp.isfile: + all_files.append(p) + # Filter again, our directories might have picked up files the + # explicitly excluded in the l10n.yml configuration. + # `browser/locales/en-US/firefox-l10n.js` is a good example. + all_files, _ = pathutils.filterpaths( + lintargs["root"], + all_files, + lintconfig["include"], + exclude=exclude, + extensions=extensions, + ) + # These should be excluded in l10n.yml + skips = {p for p in all_files if not parser.hasParser(p)} + results.extend( + result.from_config( + lintconfig, + level="warning", + path=path, + message="file format not supported in compare-locales", + ) + for path in skips + ) + all_files = [p for p in all_files if p not in skips] + files = ProjectFiles(LOCALE, l10nconfigs) + + get_reference_and_tests = l10n_base_reference_and_tests(files) + + linter = MozL10nLinter(lintconfig) + results += linter.lint(all_files, get_reference_and_tests) + return results + + +def gecko_strings_setup(**lint_args): + gs = mozpath.join(mb_util.get_state_dir(), LOCALE) + marker = mozpath.join(gs, ".hg", "l10n_pull_marker") + try: + last_pull = datetime.fromtimestamp(os.stat(marker).st_mtime) + skip_clone = datetime.now() < last_pull + PULL_AFTER + except OSError: + skip_clone = False + if skip_clone: + return + hg = mozversioncontrol.get_tool_path("hg") + mozversioncontrol.repoupdate.update_mercurial_repo( + hg, "https://hg.mozilla.org/l10n/gecko-strings", gs + ) + with open(marker, "w") as fh: + fh.flush() + + +def load_configs(lintconfig, root, l10n_base): + """Load l10n configuration files specified in the linter configuration.""" + configs = [] + env = {"l10n_base": l10n_base} + for toml in lintconfig["l10n_configs"]: + cfg = TOMLParser().parse( + mozpath.join(root, toml), env=env, ignore_missing_includes=True + ) + cfg.set_locales([LOCALE], deep=True) + configs.append(cfg) + return configs + + +def validate_linter_includes(lintconfig, l10nconfigs, lintargs): + """Check l10n.yml config against l10n.toml configs.""" + reference_paths = set( + mozpath.relpath(p["reference"].prefix, lintargs["root"]) + for project in l10nconfigs + for config in project.configs + for p in config.paths + ) + # Just check for directories + reference_dirs = sorted(p for p in reference_paths if os.path.isdir(p)) + missing_in_yml = [ + refd for refd in reference_dirs if refd not in lintconfig["include"] + ] + # These might be subdirectories in the config, though + missing_in_yml = [ + d + for d in missing_in_yml + if not any(d.startswith(parent + "/") for parent in lintconfig["include"]) + ] + if missing_in_yml: + dirs = ", ".join(missing_in_yml) + return [ + result.from_config( + lintconfig, + path=lintconfig["path"], + message="l10n.yml out of sync with l10n.toml, add: " + dirs, + ) + ] + return [] + + +class MozL10nLinter(L10nLinter): + """Subclass linter to generate the right result type.""" + + def __init__(self, lintconfig): + super(MozL10nLinter, self).__init__() + self.lintconfig = lintconfig + + def lint(self, files, get_reference_and_tests): + return [ + result.from_config(self.lintconfig, **result_data) + for result_data in super(MozL10nLinter, self).lint( + files, get_reference_and_tests + ) + ] diff --git a/tools/lint/python/pylint.py b/tools/lint/python/pylint.py new file mode 100644 index 0000000000..ba95be1f0b --- /dev/null +++ b/tools/lint/python/pylint.py @@ -0,0 +1,137 @@ +# 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 json +import os +import subprocess + +import signal + +from mozprocess import ProcessHandler + +from mozlint import result +from mozlint.pathutils import expand_exclusions + +here = os.path.abspath(os.path.dirname(__file__)) +PYLINT_REQUIREMENTS_PATH = os.path.join(here, "pylint_requirements.txt") + +PYLINT_NOT_FOUND = """ +Could not find pylint! Install pylint and try again. + + $ pip install -U --require-hashes -r {} +""".strip().format( + PYLINT_REQUIREMENTS_PATH +) + + +PYLINT_INSTALL_ERROR = """ +Unable to install correct version of pylint +Try to install it manually with: + $ pip install -U --require-hashes -r {} +""".strip().format( + PYLINT_REQUIREMENTS_PATH +) + + +class PylintProcess(ProcessHandler): + def __init__(self, config, *args, **kwargs): + self.config = config + kwargs["stream"] = False + kwargs["universal_newlines"] = True + ProcessHandler.__init__(self, *args, **kwargs) + + def run(self, *args, **kwargs): + orig = signal.signal(signal.SIGINT, signal.SIG_IGN) + ProcessHandler.run(self, *args, **kwargs) + signal.signal(signal.SIGINT, orig) + + +def setup(root, **lintargs): + virtualenv_manager = lintargs["virtualenv_manager"] + try: + virtualenv_manager.install_pip_requirements( + PYLINT_REQUIREMENTS_PATH, + quiet=True, + # The defined versions of astroid and lazy-object-proxy conflict and fail to + # install with the new 2020 pip resolver (bug 1682959) + legacy_resolver=True, + ) + except subprocess.CalledProcessError: + print(PYLINT_INSTALL_ERROR) + return 1 + + +def get_pylint_binary(): + return "pylint" + + +def run_process(config, cmd): + proc = PylintProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def parse_issues(log, config, issues_json, path): + results = [] + + try: + issues = json.loads(issues_json) + except json.decoder.JSONDecodeError: + log.debug("Could not parse the output:") + log.debug("pylint output: {}".format(issues_json)) + return [] + + for issue in issues: + res = { + "path": issue["path"], + "level": issue["type"], + "lineno": issue["line"], + "column": issue["column"], + "message": issue["message"], + "rule": issue["message-id"], + } + results.append(result.from_config(config, **res)) + return results + + +def get_pylint_version(binary): + return subprocess.check_output( + [binary, "--version"], + universal_newlines=True, + stderr=subprocess.STDOUT, + ) + + +def lint(paths, config, **lintargs): + log = lintargs["log"] + + binary = get_pylint_binary() + + log = lintargs["log"] + paths = list(expand_exclusions(paths, config, lintargs["root"])) + + cmd_args = [binary] + results = [] + + # list from https://code.visualstudio.com/docs/python/linting#_pylint + # And ignore a bit more elements + cmd_args += [ + "-fjson", + "--disable=all", + "--enable=F,E,unreachable,duplicate-key,unnecessary-semicolon,global-variable-not-assigned,unused-variable,binary-op-exception,bad-format-string,anomalous-backslash-in-string,bad-open-mode,no-else-return", # NOQA: E501 + "--disable=import-error,no-member", + ] + + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + log.debug("pylint version: {}".format(get_pylint_version(binary))) + output = " ".join(run_process(config, base_command)) + results = parse_issues(log, config, str(output), []) + + return results diff --git a/tools/lint/python/pylint_requirements.txt b/tools/lint/python/pylint_requirements.txt new file mode 100644 index 0000000000..c5e8d15723 --- /dev/null +++ b/tools/lint/python/pylint_requirements.txt @@ -0,0 +1,64 @@ +pylint==2.6.0 \ + --hash=sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210 \ + --hash=sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f +toml==0.10.1 \ + --hash=sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f \ + --hash=sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88 +mccabe==0.6.1 \ + --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ + --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced +wrapt==1.12.1 \ + --hash=sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7 +lazy-object-proxy==1.5.0 \ + --hash=sha256:0aef3fa29f7d1194d6f8a99382b1b844e5a14d3bc1ef82c3b1c4fb7e7e2019bc \ + --hash=sha256:159ae2bbb4dc3ba506aeba868d14e56a754c0be402d1f0d7fdb264e0bdf2b095 \ + --hash=sha256:161a68a427022bf13e249458be2cb8da56b055988c584d372a917c665825ae9a \ + --hash=sha256:2d58f0e6395bf41087a383a48b06b42165f3b699f1aa41ba201db84ab77be63d \ + --hash=sha256:311c9d1840042fc8e2dd80fc80272a7ea73e7646745556153c9cda85a4628b18 \ + --hash=sha256:35c3ad7b7f7d5d4a54a80f0ff5a41ab186237d6486843f8dde00c42cfab33905 \ + --hash=sha256:459ef557e669d0046fe2b92eb4822c097c00b5ef9d11df0f9bd7d4267acdfc52 \ + --hash=sha256:4a50513b6be001b9b7be2c435478fe9669249c77c241813907a44cda1fcd03f4 \ + --hash=sha256:51035b175740c44707694c521560b55b66da9d5a7c545cf22582bc02deb61664 \ + --hash=sha256:96f2cdb35bdfda10e075f12892a42cff5179bbda698992b845f36c5e92755d33 \ + --hash=sha256:a0aed261060cd0372abf08d16399b1224dbb5b400312e6b00f2b23eabe1d4e96 \ + --hash=sha256:a6052c4c7d95de2345d9c58fc0fe34fff6c27a8ed8550dafeb18ada84406cc99 \ + --hash=sha256:cbf1354292a4f7abb6a0188f74f5e902e4510ebad105be1dbc4809d1ed92f77e \ + --hash=sha256:da82b2372f5ded8806eaac95b19af89a7174efdb418d4e7beb0c6ab09cee7d95 \ + --hash=sha256:dd89f466c930d7cfe84c94b5cbe862867c88b269f23e5aa61d40945e0d746f54 \ + --hash=sha256:e3183fbeb452ec11670c2d9bfd08a57bc87e46856b24d1c335f995239bedd0e1 \ + --hash=sha256:e9a571e7168076a0d5ecaabd91e9032e86d815cca3a4bf0dafead539ef071aa5 \ + --hash=sha256:ec6aba217d0c4f71cbe48aea962a382dedcd111f47b55e8b58d4aaca519bd360 +astroid==2.4.2 \ + --hash=sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703 \ + --hash=sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386 +isort==4.3.21 \ + --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 \ + --hash=sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd +typed-ast==1.4.1 \ + --hash=sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355 \ + --hash=sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919 \ + --hash=sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa \ + --hash=sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652 \ + --hash=sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75 \ + --hash=sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01 \ + --hash=sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d \ + --hash=sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1 \ + --hash=sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907 \ + --hash=sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c \ + --hash=sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3 \ + --hash=sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b \ + --hash=sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614 \ + --hash=sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb \ + --hash=sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b \ + --hash=sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41 \ + --hash=sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6 \ + --hash=sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34 \ + --hash=sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe \ + --hash=sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4 \ + --hash=sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7 +colorama==0.4.3; sys_platform == "win32" \ + --hash=sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff \ + --hash=sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1 |