197 lines
6.7 KiB
Python
197 lines
6.7 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 typing import List
|
|
|
|
from mozlint import result
|
|
from mozlint.pathutils import expand_exclusions
|
|
|
|
|
|
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"] == "error" or detail["level"] == "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 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")
|
|
|
|
for path_group in group_paths(paths, config, root):
|
|
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)
|
|
|
|
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
|
|
* Don't support the `--fix` flag, since that could apply changes to paths that weren't
|
|
specified.
|
|
"""
|
|
if fix:
|
|
log.warn("Clippy linting does not support --fix for the gkrust crate")
|
|
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",
|
|
"--",
|
|
"--message-format=json",
|
|
]
|
|
log.debug("Run clippy with = {}".format(" ".join(clippy_args)))
|
|
completed_proc = subprocess.run(
|
|
clippy_args,
|
|
check=False, # non-zero exit codes are not unexpected
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
for l in completed_proc.stdout.splitlines():
|
|
handle_clippy_msg(config, l, log, root, paths, lint_results)
|
|
|
|
|
|
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.
|
|
* Don't support the `--fix` flag, but print out the command the user can manually run.
|
|
"""
|
|
if fix:
|
|
log.warn(
|
|
f"Clippy linting does not support --fix for the gkrust crate, "
|
|
f"run `cargo clippy -p {path_group.crate_name}` manually"
|
|
)
|
|
fix = False
|
|
clippy_args = [
|
|
cargo_bin,
|
|
"clippy",
|
|
"-p",
|
|
path_group.crate_name,
|
|
"--message-format=json",
|
|
]
|
|
if fix:
|
|
clippy_args.extend(["--fix", "--allow-dirty"])
|
|
log.debug("Run clippy with = {}".format(" ".join(clippy_args)))
|
|
completed_proc = subprocess.run(
|
|
clippy_args,
|
|
check=False, # non-zero exit codes are not unexpected
|
|
stdout=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
for l in completed_proc.stdout.splitlines():
|
|
handle_clippy_msg(config, l, log, root, None, lint_results)
|