aboutsummaryrefslogtreecommitdiff
path: root/rcc/utils/clang-tidy/clang-tidy-diff.py
diff options
context:
space:
mode:
Diffstat (limited to 'rcc/utils/clang-tidy/clang-tidy-diff.py')
-rwxr-xr-xrcc/utils/clang-tidy/clang-tidy-diff.py442
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()