summaryrefslogtreecommitdiffstats
path: root/tools/lint/python
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tools/lint/python/__init__.py3
-rw-r--r--tools/lint/python/black.py141
-rw-r--r--tools/lint/python/black_requirements.in1
-rw-r--r--tools/lint/python/black_requirements.txt87
-rwxr-xr-xtools/lint/python/check_compat.py87
-rw-r--r--tools/lint/python/compat.py91
-rw-r--r--tools/lint/python/flake8.py191
-rw-r--r--tools/lint/python/flake8_requirements.txt28
-rw-r--r--tools/lint/python/l10n_lint.py158
-rw-r--r--tools/lint/python/pylint.py137
-rw-r--r--tools/lint/python/pylint_requirements.txt64
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