Enable the all, cargo and pedantic lint groups plus the use_self nursery lint, then opt back out of a curated list of individual lints. clippy/__init__.py gains support for an allow list in clippy.yml, emitting -A clippy::<lint> flags after the -W group flags so the per-lint allows take effect. Differential Revision: https://phabricator.services.mozilla.com/D305697
267 lines
9.3 KiB
Python
267 lines
9.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 bisect
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
|
|
from mozlint import result
|
|
from mozlint.pathutils import expand_exclusions
|
|
|
|
CLIPPY_FIX_ARGS = ("--fix", "--allow-no-vcs")
|
|
|
|
|
|
def get_clippy_driver_flags(config):
|
|
"""Build clippy driver flags (-W/-A/-D) from the warn/allow/deny lists in
|
|
clippy.yml. Order matters: warns (often lint groups) come first, then the
|
|
allows that opt back out of individual lints from those groups, then the
|
|
denys. For a given lint the rightmost flag wins."""
|
|
flags = []
|
|
for lint in config.get("warn", []):
|
|
flags.extend(["-W", f"clippy::{lint}"])
|
|
for lint in config.get("allow", []):
|
|
flags.extend(["-A", f"clippy::{lint}"])
|
|
for lint in config.get("deny", []):
|
|
flags.extend(["-D", f"clippy::{lint}"])
|
|
return flags
|
|
|
|
|
|
def in_sorted_list(l, x):
|
|
i = bisect.bisect_left(l, x)
|
|
return i < len(l) and l[i] == x
|
|
|
|
|
|
def handle_clippy_msg(config, line, log, base_path, files, lint_results):
|
|
try:
|
|
detail = json.loads(line)
|
|
if "message" in detail:
|
|
p = detail["target"]["src_path"]
|
|
detail = detail["message"]
|
|
if "level" in detail:
|
|
if (detail["level"] in {"error", "failure-note"}) and not detail[
|
|
"code"
|
|
]:
|
|
log.debug(
|
|
"Error outside of clippy."
|
|
"This means that the build failed. Therefore, skipping this"
|
|
)
|
|
log.debug(f"File = {p} / Detail = {detail}")
|
|
return
|
|
# We are in a clippy warning
|
|
if len(detail["spans"]) == 0:
|
|
# For some reason, at the end of the summary, we can
|
|
# get the following line
|
|
# {'rendered': 'warning: 5 warnings emitted\n\n', 'children':
|
|
# [], 'code': None, 'level': 'warning', 'message':
|
|
# '5 warnings emitted', 'spans': []}
|
|
# if this is the case, skip it
|
|
log.debug(f"Skipping the summary line {detail} for file {p}")
|
|
return
|
|
|
|
l = detail["spans"][0]
|
|
if files and not in_sorted_list(files, p):
|
|
return
|
|
p = os.path.join(base_path, l["file_name"])
|
|
line = l["line_start"]
|
|
res = {
|
|
"path": p,
|
|
"level": detail["level"],
|
|
"lineno": line,
|
|
"column": l["column_start"],
|
|
"message": detail["message"],
|
|
"hint": detail["rendered"],
|
|
"rule": detail["code"]["code"],
|
|
"lineoffset": l["line_end"] - l["line_start"],
|
|
}
|
|
log.debug(f"Identified an issue in {p}:{line}")
|
|
lint_results["results"].append(result.from_config(config, **res))
|
|
|
|
except json.decoder.JSONDecodeError:
|
|
# Could not parse the message.
|
|
# It is usually cargo info like "Finished `release` profile", etc
|
|
return
|
|
|
|
|
|
def check_clippy_ran(completed_proc, crate_name, log):
|
|
"""Raise if clippy failed to execute (e.g. build environment not set up)."""
|
|
if completed_proc.returncode == 0:
|
|
return
|
|
|
|
def is_valid_json(line):
|
|
try:
|
|
json.loads(line)
|
|
return True
|
|
except json.JSONDecodeError:
|
|
return False
|
|
|
|
has_cargo_json = any(
|
|
is_valid_json(line) for line in completed_proc.stdout.splitlines()
|
|
)
|
|
if not has_cargo_json:
|
|
output = completed_proc.stderr.strip() or completed_proc.stdout.strip()
|
|
log.error(
|
|
"clippy failed to execute for crate '%s' (exit code %d):\n%s",
|
|
crate_name,
|
|
completed_proc.returncode,
|
|
output,
|
|
)
|
|
raise RuntimeError(
|
|
f"Failed to run clippy on '{crate_name}' "
|
|
f"(exit code {completed_proc.returncode}). "
|
|
"Ensure the build environment is set up correctly."
|
|
)
|
|
|
|
|
|
def group_paths(paths, config, root):
|
|
"""
|
|
Groups input paths based on the crate we need to check
|
|
|
|
returns: List of (crate_name, paths) tuples
|
|
"""
|
|
gkrust_path_group = PathGroup("gkrust", root)
|
|
non_gkrust_path_groups = []
|
|
non_gkrust_crates = config.get("non_gkrust_crates", {})
|
|
for crate_name, crate_root in non_gkrust_crates.items():
|
|
non_gkrust_path_groups.append(
|
|
PathGroup(crate_name, os.path.join(root, crate_root))
|
|
)
|
|
|
|
for path in paths:
|
|
path_group = gkrust_path_group
|
|
for candidate in non_gkrust_path_groups:
|
|
if path.startswith(candidate.crate_root):
|
|
path_group = candidate
|
|
break
|
|
path_group.paths.append(path)
|
|
return [p for p in [gkrust_path_group] + non_gkrust_path_groups if p.paths]
|
|
|
|
|
|
@dataclass
|
|
class PathGroup:
|
|
"""
|
|
Tracks paths to lint based on the Rust crate we're running clippy on.
|
|
"""
|
|
|
|
crate_name: str
|
|
crate_root: str
|
|
paths: list[str] = field(default_factory=list)
|
|
|
|
|
|
def lint(paths, config, log, root, substs=None, fix=None, **_lintargs):
|
|
if substs is None:
|
|
substs = {}
|
|
lint_results = {
|
|
"results": [],
|
|
"fixed": 0,
|
|
}
|
|
|
|
cargo_bin = substs.get("CARGO", "cargo")
|
|
|
|
errors = []
|
|
for path_group in group_paths(paths, config, root):
|
|
try:
|
|
if path_group.crate_name == "gkrust":
|
|
lint_gkrust(path_group, config, log, fix, root, lint_results)
|
|
else:
|
|
lint_crate(path_group, config, log, fix, root, cargo_bin, lint_results)
|
|
except RuntimeError as e:
|
|
errors.append(str(e))
|
|
|
|
if errors:
|
|
raise RuntimeError("\n".join(errors))
|
|
|
|
return lint_results
|
|
|
|
|
|
def lint_gkrust(path_group, config, log, fix, root, lint_results):
|
|
"""
|
|
Lint the gkrust crate.
|
|
|
|
This crate contains a lot of dependencies and many of them are legacy code at this point.
|
|
Use a conservative approach to linting:
|
|
* Filter out log messages that don't belong to the specified paths
|
|
* Support the `--fix` flag with path filtering to apply changes only to specified paths.
|
|
"""
|
|
paths = list(expand_exclusions(path_group.paths, config, root))
|
|
paths.sort()
|
|
# gkrust depends on things from the mach environment, so we need to run `./mach cargo` instead
|
|
# of `cargo` directly.
|
|
mach_path = root + "/mach"
|
|
# can be extended in build/cargo/cargo-clippy.yaml
|
|
clippy_args = [
|
|
sys.executable,
|
|
mach_path,
|
|
"--log-no-times",
|
|
"cargo",
|
|
"clippy",
|
|
]
|
|
if fix:
|
|
clippy_args.extend(CLIPPY_FIX_ARGS)
|
|
# --keep-going lets cargo check independent crates even after one fails,
|
|
# so a single broken crate doesn't hide warnings in everything downstream.
|
|
clippy_args.extend(["--", "--keep-going", "--message-format=json"])
|
|
driver_flags = get_clippy_driver_flags(config)
|
|
# MOZ_RUST_DEFAULT_FLAGS sets `-Dwarnings` (warnings-as-errors), which
|
|
# promotes any clippy warning to a hard error and stops cargo at the first
|
|
# offending crate. For linting we want to surface every warning across
|
|
# every included crate, so demote it back to warn-level (last `-W/-D` wins
|
|
# for the same lint group, and extra_rustflags is appended after the
|
|
# defaults).
|
|
flags = ["-W", "warnings"] + driver_flags
|
|
env = os.environ.copy()
|
|
env["extra_rustflags"] = " ".join(flags)
|
|
log.debug("Run clippy with = {}".format(" ".join(clippy_args)))
|
|
completed_proc = subprocess.run(
|
|
clippy_args,
|
|
check=False, # non-zero exit codes are not unexpected
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
)
|
|
check_clippy_ran(completed_proc, "gkrust", log)
|
|
for l in completed_proc.stdout.splitlines():
|
|
handle_clippy_msg(config, l, log, root, paths, lint_results)
|
|
|
|
if fix and completed_proc.returncode == 0:
|
|
lint_results["fixed"] += 1
|
|
|
|
|
|
def lint_crate(path_group, config, log, fix, root, cargo_bin, lint_results):
|
|
"""
|
|
Lint crates other than gkrust.
|
|
|
|
These are newer and more self-contained, so we can use a more aggressive approach to linting:
|
|
* Print out all clippy errors for the crate.
|
|
* Support the `--fix` flag to automatically apply fixes.
|
|
"""
|
|
clippy_args = [
|
|
cargo_bin,
|
|
"clippy",
|
|
"-p",
|
|
path_group.crate_name,
|
|
"--message-format=json",
|
|
]
|
|
if fix:
|
|
clippy_args.extend([*CLIPPY_FIX_ARGS, "--allow-dirty"])
|
|
driver_flags = get_clippy_driver_flags(config)
|
|
if driver_flags:
|
|
clippy_args.extend(["--"] + driver_flags)
|
|
log.debug("Run clippy with = {}".format(" ".join(clippy_args)))
|
|
completed_proc = subprocess.run(
|
|
clippy_args,
|
|
check=False, # non-zero exit codes are not unexpected
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
check_clippy_ran(completed_proc, path_group.crate_name, log)
|
|
|
|
for l in completed_proc.stdout.splitlines():
|
|
handle_clippy_msg(config, l, log, root, None, lint_results)
|
|
|
|
if fix and completed_proc.returncode == 0:
|
|
lint_results["fixed"] += 1
|