diff options
Diffstat (limited to 'rcc/utils/clang-tidy/clang-tidy-diff.py')
| -rwxr-xr-x | rcc/utils/clang-tidy/clang-tidy-diff.py | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/rcc/utils/clang-tidy/clang-tidy-diff.py b/rcc/utils/clang-tidy/clang-tidy-diff.py new file mode 100755 index 0000000..059b2a2 --- /dev/null +++ b/rcc/utils/clang-tidy/clang-tidy-diff.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +# +# ===-----------------------------------------------------------------------===# +# +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ===-----------------------------------------------------------------------===# + +r""" +ClangTidy Diff Checker +====================== + +This script reads input from a unified diff, runs clang-tidy on all changed +files and outputs clang-tidy warnings in changed lines only. This is useful to +detect clang-tidy regressions in the lines touched by a specific patch. +Example usage for git/svn users: + + git diff -U0 HEAD^ | clang-tidy-diff.py -p1 + svn diff --diff-cmd=diff -x-U0 | \ + clang-tidy-diff.py -fix -checks=-*,modernize-use-override + +""" + +import argparse +import glob +import json +import multiprocessing +import os +import queue +import re +import shutil +import subprocess +import sys +import tempfile +import threading +import traceback +from pathlib import Path + +try: + import yaml +except ImportError: + yaml = None + + +def run_tidy(task_queue, lock, timeout, failed_files): + watchdog = None + while True: + command = task_queue.get() + try: + proc = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + if timeout is not None: + watchdog = threading.Timer(timeout, proc.kill) + watchdog.start() + + stdout, stderr = proc.communicate() + if proc.returncode != 0: + if proc.returncode < 0: + msg = "Terminated by signal %d : %s\n" % ( + -proc.returncode, + " ".join(command), + ) + stderr += msg.encode("utf-8") + failed_files.append(command) + + with lock: + if stdout: + sys.stdout.write(stdout.decode("utf-8") + "\n") + sys.stdout.flush() + if stderr: + sys.stderr.write(stderr.decode("utf-8") + "\n") + sys.stderr.flush() + except Exception as e: + with lock: + sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n") + finally: + with lock: + if not (timeout is None or watchdog is None): + if not watchdog.is_alive(): + sys.stderr.write( + "Terminated by timeout: " + " ".join(command) + "\n" + ) + watchdog.cancel() + task_queue.task_done() + + +def start_workers(max_tasks, tidy_caller, arguments): + for _ in range(max_tasks): + t = threading.Thread(target=tidy_caller, args=arguments) + t.daemon = True + t.start() + + +def merge_replacement_files(tmpdir, mergefile): + """Merge all replacement files in a directory into a single file""" + # The fixes suggested by clang-tidy >= 4.0.0 are given under + # the top level key 'Diagnostics' in the output yaml files + mergekey = "Diagnostics" + merged = [] + for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")): + content = yaml.safe_load(open(replacefile, "r")) + if not content: + continue # Skip empty files. + merged.extend(content.get(mergekey, [])) + + if merged: + # MainSourceFile: The key is required by the definition inside + # include/clang/Tooling/ReplacementsYaml.h, but the value + # is actually never used inside clang-apply-replacements, + # so we set it to '' here. + output = {"MainSourceFile": "", mergekey: merged} + with open(mergefile, "w") as out: + yaml.safe_dump(output, out) + else: + # Empty the file: + open(mergefile, "w").close() + + +def get_compiling_files(args): + """Read a compile_commands.json database and return a set of file paths""" + current_dir = Path.cwd() + compile_commands_json = ( + (current_dir / args.build_path) if args.build_path else current_dir + ) + compile_commands_json = compile_commands_json / "compile_commands.json" + files = set() + with open(compile_commands_json) as db_file: + db_json = json.load(db_file) + for entry in db_json: + if "file" not in entry: + continue + files.add(Path(entry["file"])) + return files + + +def main(): + parser = argparse.ArgumentParser( + description="Run clang-tidy against changed files, and " + "output diagnostics only for modified " + "lines." + ) + parser.add_argument( + "-clang-tidy-binary", + metavar="PATH", + default="clang-tidy", + help="path to clang-tidy binary", + ) + parser.add_argument( + "-p", + metavar="NUM", + default=0, + help="strip the smallest prefix containing P slashes", + ) + parser.add_argument( + "-regex", + metavar="PATTERN", + default=None, + help="custom pattern selecting file paths to check " + "(case sensitive, overrides -iregex)", + ) + parser.add_argument( + "-iregex", + metavar="PATTERN", + default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)", + help="custom pattern selecting file paths to check " + "(case insensitive, overridden by -regex)", + ) + parser.add_argument( + "-j", + type=int, + default=0, + help="number of tidy instances to be run in parallel.", + ) + parser.add_argument( + "-timeout", type=int, default=None, help="timeout per each file in seconds." + ) + parser.add_argument( + "-fix", action="store_true", default=False, help="apply suggested fixes" + ) + parser.add_argument( + "-checks", + help="checks filter, when not specified, use clang-tidy " "default", + default="", + ) + parser.add_argument( + "-config-file", + dest="config_file", + help="Specify the path of .clang-tidy or custom config file", + default="", + ) + parser.add_argument("-use-color", action="store_true", help="Use colors in output") + parser.add_argument( + "-path", dest="build_path", help="Path used to read a compile command database." + ) + if yaml: + parser.add_argument( + "-export-fixes", + metavar="FILE_OR_DIRECTORY", + dest="export_fixes", + help="A directory or a yaml file to store suggested fixes in, " + "which can be applied with clang-apply-replacements. If the " + "parameter is a directory, the fixes of each compilation unit are " + "stored in individual yaml files in the directory.", + ) + else: + parser.add_argument( + "-export-fixes", + metavar="DIRECTORY", + dest="export_fixes", + help="A directory to store suggested fixes in, which can be applied " + "with clang-apply-replacements. The fixes of each compilation unit are " + "stored in individual yaml files in the directory.", + ) + parser.add_argument( + "-extra-arg", + dest="extra_arg", + action="append", + default=[], + help="Additional argument to append to the compiler " "command line.", + ) + parser.add_argument( + "-extra-arg-before", + dest="extra_arg_before", + action="append", + default=[], + help="Additional argument to prepend to the compiler " "command line.", + ) + parser.add_argument( + "-removed-arg", + dest="removed_arg", + action="append", + default=[], + help="Arguments to remove from the compiler command line.", + ) + parser.add_argument( + "-quiet", + action="store_true", + default=False, + help="Run clang-tidy in quiet mode", + ) + parser.add_argument( + "-load", + dest="plugins", + action="append", + default=[], + help="Load the specified plugin in clang-tidy.", + ) + parser.add_argument( + "-allow-no-checks", + action="store_true", + help="Allow empty enabled checks.", + ) + parser.add_argument( + "-only-check-in-db", + dest="skip_non_compiling", + default=False, + action="store_true", + help="Only check files in the compilation database", + ) + parser.add_argument( + "-warnings-as-errors", + help="Upgrades clang-tidy warnings to errors. Same format as '-checks'.", + default="", + ) + parser.add_argument( + "-hide-progress", + action="store_true", + help="Hide progress", + ) + + clang_tidy_args = [] + argv = sys.argv[1:] + if "--" in argv: + clang_tidy_args.extend(argv[argv.index("--") :]) + argv = argv[: argv.index("--")] + + args = parser.parse_args(argv) + + compiling_files = get_compiling_files(args) if args.skip_non_compiling else None + + # Extract changed lines for each file. + filename = None + lines_by_file = {} + for line in sys.stdin: + match = re.search(r'^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line) + if match: + filename = match.group(2) + if filename is None: + continue + + if args.regex is not None: + if not re.match("^%s$" % args.regex, filename): + continue + else: + if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): + continue + + # Skip any files not in the compiling list + if ( + compiling_files is not None + and (Path.cwd() / filename) not in compiling_files + ): + continue + + match = re.search(r"^@@.*\+(\d+)(,(\d+))?", line) + if match: + start_line = int(match.group(1)) + line_count = 1 + if match.group(3): + line_count = int(match.group(3)) + if line_count == 0: + continue + end_line = start_line + line_count - 1 + lines_by_file.setdefault(filename, []).append([start_line, end_line]) + + if not any(lines_by_file): + print("No relevant changes found.") + sys.exit(0) + + max_task_count = args.j + if max_task_count == 0: + max_task_count = multiprocessing.cpu_count() + max_task_count = min(len(lines_by_file), max_task_count) + if not args.hide_progress: + print(f"Running clang-tidy in {max_task_count} threads...") + + combine_fixes = False + export_fixes_dir = None + delete_fixes_dir = False + if args.export_fixes is not None: + # if a directory is given, create it if it does not exist + if args.export_fixes.endswith(os.path.sep) and not os.path.isdir( + args.export_fixes + ): + os.makedirs(args.export_fixes) + + if not os.path.isdir(args.export_fixes): + if not yaml: + raise RuntimeError( + "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory." + ) + + combine_fixes = True + + if os.path.isdir(args.export_fixes): + export_fixes_dir = args.export_fixes + + if combine_fixes: + export_fixes_dir = tempfile.mkdtemp() + delete_fixes_dir = True + + # Tasks for clang-tidy. + task_queue = queue.Queue(max_task_count) + # A lock for console output. + lock = threading.Lock() + + # List of files with a non-zero return code. + failed_files = [] + + # Run a pool of clang-tidy workers. + start_workers( + max_task_count, run_tidy, (task_queue, lock, args.timeout, failed_files) + ) + + # Form the common args list. + common_clang_tidy_args = [] + if args.fix: + common_clang_tidy_args.append("-fix") + if args.checks != "": + common_clang_tidy_args.append("-checks=" + args.checks) + if args.config_file != "": + common_clang_tidy_args.append("-config-file=" + args.config_file) + if args.quiet: + common_clang_tidy_args.append("-quiet") + if args.build_path is not None: + common_clang_tidy_args.append("-p=%s" % args.build_path) + if args.use_color: + common_clang_tidy_args.append("--use-color") + if args.allow_no_checks: + common_clang_tidy_args.append("--allow-no-checks") + for arg in args.extra_arg: + common_clang_tidy_args.append("-extra-arg=%s" % arg) + for arg in args.extra_arg_before: + common_clang_tidy_args.append("-extra-arg-before=%s" % arg) + for arg in args.removed_arg: + common_clang_tidy_args.append("-removed-arg=%s" % arg) + for plugin in args.plugins: + common_clang_tidy_args.append("-load=%s" % plugin) + if args.warnings_as_errors: + common_clang_tidy_args.append("-warnings-as-errors=" + args.warnings_as_errors) + + for name in lines_by_file: + line_filter_json = json.dumps( + [{"name": name, "lines": lines_by_file[name]}], separators=(",", ":") + ) + + # Run clang-tidy on files containing changes. + command = [args.clang_tidy_binary] + command.append("-line-filter=" + line_filter_json) + if args.export_fixes is not None: + # Get a temporary file. We immediately close the handle so clang-tidy can + # overwrite it. + (handle, tmp_name) = tempfile.mkstemp(suffix=".yaml", dir=export_fixes_dir) + os.close(handle) + command.append("-export-fixes=" + tmp_name) + command.extend(common_clang_tidy_args) + command.append(name) + command.extend(clang_tidy_args) + + task_queue.put(command) + + # Application return code + return_code = 0 + + # Wait for all threads to be done. + task_queue.join() + # Application return code + return_code = 0 + if failed_files: + return_code = 1 + + if combine_fixes: + if not args.hide_progress: + print(f"Writing fixes to {args.export_fixes} ...") + try: + merge_replacement_files(export_fixes_dir, args.export_fixes) + except Exception: + sys.stderr.write("Error exporting fixes.\n") + traceback.print_exc() + return_code = 1 + + if delete_fixes_dir: + shutil.rmtree(export_fixes_dir) + sys.exit(return_code) + + +if __name__ == "__main__": + main() |
