15This script reads input from a unified diff, runs clang-tidy on all changed
16files and outputs clang-tidy warnings in changed lines only. This is useful to
17detect clang-tidy regressions in the lines touched by a specific patch.
18Example usage for git/svn users:
20 git diff -U0 HEAD^ | clang-tidy-diff.py -p1
21 svn diff --diff-cmd=diff -x-U0 | \
22 clang-tidy-diff.py -fix -checks=-*,modernize-use-override
44is_py2 = sys.version[0] ==
"2"
52def run_tidy(task_queue, lock, timeout, failed_files):
55 command = task_queue.get()
57 proc = subprocess.Popen(
58 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
61 if timeout
is not None:
62 watchdog = threading.Timer(timeout, proc.kill)
65 stdout, stderr = proc.communicate()
66 if proc.returncode != 0:
67 if proc.returncode < 0:
68 msg =
"Terminated by signal %d : %s\n" % (
72 stderr += msg.encode(
"utf-8")
73 failed_files.append(command)
76 sys.stdout.write(stdout.decode(
"utf-8") +
"\n")
79 sys.stderr.write(stderr.decode(
"utf-8") +
"\n")
81 except Exception
as e:
83 sys.stderr.write(
"Failed: " + str(e) +
": ".join(command) +
"\n")
86 if not (timeout
is None or watchdog
is None):
87 if not watchdog.is_alive():
89 "Terminated by timeout: " +
" ".join(command) +
"\n"
92 task_queue.task_done()
96 for _
in range(max_tasks):
97 t = threading.Thread(target=tidy_caller, args=arguments)
103 """Merge all replacement files in a directory into a single file"""
106 mergekey =
"Diagnostics"
108 for replacefile
in glob.iglob(os.path.join(tmpdir,
"*.yaml")):
109 content = yaml.safe_load(open(replacefile,
"r"))
112 merged.extend(content.get(mergekey, []))
119 output = {
"MainSourceFile":
"", mergekey: merged}
120 with open(mergefile,
"w")
as out:
121 yaml.safe_dump(output, out)
124 open(mergefile,
"w").close()
128 parser = argparse.ArgumentParser(
129 description=
"Run clang-tidy against changed files, and "
130 "output diagnostics only for modified "
134 "-clang-tidy-binary",
136 default=
"clang-tidy",
137 help=
"path to clang-tidy binary",
143 help=
"strip the smallest prefix containing P slashes",
149 help=
"custom pattern selecting file paths to check "
150 "(case sensitive, overrides -iregex)",
155 default=
r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
156 help=
"custom pattern selecting file paths to check "
157 "(case insensitive, overridden by -regex)",
163 help=
"number of tidy instances to be run in parallel.",
166 "-timeout", type=int, default=
None, help=
"timeout per each file in seconds."
169 "-fix", action=
"store_true", default=
False, help=
"apply suggested fixes"
173 help=
"checks filter, when not specified, use clang-tidy " "default",
179 help=
"Specify the path of .clang-tidy or custom config file",
182 parser.add_argument(
"-use-color", action=
"store_true", help=
"Use colors in output")
184 "-path", dest=
"build_path", help=
"Path used to read a compile command database."
189 metavar=
"FILE_OR_DIRECTORY",
191 help=
"A directory or a yaml file to store suggested fixes in, "
192 "which can be applied with clang-apply-replacements. If the "
193 "parameter is a directory, the fixes of each compilation unit are "
194 "stored in individual yaml files in the directory.",
201 help=
"A directory to store suggested fixes in, which can be applied "
202 "with clang-apply-replacements. The fixes of each compilation unit are "
203 "stored in individual yaml files in the directory.",
210 help=
"Additional argument to append to the compiler " "command line.",
214 dest=
"extra_arg_before",
217 help=
"Additional argument to prepend to the compiler " "command line.",
223 help=
"Run clang-tidy in quiet mode",
230 help=
"Load the specified plugin in clang-tidy.",
235 help=
"Allow empty enabled checks.",
241 clang_tidy_args.extend(argv[argv.index(
"--") :])
242 argv = argv[: argv.index(
"--")]
244 args = parser.parse_args(argv)
249 for line
in sys.stdin:
250 match = re.search(
'^\\+\\+\\+\\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
252 filename = match.group(2)
256 if args.regex
is not None:
257 if not re.match(
"^%s$" % args.regex, filename):
260 if not re.match(
"^%s$" % args.iregex, filename, re.IGNORECASE):
263 match = re.search(
r"^@@.*\+(\d+)(,(\d+))?", line)
265 start_line = int(match.group(1))
268 line_count = int(match.group(3))
271 end_line = start_line + line_count - 1
272 lines_by_file.setdefault(filename, []).append([start_line, end_line])
274 if not any(lines_by_file):
275 print(
"No relevant changes found.")
278 max_task_count = args.j
279 if max_task_count == 0:
280 max_task_count = multiprocessing.cpu_count()
281 max_task_count = min(len(lines_by_file), max_task_count)
283 combine_fixes =
False
284 export_fixes_dir =
None
285 delete_fixes_dir =
False
286 if args.export_fixes
is not None:
288 if args.export_fixes.endswith(os.path.sep)
and not os.path.isdir(
291 os.makedirs(args.export_fixes)
293 if not os.path.isdir(args.export_fixes):
296 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
301 if os.path.isdir(args.export_fixes):
302 export_fixes_dir = args.export_fixes
305 export_fixes_dir = tempfile.mkdtemp()
306 delete_fixes_dir =
True
309 task_queue = queue.Queue(max_task_count)
311 lock = threading.Lock()
318 max_task_count, run_tidy, (task_queue, lock, args.timeout, failed_files)
322 common_clang_tidy_args = []
324 common_clang_tidy_args.append(
"-fix")
325 if args.checks !=
"":
326 common_clang_tidy_args.append(
"-checks=" + args.checks)
327 if args.config_file !=
"":
328 common_clang_tidy_args.append(
"-config-file=" + args.config_file)
330 common_clang_tidy_args.append(
"-quiet")
331 if args.build_path
is not None:
332 common_clang_tidy_args.append(
"-p=%s" % args.build_path)
334 common_clang_tidy_args.append(
"--use-color")
335 if args.allow_no_checks:
336 common_clang_tidy_args.append(
"--allow-no-checks")
337 for arg
in args.extra_arg:
338 common_clang_tidy_args.append(
"-extra-arg=%s" % arg)
339 for arg
in args.extra_arg_before:
340 common_clang_tidy_args.append(
"-extra-arg-before=%s" % arg)
341 for plugin
in args.plugins:
342 common_clang_tidy_args.append(
"-load=%s" % plugin)
344 for name
in lines_by_file:
345 line_filter_json = json.dumps(
346 [{
"name": name,
"lines": lines_by_file[name]}], separators=(
",",
":")
350 command = [args.clang_tidy_binary]
351 command.append(
"-line-filter=" + line_filter_json)
352 if args.export_fixes
is not None:
355 (handle, tmp_name) = tempfile.mkstemp(suffix=
".yaml", dir=export_fixes_dir)
357 command.append(
"-export-fixes=" + tmp_name)
358 command.extend(common_clang_tidy_args)
360 command.extend(clang_tidy_args)
362 task_queue.put(command)
375 print(
"Writing fixes to " + args.export_fixes +
" ...")
379 sys.stderr.write(
"Error exporting fixes.\n")
380 traceback.print_exc()
384 shutil.rmtree(export_fixes_dir)
385 sys.exit(return_code)
388if __name__ ==
"__main__":
def merge_replacement_files(tmpdir, mergefile)
def start_workers(max_tasks, tidy_caller, arguments)
def run_tidy(task_queue, lock, timeout, failed_files)