blob: 1d70a5cd4bde0bda530b07b3cccc4ab261573149 [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
#
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Runs clippy on a set of gn targets or rust source files
import argparse
import json
import os
from pathlib import Path
import subprocess
import sys
import time
import rust
from rust import FUCHSIA_BUILD_DIR, HOST_PLATFORM, PREBUILT_THIRD_PARTY_DIR
def main():
args = parse_args()
build_dir = Path(args.out_dir) if args.out_dir else FUCHSIA_BUILD_DIR
generated_file = build_dir / "clippy_target_mapping.json"
if args.all:
clippy_targets = get_targets(
generated_file, set(), build_dir, get_all=True
)
elif args.files:
input_files = {os.path.relpath(f, build_dir) for f in args.input}
clippy_targets = get_targets(generated_file, input_files, build_dir)
if args.verbose and not args.get_outputs:
print("Found the following targets for those source files:")
print(*(t.gn_target for t in clippy_targets), sep="\n")
else:
clippy_targets = []
for target in args.input:
if target.endswith(".rs"):
print(
f"Warning: {target} looks like a source file rather than a target, "
"maybe you meant to use --files ?"
)
gn_target = rust.GnTarget(target, args.fuchsia_dir)
gn_target.label_name += ".clippy"
clippy_targets.append(gn_target)
output_files = [
os.path.relpath(t.gen_dir(build_dir).joinpath(t.label_name), build_dir)
for t in clippy_targets
]
if args.get_outputs:
print(*output_files, sep="\n")
return 0
if not output_files:
print("Error: Couldn't find any clippy outputs for those inputs")
return 1
if args.no_build:
run_time = 0
returncode = 0
else:
run_time = time.time()
returncode = build_targets(
output_files,
build_dir,
args.fuchsia_dir,
args.verbose,
args.quiet,
args.raw,
).returncode
lints = {}
for clippy_output in output_files:
clippy_output = build_dir / clippy_output
# If we failed to build all targets we can keep going and print any
# lints that were collected.
if returncode != 0:
if not clippy_output.exists():
continue
if os.path.getmtime(clippy_output) < run_time:
continue
with open(clippy_output) as f:
error_reported = False
for line in f:
try:
lint = json.loads(line)
except json.decoder.JSONDecodeError:
if not error_reported:
print(f"Malformed output: {clippy_output}")
returncode = 1
error_reported = True
continue
# filter out "n warnings emitted" messages
if not lint["spans"]:
continue
# filter out lints for files we didn't ask for
if args.files and all(
span["file_name"] not in input_files
for span in lint["spans"]
):
continue
lints[fingerprint_diagnostic(lint)] = lint
for lint in lints.values():
print(json.dumps(fix_paths(lint)) if args.raw else lint["rendered"])
if not args.raw:
print(len(lints), "warning(s) emitted\n")
return returncode
# To deduplicate lints, use the message, code, all top level spans, and macro
# expansion spans
def fingerprint_diagnostic(lint):
code = lint.get("code")
def expand_spans(span):
yield span
if expansion := span.get("expansion"):
yield from expand_spans(expansion["span"])
spans = [x for span in lint["spans"] for x in expand_spans(span)]
return (
lint["message"],
code.get("code") if code else None,
frozenset(
(x["file_name"], x["byte_start"], x["byte_end"]) for x in spans
),
)
# Rewrite paths in a diagnostic to be relative to the current directory.
def fix_paths(lint):
fix = lambda path: os.path.relpath(os.path.join(FUCHSIA_BUILD_DIR, path))
for span in lint["spans"]:
span["file_name"] = fix(span["file_name"])
lint["children"] = [fix_paths(child) for child in lint["children"]]
return lint
def build_targets(output_files, build_dir, fuchsia_dir, verbose, quiet, raw):
prebuilt = PREBUILT_THIRD_PARTY_DIR
if fuchsia_dir:
prebuilt = Path(fuchsia_dir) / "prebuilt" / "third_party"
ninja = [
prebuilt / "ninja" / HOST_PLATFORM / "ninja",
"-C",
build_dir,
"-k",
"0",
]
if verbose:
ninja += ["--verbose"]
if quiet:
ninja += ["--quiet"]
output = sys.stderr if raw else None
env = os.environ
env.setdefault("NINJA_PERSISTENT_MODE", "1")
return subprocess.run(ninja + output_files, stdout=output, env=env)
def get_targets(source_map, input_files, build_dir, get_all=False):
targets = set()
with open(source_map) as f:
raw = json.load(f)
for target in raw:
clippy_target = rust.GnTarget(target["clippy"], build_dir)
if get_all or any(f in input_files for f in target["src"]):
targets.add(clippy_target)
return targets
def parse_args():
parser = argparse.ArgumentParser(
description="Run cargo clippy on a set of targets or rust files"
)
parser.add_argument(
"--verbose", "-v", help="verbose", action="store_true", default=False
)
parser.add_argument(
"--quiet",
help="don't show progress status",
action="store_true",
default=False,
)
parser.add_argument(
"--files",
"-f",
action="store_true",
help="treat the inputs as source files rather than gn targets",
)
inputs = parser.add_mutually_exclusive_group(required=True)
inputs.add_argument("input", nargs="*", default=[])
inputs.add_argument(
"--all", action="store_true", help="run on all clippy targets"
)
advanced = parser.add_argument_group("advanced")
advanced.add_argument(
"--out-dir", help="path to the Fuchsia build directory"
)
advanced.add_argument(
"--fuchsia-dir", help="path to the Fuchsia root directory"
)
advanced.add_argument(
"--raw",
action="store_true",
help="emit full json rather than human readable messages",
)
advanced.add_argument(
"--get-outputs",
action="store_true",
help="emit a list of clippy output files rather than lints",
)
advanced.add_argument(
"--no-build",
action="store_true",
help="don't build the clippy output, instead expect that it already exists",
)
return parser.parse_args()
if __name__ == "__main__":
sys.exit(main())