forked from mirrors/gecko-dev
Ruff is a very fast linter implemented in Rust and it can act as a drop-in replacement for flake8. When running the same set of rules across all files in mozilla-central (without mozlint), flake8 takes 900 seconds whereas ruff takes 0.9 seconds. Ruff also implements rules from other popular Python linters such as pylint, isort and pyupgrade. There are even plans to implement feature parity with black in the future. Ultimately, it can become our one stop shop for all Python linting and formatting. This stack will swap out all our Python lint tools for ruff (excluding black for now). Differential Revision: https://phabricator.services.mozilla.com/D172313
177 lines
5.3 KiB
Python
177 lines
5.3 KiB
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 json
|
|
import os
|
|
import platform
|
|
import re
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import mozfile
|
|
from mozlint import result
|
|
from mozprocess.processhandler import ProcessHandler
|
|
|
|
here = os.path.abspath(os.path.dirname(__file__))
|
|
RUFF_REQUIREMENTS_PATH = os.path.join(here, "ruff_requirements.txt")
|
|
|
|
RUFF_NOT_FOUND = """
|
|
Could not find ruff! Install ruff and try again.
|
|
|
|
$ pip install -U --require-hashes -r {}
|
|
""".strip().format(
|
|
RUFF_REQUIREMENTS_PATH
|
|
)
|
|
|
|
|
|
RUFF_INSTALL_ERROR = """
|
|
Unable to install correct version of ruff!
|
|
Try to install it manually with:
|
|
$ pip install -U --require-hashes -r {}
|
|
""".strip().format(
|
|
RUFF_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_ruff_version(binary):
|
|
"""
|
|
Returns found binary's version
|
|
"""
|
|
try:
|
|
output = subprocess.check_output(
|
|
[binary, "--version"],
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
output = e.output
|
|
|
|
matches = re.match(r"ruff ([0-9\.]+)", output)
|
|
if matches:
|
|
return matches[1]
|
|
print("Error: Could not parse the version '{}'".format(output))
|
|
|
|
|
|
def setup(root, log, **lintargs):
|
|
virtualenv_bin_path = lintargs.get("virtualenv_bin_path")
|
|
binary = mozfile.which("ruff", path=(virtualenv_bin_path, default_bindir()))
|
|
|
|
if binary and os.path.isfile(binary):
|
|
log.debug(f"Looking for ruff at {binary}")
|
|
version = get_ruff_version(binary)
|
|
versions = [
|
|
line.split()[0].strip()
|
|
for line in open(RUFF_REQUIREMENTS_PATH).readlines()
|
|
if line.startswith("ruff==")
|
|
]
|
|
if [f"ruff=={version}"] == versions:
|
|
log.debug("ruff is present with expected version {}".format(version))
|
|
return 0
|
|
else:
|
|
log.debug("ruff is present but unexpected version {}".format(version))
|
|
|
|
virtualenv_manager = lintargs["virtualenv_manager"]
|
|
try:
|
|
virtualenv_manager.install_pip_requirements(RUFF_REQUIREMENTS_PATH, quiet=True)
|
|
except subprocess.CalledProcessError:
|
|
print(RUFF_INSTALL_ERROR)
|
|
return 1
|
|
|
|
|
|
class RuffProcess(ProcessHandler):
|
|
def __init__(self, config, *args, **kwargs):
|
|
self.config = config
|
|
self.stderr = []
|
|
kwargs["stream"] = False
|
|
kwargs["universal_newlines"] = True
|
|
kwargs["processStderrLine"] = lambda line: print(line, file=sys.stderr)
|
|
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 = RuffProcess(config, cmd)
|
|
proc.run()
|
|
try:
|
|
proc.wait()
|
|
except KeyboardInterrupt:
|
|
proc.kill()
|
|
|
|
return "\n".join(proc.output)
|
|
|
|
|
|
def lint(paths, config, log, **lintargs):
|
|
fixed = 0
|
|
results = []
|
|
|
|
if not paths:
|
|
return {"results": results, "fixed": fixed}
|
|
|
|
# Currently ruff only lints non `.py` files if they are explicitly passed
|
|
# in. So we need to find any non-py files manually. This can be removed
|
|
# after https://github.com/charliermarsh/ruff/issues/3410 is fixed.
|
|
exts = [e for e in config["extensions"] if e != "py"]
|
|
non_py_files = []
|
|
for path in paths:
|
|
p = Path(path)
|
|
if not p.is_dir():
|
|
continue
|
|
for ext in exts:
|
|
non_py_files.extend([str(f) for f in p.glob(f"**/*.{ext}")])
|
|
|
|
args = ["ruff", "check", "--force-exclude"] + paths + non_py_files
|
|
|
|
if config["exclude"]:
|
|
args.append(f"--extend-exclude={','.join(config['exclude'])}")
|
|
|
|
if lintargs.get("fix"):
|
|
# Do a first pass with --fix-only as the json format doesn't return the
|
|
# number of fixed issues.
|
|
output = run_process(config, args + ["--fix-only"])
|
|
matches = re.match(r"Fixed (\d+) errors?.", output)
|
|
if matches:
|
|
fixed = int(matches[1])
|
|
|
|
output = run_process(config, args + ["--format=json"])
|
|
if not output:
|
|
return []
|
|
|
|
try:
|
|
issues = json.loads(output)
|
|
except json.JSONDecodeError:
|
|
log.error(f"could not parse output: {output}")
|
|
return []
|
|
|
|
warning_rules = set(config.get("warning-rules", []))
|
|
for issue in issues:
|
|
res = {
|
|
"path": issue["filename"],
|
|
"lineno": issue["location"]["row"],
|
|
"column": issue["location"]["column"],
|
|
"lineoffset": issue["end_location"]["row"] - issue["location"]["row"],
|
|
"message": issue["message"],
|
|
"rule": issue["code"],
|
|
"level": "warning" if issue["code"] in warning_rules else "error",
|
|
}
|
|
if issue["fix"]:
|
|
res["hint"] = issue["fix"]["message"]
|
|
|
|
results.append(result.from_config(config, **res))
|
|
|
|
return {"results": results, "fixed": fixed}
|