mirror of
https://github.com/zebrajr/pytorch.git
synced 2025-12-06 12:20:52 +01:00
Update progress and error reporting in clang-tidy (#61672)
Summary: Pull Request resolved: https://github.com/pytorch/pytorch/pull/61672 This PR adds a progress bar to clang-tidy, and updates how it threads error codes (when run in parallel). The progress bar is disabled on GHA because backspace escape codes are not supported. It also adds a `--quiet` flag to the script. Screenshot of progress bar: <img width="955" alt="Screen Shot 2021-07-14 at 3 17 11 PM" src="https://user-images.githubusercontent.com/40111357/125686114-a8a7c154-3e65-43a8-aa8f-c1fb14d51d27.png"> Test Plan: Imported from OSS Reviewed By: malfet Differential Revision: D29763848 Pulled By: 1ntEgr8 fbshipit-source-id: cbd352593b279f279911bc3bb8d5ed54abd5f1d5
This commit is contained in:
parent
24a6eb3fda
commit
66c8d21d7b
42
.clang-tidy-oss
Normal file
42
.clang-tidy-oss
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
# NOTE there must be no spaces before the '-', so put the comma last.
|
||||
InheritParentConfig: true
|
||||
Checks: '
|
||||
bugprone-*,
|
||||
-bugprone-forward-declaration-namespace,
|
||||
-bugprone-macro-parentheses,
|
||||
-bugprone-lambda-function-name,
|
||||
-bugprone-reserved-identifier,
|
||||
cppcoreguidelines-*,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-interfaces-global-init,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-cstyle-cast,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-static-cast-downcast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-facebook-hte-RelativeInclude,
|
||||
hicpp-exception-baseclass,
|
||||
hicpp-avoid-goto,
|
||||
modernize-*,
|
||||
-modernize-concat-nested-namespaces,
|
||||
-modernize-return-braced-init-list,
|
||||
-modernize-use-auto,
|
||||
-modernize-use-default-member-init,
|
||||
-modernize-use-using,
|
||||
-modernize-use-trailing-return-type,
|
||||
performance-*,
|
||||
-performance-noexcept-move-constructor,
|
||||
-performance-unnecessary-value-param,
|
||||
'
|
||||
HeaderFilterRegex: 'torch/csrc/.*'
|
||||
AnalyzeTemporaryDtors: false
|
||||
WarningsAsErrors: '*'
|
||||
CheckOptions:
|
||||
...
|
||||
17
.github/workflows/lint.yml
vendored
17
.github/workflows/lint.yml
vendored
|
|
@ -332,13 +332,19 @@ jobs:
|
|||
run: |
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
wget -O pr.diff "https://patch-diff.githubusercontent.com/raw/pytorch/pytorch/pull/$PR_NUMBER.diff"
|
||||
- name: Generate build files
|
||||
run: |
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
python3 -m tools.linter.clang_tidy.generate_build_files
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
|
||||
# The Docker image has our custom build, so we don't need to install it
|
||||
python3 -m tools.linter.clang_tidy \
|
||||
--clang-tidy-exe "$(which clang-tidy)" \
|
||||
--diff-file pr.diff 2>&1 | tee "${GITHUB_WORKSPACE}"/clang-tidy-output.txt
|
||||
--diff-file pr.diff \
|
||||
--disable-progress-bar 2>&1 | tee "${GITHUB_WORKSPACE}"/clang-tidy-output.txt
|
||||
- name: Annotate output
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
|
|
@ -354,6 +360,15 @@ jobs:
|
|||
with:
|
||||
name: clang-tidy
|
||||
path: clang-tidy-output/
|
||||
- name: Check for warnings
|
||||
run: |
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
set -eu
|
||||
cat "${GITHUB_WORKSPACE}"/clang-tidy-output.txt
|
||||
if grep -Fq "Warnings detected!" "${GITHUB_WORKSPACE}"/clang-tidy-output.txt; then
|
||||
echo 'Please fix the above clang-tidy warnings.'
|
||||
false
|
||||
fi
|
||||
|
||||
cmakelint:
|
||||
runs-on: ubuntu-18.04
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ class ClangTidy(Check):
|
|||
common_options = [
|
||||
"--clang-tidy-exe",
|
||||
".clang-tidy-bin/clang-tidy",
|
||||
"--parallel",
|
||||
]
|
||||
|
||||
def filter_files(self, files: List[str]) -> List[str]:
|
||||
|
|
@ -303,14 +302,15 @@ class ClangTidy(Check):
|
|||
|
||||
async def quick(self, files: List[str]) -> CommandResult:
|
||||
return await shell_cmd(
|
||||
[sys.executable, "tools/linter/clang_tidy", "--paths"]
|
||||
[sys.executable, "-m", "tools.linter.clang_tidy", "--paths"]
|
||||
+ [os.path.join(REPO_ROOT, f) for f in files]
|
||||
+ self.common_options,
|
||||
)
|
||||
|
||||
async def full(self) -> None:
|
||||
await shell_cmd(
|
||||
[sys.executable, "tools/linter/clang_tidy"] + self.common_options
|
||||
[sys.executable, "-m", "tools.linter.clang_tidy"] + self.common_options,
|
||||
redirect=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
from typing import List
|
||||
|
||||
|
||||
|
|
@ -77,9 +78,9 @@ DEFAULTS = {
|
|||
"paths": ["torch/csrc/"],
|
||||
"include-dir": ["/usr/lib/llvm-11/include/openmp"] + clang_search_dirs(),
|
||||
"clang-tidy-exe": INSTALLATION_PATH,
|
||||
"parallel": True,
|
||||
"compile-commands-dir": "build",
|
||||
"config-file": ".clang-tidy",
|
||||
"config-file": ".clang-tidy-oss",
|
||||
"disable-progress-bar": False,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -132,24 +133,12 @@ def parse_args() -> argparse.Namespace:
|
|||
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("-q", "--quiet", action="store_true", help="Don't print output")
|
||||
parser.add_argument(
|
||||
"--config-file",
|
||||
default=DEFAULTS["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",
|
||||
default=DEFAULTS["parallel"],
|
||||
help="Run clang tidy in parallel per-file (requires ninja to be installed).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--print-include-paths",
|
||||
action="store_true",
|
||||
|
|
@ -168,6 +157,12 @@ def parse_args() -> argparse.Namespace:
|
|||
action="store_true",
|
||||
help="Add NOLINT to suppress clang-tidy violations",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-progress-bar",
|
||||
action="store_true",
|
||||
default=DEFAULTS["disable-progress-bar"],
|
||||
help="Disable the progress bar",
|
||||
)
|
||||
parser.add_argument(
|
||||
"extra_args", nargs="*", help="Extra arguments to forward to clang-tidy"
|
||||
)
|
||||
|
|
@ -185,15 +180,15 @@ def main() -> None:
|
|||
|
||||
if not exists:
|
||||
msg = (
|
||||
"Could not find '.clang-tidy-bin/clang-tidy'\n"
|
||||
"You can install it by running:\n"
|
||||
" python3 tools/linter/install/clang_tidy.py"
|
||||
f"Could not find '{options.clang_tidy_exe}'\n"
|
||||
+ "We provide a custom build of clang-tidy that has additional checks.\n"
|
||||
+ "You can install it by running:\n"
|
||||
+ "$ python3 tools/linter/install/clang_tidy.py"
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return_code = run(options)
|
||||
if return_code != 0:
|
||||
raise RuntimeError("Warnings found in clang-tidy output!")
|
||||
result, _ = run(options)
|
||||
sys.exit(result.returncode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import os
|
|||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import asyncio
|
||||
import shlex
|
||||
|
|
@ -35,12 +34,293 @@ Patterns = collections.namedtuple("Patterns", "positive, negative")
|
|||
# compiled -- translation units are, of which there is one per implementation
|
||||
# (c/cc/cpp) file.
|
||||
DEFAULT_FILE_PATTERN = re.compile(r"^.*\.c(c|pp)?$")
|
||||
|
||||
CLANG_WARNING_PATTERN = re.compile(r"([^:]+):(\d+):\d+:\s+warning:.*\[([^\]]+)\]")
|
||||
|
||||
|
||||
CLANG_WARNING_PATTERN = re.compile(
|
||||
r"([^:]+):(\d+):\d+:\s+(warning|error):.*\[([^\]]+)\]"
|
||||
)
|
||||
# Set from command line arguments in main().
|
||||
VERBOSE = False
|
||||
QUIET = False
|
||||
|
||||
|
||||
def log(*args: Any, **kwargs: Any) -> None:
|
||||
if not QUIET:
|
||||
print(*args, **kwargs)
|
||||
|
||||
|
||||
class CommandResult:
|
||||
def __init__(self, returncode: int, stdout: str, stderr: str):
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout.strip()
|
||||
self.stderr = stderr.strip()
|
||||
|
||||
def failed(self) -> bool:
|
||||
return self.returncode != 0
|
||||
|
||||
def __add__(self, other: "CommandResult") -> "CommandResult":
|
||||
return CommandResult(
|
||||
self.returncode + other.returncode,
|
||||
f"{self.stdout}\n{other.stdout}",
|
||||
f"{self.stderr}\n{other.stderr}",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.stdout}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"returncode: {self.returncode}\n"
|
||||
+ f"stdout: {self.stdout}\n"
|
||||
+ f"stderr: {self.stderr}"
|
||||
)
|
||||
|
||||
|
||||
class ProgressMeter:
|
||||
def __init__(
|
||||
self, num_items: int, start_msg: str = "", disable_progress_bar: bool = False
|
||||
) -> None:
|
||||
self.num_items = num_items
|
||||
self.num_processed = 0
|
||||
self.width = 80
|
||||
self.disable_progress_bar = disable_progress_bar
|
||||
|
||||
# helper escape sequences
|
||||
self._clear_to_end = "\x1b[2K"
|
||||
self._move_to_previous_line = "\x1b[F"
|
||||
self._move_to_start_of_line = "\r"
|
||||
self._move_to_next_line = "\n"
|
||||
|
||||
if self.disable_progress_bar:
|
||||
log(start_msg)
|
||||
else:
|
||||
self._write(
|
||||
start_msg
|
||||
+ self._move_to_next_line
|
||||
+ "[>"
|
||||
+ (self.width * " ")
|
||||
+ "]"
|
||||
+ self._move_to_start_of_line
|
||||
)
|
||||
self._flush()
|
||||
|
||||
def _write(self, s: str) -> None:
|
||||
sys.stderr.write(s)
|
||||
|
||||
def _flush(self) -> None:
|
||||
sys.stderr.flush()
|
||||
|
||||
def update(self, msg: str) -> None:
|
||||
if self.disable_progress_bar:
|
||||
return
|
||||
|
||||
# Once we've processed all items, clear the progress bar
|
||||
if self.num_processed == self.num_items - 1:
|
||||
self._write(self._clear_to_end)
|
||||
return
|
||||
|
||||
# NOP if we've already processed all items
|
||||
if self.num_processed > self.num_items:
|
||||
return
|
||||
|
||||
self.num_processed += 1
|
||||
|
||||
self._write(
|
||||
self._move_to_previous_line
|
||||
+ self._clear_to_end
|
||||
+ msg
|
||||
+ self._move_to_next_line
|
||||
)
|
||||
|
||||
progress = int((self.num_processed / self.num_items) * self.width)
|
||||
padding = self.width - progress
|
||||
self._write(
|
||||
self._move_to_start_of_line
|
||||
+ self._clear_to_end
|
||||
+ f"({self.num_processed} of {self.num_items}) "
|
||||
+ f"[{progress*'='}>{padding*' '}]"
|
||||
+ self._move_to_start_of_line
|
||||
)
|
||||
self._flush()
|
||||
|
||||
def print(self, msg: str) -> None:
|
||||
if QUIET:
|
||||
return
|
||||
elif self.disable_progress_bar:
|
||||
print(msg)
|
||||
else:
|
||||
self._write(
|
||||
self._clear_to_end
|
||||
+ self._move_to_previous_line
|
||||
+ self._clear_to_end
|
||||
+ msg
|
||||
+ self._move_to_next_line
|
||||
+ self._move_to_next_line
|
||||
)
|
||||
self._flush()
|
||||
|
||||
|
||||
class ClangTidyWarning:
|
||||
def __init__(self, name: str, occurrences: List[Tuple[str, int]]):
|
||||
self.name = name
|
||||
self.occurrences = occurrences
|
||||
|
||||
def __str__(self) -> str:
|
||||
base = f"[{self.name}] occurred {len(self.occurrences)} times\n"
|
||||
for occ in self.occurrences:
|
||||
base += f" {occ[0]}:{occ[1]}\n"
|
||||
return base
|
||||
|
||||
|
||||
async def run_shell_command(
|
||||
cmd: List[str], on_completed: Any = None, *args: Any
|
||||
) -> CommandResult:
|
||||
"""Executes a shell command and runs an optional callback when complete"""
|
||||
if VERBOSE:
|
||||
log("Running: ", " ".join(cmd))
|
||||
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
" ".join(shlex.quote(x) for x in cmd), # type: ignore[attr-defined]
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
output = await proc.communicate()
|
||||
result = CommandResult(
|
||||
returncode=proc.returncode if proc.returncode is not None else -1,
|
||||
stdout=output[0].decode("utf-8").strip(),
|
||||
stderr=output[1].decode("utf-8").strip(),
|
||||
)
|
||||
|
||||
if on_completed:
|
||||
on_completed(result, *args)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _run_clang_tidy_in_parallel(
|
||||
commands: List[Tuple[List[str], str]], disable_progress_bar: bool
|
||||
) -> CommandResult:
|
||||
progress_meter = ProgressMeter(
|
||||
len(commands),
|
||||
f"Processing {len(commands)} clang-tidy jobs",
|
||||
disable_progress_bar=disable_progress_bar,
|
||||
)
|
||||
|
||||
async def gather_with_concurrency(n: int, tasks: List[Any]) -> Any:
|
||||
semaphore = asyncio.Semaphore(n)
|
||||
|
||||
async def sem_task(task: Any) -> Any:
|
||||
async with semaphore:
|
||||
return await task
|
||||
|
||||
return await asyncio.gather(
|
||||
*(sem_task(task) for task in tasks), return_exceptions=True
|
||||
)
|
||||
|
||||
async def helper() -> Any:
|
||||
def on_completed(result: CommandResult, filename: str) -> None:
|
||||
if result.failed():
|
||||
msg = str(result) if not VERBOSE else repr(result)
|
||||
progress_meter.print(msg)
|
||||
progress_meter.update(f"Processed {filename}")
|
||||
|
||||
coros = [
|
||||
run_shell_command(cmd, on_completed, filename)
|
||||
for (cmd, filename) in commands
|
||||
]
|
||||
return await gather_with_concurrency(multiprocessing.cpu_count(), coros)
|
||||
|
||||
results = await helper()
|
||||
return sum(results, CommandResult(0, "", ""))
|
||||
|
||||
|
||||
async def _run_clang_tidy(
|
||||
options: Any, line_filters: List[Dict[str, Any]], files: Iterable[str]
|
||||
) -> CommandResult:
|
||||
"""Executes the actual clang-tidy command in the shell."""
|
||||
|
||||
base = [options.clang_tidy_exe]
|
||||
|
||||
# Apply common options
|
||||
base += ["-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.
|
||||
base += [
|
||||
"-config",
|
||||
json.dumps(yaml.load(config, Loader=yaml.SafeLoader)),
|
||||
]
|
||||
if options.print_include_paths:
|
||||
base += ["--extra-arg", "-v"]
|
||||
if options.include_dir:
|
||||
for dir in options.include_dir:
|
||||
base += ["--extra-arg", f"-I{dir}"]
|
||||
base += options.extra_args
|
||||
if line_filters:
|
||||
base += ["-line-filter", json.dumps(line_filters)]
|
||||
|
||||
# Apply per-file options
|
||||
commands = []
|
||||
for f in files:
|
||||
command = list(base) + [map_filename(options.compile_commands_dir, f)]
|
||||
commands.append((command, f))
|
||||
|
||||
if options.dry_run:
|
||||
return CommandResult(0, str([c for c, _ in commands]), "")
|
||||
|
||||
return await _run_clang_tidy_in_parallel(commands, options.disable_progress_bar)
|
||||
|
||||
|
||||
def extract_warnings(
|
||||
output: str, base_dir: str = "."
|
||||
) -> Tuple[Dict[str, Dict[int, Set[str]]], List[ClangTidyWarning]]:
|
||||
warn2occ: Dict[str, List[Tuple[str, int]]] = {}
|
||||
fixes: Dict[str, Dict[int, Set[str]]] = {}
|
||||
for line in output.splitlines():
|
||||
p = CLANG_WARNING_PATTERN.match(line)
|
||||
if p is None:
|
||||
continue
|
||||
if os.path.isabs(p.group(1)):
|
||||
path = os.path.abspath(p.group(1))
|
||||
else:
|
||||
path = os.path.abspath(os.path.join(base_dir, p.group(1)))
|
||||
line_no = int(p.group(2))
|
||||
|
||||
# Filter out any options (which start with '-')
|
||||
warning_names = set([w for w in p.group(4).split(",") if not w.startswith("-")])
|
||||
|
||||
for name in warning_names:
|
||||
if name not in warn2occ:
|
||||
warn2occ[name] = []
|
||||
warn2occ[name].append((path, line_no))
|
||||
|
||||
if path not in fixes:
|
||||
fixes[path] = {}
|
||||
if line_no not in fixes[path]:
|
||||
fixes[path][line_no] = set()
|
||||
fixes[path][line_no].update(warning_names)
|
||||
|
||||
warnings = [ClangTidyWarning(name, sorted(occ)) for name, occ in warn2occ.items()]
|
||||
|
||||
return fixes, warnings
|
||||
|
||||
|
||||
def apply_nolint(fname: str, warnings: Dict[int, Set[str]]) -> None:
|
||||
with open(fname, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
line_offset = -1 # As in .cpp files lines are numbered starting from 1
|
||||
for line_no in sorted(warnings.keys()):
|
||||
nolint_diagnostics = ",".join(warnings[line_no])
|
||||
line_no += line_offset
|
||||
indent = " " * (len(lines[line_no]) - len(lines[line_no].lstrip(" ")))
|
||||
lines.insert(line_no, f"{indent}// NOLINTNEXTLINE({nolint_diagnostics})\n")
|
||||
line_offset += 1
|
||||
|
||||
with open(fname, mode="w") as f:
|
||||
f.write("".join(lines))
|
||||
|
||||
|
||||
# Functions for correct handling of "ATen/native/cpu" mapping
|
||||
|
|
@ -63,19 +343,6 @@ def map_filenames(build_folder: str, fnames: Iterable[str]) -> List[str]:
|
|||
return [map_filename(build_folder, fname) for fname in fnames]
|
||||
|
||||
|
||||
def run_shell_command(arguments: List[str]) -> str:
|
||||
"""Executes a shell command."""
|
||||
if VERBOSE:
|
||||
print(" ".join(arguments))
|
||||
try:
|
||||
output = subprocess.check_output(arguments).decode().strip()
|
||||
except subprocess.CalledProcessError as error:
|
||||
error_output = error.output.decode().strip()
|
||||
raise RuntimeError(f"Error executing {' '.join(arguments)}: {error_output}")
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def split_negative_from_positive_patterns(patterns: Iterable[str]) -> Patterns:
|
||||
"""Separates negative patterns (that start with a dash) from positive patterns"""
|
||||
positive, negative = [], []
|
||||
|
|
@ -109,20 +376,20 @@ def get_file_patterns(globs: Iterable[str], regexes: Iterable[str]) -> Patterns:
|
|||
def filter_files(files: Iterable[str], file_patterns: Patterns) -> Iterable[str]:
|
||||
"""Returns all files that match any of the patterns."""
|
||||
if VERBOSE:
|
||||
print("Filtering with these file patterns: {}".format(file_patterns))
|
||||
log("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("{} omitted due to file filters".format(file))
|
||||
log(f"{file} omitted due to file filters")
|
||||
|
||||
|
||||
def get_all_files(paths: List[str]) -> List[str]:
|
||||
async def get_all_files(paths: List[str]) -> List[str]:
|
||||
"""Returns all files that are tracked by git in the given paths."""
|
||||
output = run_shell_command(["git", "ls-files"] + paths)
|
||||
return output.split("\n")
|
||||
output = await run_shell_command(["git", "ls-files"] + paths)
|
||||
return str(output).strip().splitlines()
|
||||
|
||||
|
||||
def find_changed_lines(diff: str) -> Dict[str, List[Tuple[int, int]]]:
|
||||
|
|
@ -150,138 +417,6 @@ def find_changed_lines(diff: str) -> Dict[str, List[Tuple[int, int]]]:
|
|||
return dict(files)
|
||||
|
||||
|
||||
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: Iterable[List[str]]) -> str:
|
||||
"""runs all the commands in parallel with ninja, commands is a List[List[str]]"""
|
||||
|
||||
async def run_command(cmd: List[str]) -> str:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
" ".join(shlex.quote(x) for x in cmd), # type: ignore[attr-defined]
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await proc.communicate()
|
||||
return f">>>\nstdout:\n{stdout.decode()}\nstderr:\n{stderr.decode()}\n<<<"
|
||||
|
||||
async def gather_with_concurrency(n: int, tasks: List[Any]) -> Any:
|
||||
semaphore = asyncio.Semaphore(n)
|
||||
|
||||
async def sem_task(task: Any) -> Any:
|
||||
async with semaphore:
|
||||
return await task
|
||||
|
||||
return await asyncio.gather(
|
||||
*(sem_task(task) for task in tasks), return_exceptions=True
|
||||
)
|
||||
|
||||
async def helper() -> Any:
|
||||
coros = [run_command(cmd) for cmd in commands]
|
||||
return await gather_with_concurrency(multiprocessing.cpu_count(), coros)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
results = loop.run_until_complete(helper())
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def run_clang_tidy(
|
||||
options: Any, line_filters: List[Dict[str, Any]], files: Iterable[str]
|
||||
) -> str:
|
||||
"""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, Loader=yaml.SafeLoader)),
|
||||
]
|
||||
if options.print_include_paths:
|
||||
command += ["--extra-arg", "-v"]
|
||||
if options.include_dir:
|
||||
for dir in options.include_dir:
|
||||
command += ["--extra-arg", f"-I{dir}"]
|
||||
|
||||
command += options.extra_args
|
||||
|
||||
if line_filters:
|
||||
command += ["-line-filter", json.dumps(line_filters)]
|
||||
|
||||
if options.parallel:
|
||||
commands = [
|
||||
list(command) + [map_filename(options.compile_commands_dir, f)]
|
||||
for f in files
|
||||
]
|
||||
output = run_shell_commands_in_parallel(commands)
|
||||
else:
|
||||
command += map_filenames(options.compile_commands_dir, 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 extract_warnings(
|
||||
output: str, base_dir: str = "."
|
||||
) -> Dict[str, Dict[int, Set[str]]]:
|
||||
rc: Dict[str, Dict[int, Set[str]]] = {}
|
||||
for line in output.split("\n"):
|
||||
p = CLANG_WARNING_PATTERN.match(line)
|
||||
if p is None:
|
||||
continue
|
||||
if os.path.isabs(p.group(1)):
|
||||
path = os.path.abspath(p.group(1))
|
||||
else:
|
||||
path = os.path.abspath(os.path.join(base_dir, p.group(1)))
|
||||
line_no = int(p.group(2))
|
||||
warnings = set(p.group(3).split(","))
|
||||
if path not in rc:
|
||||
rc[path] = {}
|
||||
if line_no not in rc[path]:
|
||||
rc[path][line_no] = set()
|
||||
rc[path][line_no].update(warnings)
|
||||
return rc
|
||||
|
||||
|
||||
def apply_nolint(fname: str, warnings: Dict[int, Set[str]]) -> None:
|
||||
with open(fname, encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
|
||||
line_offset = -1 # As in .cpp files lines are numbered starting from 1
|
||||
for line_no in sorted(warnings.keys()):
|
||||
nolint_diagnostics = ",".join(warnings[line_no])
|
||||
line_no += line_offset
|
||||
indent = " " * (len(lines[line_no]) - len(lines[line_no].lstrip(" ")))
|
||||
lines.insert(line_no, f"{indent}// NOLINTNEXTLINE({nolint_diagnostics})\n")
|
||||
line_offset += 1
|
||||
|
||||
with open(fname, mode="w") as f:
|
||||
f.write("".join(lines))
|
||||
|
||||
|
||||
def filter_from_diff(
|
||||
paths: List[str], diffs: List[str]
|
||||
) -> Tuple[List[str], List[Dict[Any, Any]]]:
|
||||
|
|
@ -311,52 +446,62 @@ def filter_from_diff_file(
|
|||
return filter_from_diff(paths, [diff])
|
||||
|
||||
|
||||
def filter_default(paths: List[str]) -> Tuple[List[str], List[Dict[Any, Any]]]:
|
||||
return get_all_files(paths), []
|
||||
async def filter_default(paths: List[str]) -> Tuple[List[str], List[Dict[Any, Any]]]:
|
||||
return await get_all_files(paths), []
|
||||
|
||||
|
||||
def run(options: Any) -> int:
|
||||
# This flag is pervasive enough to set it globally. It makes the code
|
||||
async def _run(options: Any) -> Tuple[CommandResult, List[ClangTidyWarning]]:
|
||||
# These flags are pervasive enough to set it globally. It makes the code
|
||||
# cleaner compared to threading it through every single function.
|
||||
global VERBOSE
|
||||
global QUIET
|
||||
VERBOSE = options.verbose
|
||||
QUIET = options.quiet
|
||||
|
||||
# Normalize the paths first.
|
||||
# Normalize the paths first
|
||||
paths = [path.rstrip("/") for path in options.paths]
|
||||
|
||||
# Filter files
|
||||
if options.diff_file:
|
||||
files, line_filters = filter_from_diff_file(options.paths, options.diff_file)
|
||||
else:
|
||||
files, line_filters = filter_default(options.paths)
|
||||
files, line_filters = await filter_default(options.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.
|
||||
# clang-tidy errors when it does not get input files.
|
||||
if not files:
|
||||
print("No files detected.")
|
||||
sys.exit()
|
||||
log("No files detected")
|
||||
return CommandResult(0, "", ""), []
|
||||
|
||||
clang_tidy_output = run_clang_tidy(options, line_filters, files)
|
||||
warnings = extract_warnings(
|
||||
clang_tidy_output, base_dir=options.compile_commands_dir
|
||||
result = await _run_clang_tidy(options, line_filters, files)
|
||||
fixes, warnings = extract_warnings(
|
||||
result.stdout, base_dir=options.compile_commands_dir
|
||||
)
|
||||
|
||||
if options.suppress_diagnostics:
|
||||
warnings = extract_warnings(
|
||||
clang_tidy_output, base_dir=options.compile_commands_dir
|
||||
)
|
||||
for fname in warnings.keys():
|
||||
for fname in fixes.keys():
|
||||
mapped_fname = map_filename(options.compile_commands_dir, fname)
|
||||
print(f"Applying fixes to {mapped_fname}")
|
||||
apply_nolint(fname, warnings[fname])
|
||||
log(f"Applying fixes to {mapped_fname}")
|
||||
apply_nolint(fname, fixes[fname])
|
||||
if os.path.relpath(fname) != mapped_fname:
|
||||
shutil.copyfile(fname, mapped_fname)
|
||||
|
||||
pwd = os.getcwd() + "/"
|
||||
if options.dry_run:
|
||||
print(clang_tidy_output)
|
||||
for line in clang_tidy_output.splitlines():
|
||||
if line.startswith(pwd):
|
||||
print(line[len(pwd) :])
|
||||
log(result)
|
||||
elif result.failed():
|
||||
# If you change this message, update the error checking logic in
|
||||
# .github/workflows/lint.yml
|
||||
msg = "Warnings detected!"
|
||||
log(msg)
|
||||
log("Summary:")
|
||||
for w in warnings:
|
||||
log(str(w))
|
||||
|
||||
return len(warnings.keys())
|
||||
return result, warnings
|
||||
|
||||
|
||||
def run(options: Any) -> Tuple[CommandResult, List[ClangTidyWarning]]:
|
||||
loop = asyncio.get_event_loop()
|
||||
return loop.run_until_complete(_run(options))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user