diff options
Diffstat (limited to 'tools/lint/rust/__init__.py')
-rw-r--r-- | tools/lint/rust/__init__.py | 180 |
1 files changed, 180 insertions, 0 deletions
diff --git a/tools/lint/rust/__init__.py b/tools/lint/rust/__init__.py new file mode 100644 index 0000000000..4f63930b49 --- /dev/null +++ b/tools/lint/rust/__init__.py @@ -0,0 +1,180 @@ +# 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 os +import re +import signal +import subprocess +from collections import namedtuple + +import six +from mozboot.util import get_tools_dir +from mozfile import which +from mozlint import result +from mozlint.pathutils import expand_exclusions +from mozprocess import ProcessHandler +from packaging.version import Version + +RUSTFMT_NOT_FOUND = """ +Could not find rustfmt! Install rustfmt and try again. + + $ rustup component add rustfmt + +And make sure that it is in the PATH +""".strip() + + +RUSTFMT_INSTALL_ERROR = """ +Unable to install correct version of rustfmt +Try to install it manually with: + $ rustup component add rustfmt +""".strip() + + +RUSTFMT_WRONG_VERSION = """ +You are probably using an old version of rustfmt. +Expected version is {version}. +Try to update it: + $ rustup update stable +""".strip() + + +def parse_issues(config, output, paths): + RustfmtDiff = namedtuple("RustfmtDiff", ["file", "line", "diff"]) + issues = [] + diff_line = re.compile("^Diff in (.*) at line ([0-9]*):") + file = "" + line_no = 0 + diff = "" + for line in output: + line = six.ensure_text(line) + match = diff_line.match(line) + if match: + if diff: + issues.append(RustfmtDiff(file, line_no, diff.rstrip("\n"))) + diff = "" + file, line_no = match.groups() + else: + diff += line + "\n" + # the algorithm above will always skip adding the last issue + issues.append(RustfmtDiff(file, line_no, diff)) + file = os.path.normcase(os.path.normpath(file)) + results = [] + for issue in issues: + # rustfmt can not be supplied the paths to the files we want to analyze + # therefore, for each issue detected, we check if any of the the paths + # supplied are part of the file name. + # This just filters out the issues that are not part of paths. + if any([os.path.normcase(os.path.normpath(path)) in file for path in paths]): + res = { + "path": issue.file, + "diff": issue.diff, + "level": "warning", + "lineno": issue.line, + } + results.append(result.from_config(config, **res)) + return {"results": results, "fixed": 0} + + +class RustfmtProcess(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 = RustfmtProcess(config, cmd) + proc.run() + try: + proc.wait() + except KeyboardInterrupt: + proc.kill() + + return proc.output + + +def get_rustfmt_binary(): + """ + Returns the path of the first rustfmt binary available + if not found returns None + """ + binary = os.environ.get("RUSTFMT") + if binary: + return binary + + rust_path = os.path.join(get_tools_dir(), "rustc", "bin") + return which("rustfmt", path=os.pathsep.join([rust_path, os.environ["PATH"]])) + + +def get_rustfmt_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 + + version = re.findall(r"\d.\d+.\d+", output)[0] + return Version(version) + + +def lint(paths, config, fix=None, **lintargs): + log = lintargs["log"] + paths = list(expand_exclusions(paths, config, lintargs["root"])) + + # An empty path array can occur when the user passes in `-n`. If we don't + # return early in this case, rustfmt will attempt to read stdin and hang. + if not paths: + return [] + + binary = get_rustfmt_binary() + + if not binary: + print(RUSTFMT_NOT_FOUND) + if "MOZ_AUTOMATION" in os.environ: + return 1 + return [] + + min_version_str = config.get("min_rustfmt_version") + min_version = Version(min_version_str) + actual_version = get_rustfmt_version(binary) + log.debug( + "Found version: {}. Minimal expected version: {}".format( + actual_version, min_version + ) + ) + + if actual_version < min_version: + print(RUSTFMT_WRONG_VERSION.format(version=min_version_str)) + return 1 + + cmd_args = [binary] + cmd_args.append("--check") + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + issues = parse_issues(config, output, paths) + + if fix: + issues["fixed"] = len(issues["results"]) + issues["results"] = [] + cmd_args.remove("--check") + + base_command = cmd_args + paths + log.debug("Command: {}".format(" ".join(cmd_args))) + output = run_process(config, base_command) + + return issues |