mirror of
https://github.com/pytorch/pytorch.git
synced 2025-10-21 05:34:18 +08:00
Summary: Pull Request resolved: https://github.com/pytorch/pytorch/pull/19082 When you have just one line of deletions, just as with additions, there is no count printed. Without this fix, we ignore all globs with single-line deletions when selecting which lines were changed. When all the changes in the file were single-line, this meant no line-filtering at all! Differential Revision: D14860426 fbshipit-source-id: c60e9d84f9520871fc0c08fa8c772c227d06fa27
296 lines
9.6 KiB
Python
296 lines
9.6 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
A driver script to run clang-tidy on changes detected via git.
|
|
|
|
By default, clang-tidy runs on all files you point it at. This means that even
|
|
if you changed only parts of that file, you will get warnings for the whole
|
|
file. This script has the ability to ask git for the exact lines that have
|
|
changed since a particular git revision, and makes clang-tidy only lint those.
|
|
This makes it much less overhead to integrate in CI and much more relevant to
|
|
developers. This git-enabled mode is optional, and full scans of a directory
|
|
tree are also possible. In both cases, the script allows filtering files via
|
|
glob or regular expressions.
|
|
"""
|
|
|
|
import argparse
|
|
import collections
|
|
import fnmatch
|
|
import json
|
|
import os.path
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
try:
|
|
from shlex import quote
|
|
except ImportError:
|
|
from pipes import quote
|
|
|
|
Patterns = collections.namedtuple("Patterns", "positive, negative")
|
|
|
|
|
|
# NOTE: Clang-tidy cannot lint headers directly, because headers are not
|
|
# compiled -- translation units are, of which there is one per implementation
|
|
# (c/cc/cpp) file.
|
|
DEFAULT_FILE_PATTERN = re.compile(r".*\.c(c|pp)?")
|
|
|
|
# @@ -start,count +start,count @@
|
|
CHUNK_PATTERN = r"^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@"
|
|
|
|
|
|
# Set from command line arguments in main().
|
|
VERBOSE = False
|
|
|
|
|
|
def run_shell_command(arguments):
|
|
"""Executes a shell command."""
|
|
if VERBOSE:
|
|
print(" ".join(arguments))
|
|
try:
|
|
output = subprocess.check_output(arguments).decode().strip()
|
|
except subprocess.CalledProcessError:
|
|
_, error, _ = sys.exc_info()
|
|
error_output = error.output.decode().strip()
|
|
raise RuntimeError("Error executing {}: {}".format(" ".join(arguments), error_output))
|
|
|
|
return output
|
|
|
|
|
|
def split_negative_from_positive_patterns(patterns):
|
|
"""Separates negative patterns (that start with a dash) from positive patterns"""
|
|
positive, negative = [], []
|
|
for pattern in patterns:
|
|
if pattern.startswith("-"):
|
|
negative.append(pattern[1:])
|
|
else:
|
|
positive.append(pattern)
|
|
|
|
return Patterns(positive, negative)
|
|
|
|
|
|
def get_file_patterns(globs, regexes):
|
|
"""Returns a list of compiled regex objects from globs and regex pattern strings."""
|
|
# fnmatch.translate converts a glob into a regular expression.
|
|
# https://docs.python.org/2/library/fnmatch.html#fnmatch.translate
|
|
glob = split_negative_from_positive_patterns(globs)
|
|
regexes = split_negative_from_positive_patterns(regexes)
|
|
|
|
positive_regexes = regexes.positive + [fnmatch.translate(g) for g in glob.positive]
|
|
negative_regexes = regexes.negative + [fnmatch.translate(g) for g in glob.negative]
|
|
|
|
positive_patterns = [re.compile(regex) for regex in positive_regexes] or [
|
|
DEFAULT_FILE_PATTERN
|
|
]
|
|
negative_patterns = [re.compile(regex) for regex in negative_regexes]
|
|
|
|
return Patterns(positive_patterns, negative_patterns)
|
|
|
|
|
|
def filter_files(files, file_patterns):
|
|
"""Returns all files that match any of the patterns."""
|
|
if VERBOSE:
|
|
print("Filtering with these file patterns: {}".format(file_patterns))
|
|
for file in files:
|
|
if not any(n.match(file) for n in file_patterns.negative):
|
|
if any(p.match(file) for p in file_patterns.positive):
|
|
yield file
|
|
continue
|
|
if VERBOSE:
|
|
print("{} ommitted due to file filters".format(file))
|
|
|
|
|
|
def get_changed_files(revision, paths):
|
|
"""Runs git diff to get the paths of all changed files."""
|
|
# --diff-filter AMU gets us files that are (A)dded, (M)odified or (U)nmerged (in the working copy).
|
|
# --name-only makes git diff return only the file paths, without any of the source changes.
|
|
command = "git diff-index --diff-filter=AMU --ignore-all-space --name-only"
|
|
output = run_shell_command(shlex.split(command) + [revision] + paths)
|
|
return output.split("\n")
|
|
|
|
|
|
def get_all_files(paths):
|
|
"""Returns all files that are tracked by git in the given paths."""
|
|
output = run_shell_command(["git", "ls-files"] + paths)
|
|
return output.split("\n")
|
|
|
|
|
|
def get_changed_lines(revision, filename):
|
|
"""Runs git diff to get the line ranges of all file changes."""
|
|
command = shlex.split("git diff-index --unified=0") + [revision, filename]
|
|
output = run_shell_command(command)
|
|
changed_lines = []
|
|
for chunk in re.finditer(CHUNK_PATTERN, output, re.MULTILINE):
|
|
start = int(chunk.group(1))
|
|
count = int(chunk.group(2) or 1)
|
|
changed_lines.append([start, start + count])
|
|
|
|
return {"name": filename, "lines": changed_lines}
|
|
|
|
ninja_template = """
|
|
rule do_cmd
|
|
command = $cmd
|
|
description = Running clang-tidy
|
|
|
|
{build_rules}
|
|
"""
|
|
|
|
build_template = """
|
|
build {i}: do_cmd
|
|
cmd = {cmd}
|
|
"""
|
|
|
|
|
|
def run_shell_commands_in_parallel(commands):
|
|
"""runs all the commands in parallel with ninja, commands is a List[List[str]]"""
|
|
build_entries = [build_template.format(i=i, cmd=' '.join([quote(s) for s in command]))
|
|
for i, command in enumerate(commands)]
|
|
|
|
file_contents = ninja_template.format(build_rules='\n'.join(build_entries)).encode()
|
|
f = tempfile.NamedTemporaryFile(delete=False)
|
|
try:
|
|
f.write(file_contents)
|
|
f.close()
|
|
return run_shell_command(['ninja', '-f', f.name])
|
|
finally:
|
|
os.unlink(f.name)
|
|
|
|
|
|
def run_clang_tidy(options, line_filters, files):
|
|
"""Executes the actual clang-tidy command in the shell."""
|
|
command = [options.clang_tidy_exe, "-p", options.compile_commands_dir]
|
|
if not options.config_file and os.path.exists(".clang-tidy"):
|
|
options.config_file = ".clang-tidy"
|
|
if options.config_file:
|
|
import yaml
|
|
|
|
with open(options.config_file) as config:
|
|
# Here we convert the YAML config file to a JSON blob.
|
|
command += ["-config", json.dumps(yaml.load(config))]
|
|
command += options.extra_args
|
|
|
|
if line_filters:
|
|
command += ["-line-filter", json.dumps(line_filters)]
|
|
|
|
if options.parallel:
|
|
commands = [list(command) + [f] for f in files]
|
|
output = run_shell_commands_in_parallel(commands)
|
|
else:
|
|
command += files
|
|
if options.dry_run:
|
|
command = [re.sub(r"^([{[].*[]}])$", r"'\1'", arg) for arg in command]
|
|
return " ".join(command)
|
|
|
|
output = run_shell_command(command)
|
|
|
|
if not options.keep_going and "[clang-diagnostic-error]" in output:
|
|
message = "Found clang-diagnostic-errors in clang-tidy output: {}"
|
|
raise RuntimeError(message.format(output))
|
|
|
|
return output
|
|
|
|
|
|
def parse_options():
|
|
"""Parses the command line options."""
|
|
parser = argparse.ArgumentParser(description="Run Clang-Tidy (on your Git changes)")
|
|
parser.add_argument(
|
|
"-e",
|
|
"--clang-tidy-exe",
|
|
default="clang-tidy",
|
|
help="Path to clang-tidy executable",
|
|
)
|
|
parser.add_argument(
|
|
"-g",
|
|
"--glob",
|
|
action="append",
|
|
default=[],
|
|
help="Only lint files that match these glob patterns "
|
|
"(see documentation for `fnmatch` for supported syntax)."
|
|
"If a pattern starts with a - the search is negated for that pattern.",
|
|
)
|
|
parser.add_argument(
|
|
"-x",
|
|
"--regex",
|
|
action="append",
|
|
default=[],
|
|
help="Only lint files that match these regular expressions (from the start of the filename). "
|
|
"If a pattern starts with a - the search is negated for that pattern.",
|
|
)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--compile-commands-dir",
|
|
default="build",
|
|
help="Path to the folder containing compile_commands.json",
|
|
)
|
|
parser.add_argument(
|
|
"-d", "--diff", help="Git revision to diff against to get changes"
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--paths",
|
|
nargs="+",
|
|
default=["."],
|
|
help="Lint only the given paths (recursively)",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Only show the command to be executed, without running it",
|
|
)
|
|
parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
|
|
parser.add_argument(
|
|
"--config-file",
|
|
help="Path to a clang-tidy config file. Defaults to '.clang-tidy'.",
|
|
)
|
|
parser.add_argument(
|
|
"-k",
|
|
"--keep-going",
|
|
action="store_true",
|
|
help="Don't error on compiler errors (clang-diagnostic-error)",
|
|
)
|
|
parser.add_argument(
|
|
"-j",
|
|
"--parallel",
|
|
action="store_true",
|
|
help="Run clang tidy in parallel per-file (requires ninja to be installed).",
|
|
)
|
|
parser.add_argument(
|
|
"extra_args", nargs="*", help="Extra arguments to forward to clang-tidy"
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
options = parse_options()
|
|
|
|
# This flag is pervasive enough to set it globally. It makes the code
|
|
# cleaner compared to threading it through every single function.
|
|
global VERBOSE
|
|
VERBOSE = options.verbose
|
|
|
|
# Normalize the paths first.
|
|
paths = [path.rstrip("/") for path in options.paths]
|
|
if options.diff:
|
|
files = get_changed_files(options.diff, paths)
|
|
else:
|
|
files = get_all_files(paths)
|
|
file_patterns = get_file_patterns(options.glob, options.regex)
|
|
files = list(filter_files(files, file_patterns))
|
|
|
|
# clang-tidy error's when it does not get input files.
|
|
if not files:
|
|
print("No files detected.")
|
|
sys.exit()
|
|
|
|
line_filters = []
|
|
if options.diff:
|
|
line_filters = [get_changed_lines(options.diff, f) for f in files]
|
|
|
|
print(run_clang_tidy(options, line_filters, files))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|