14Parallel clang-tidy runner
15==========================
17Runs clang-tidy over all files in a compilation database. Requires clang-tidy
18and clang-apply-replacements in $PATH.
21- Run clang-tidy on all files in the current working directory with a default
22 set of checks and show warnings
in the cpp files
and all project headers.
23 run-clang-tidy.py $PWD
25- Fix all header guards.
26 run-clang-tidy.py -fix -checks=-*,llvm-header-guard
28- Fix all header guards included
from clang-tidy
and header guards
29 for clang-tidy headers.
30 run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \
31 -header-filter=extra/clang-tidy
33Compilation database setup:
34http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
39from dataclasses import dataclass
51from types import ModuleType
52from typing import Any, Awaitable, Callable, List, Optional, TypeVar
55yaml: Optional[ModuleType] = None
63 """Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
66 if val
in [
"",
"true",
"1"]:
68 elif val
in [
"false",
"0"]:
72 raise argparse.ArgumentTypeError(
73 f
"'{val}' is invalid value for boolean argument! Try 0 or 1."
78 """Adjusts the directory until a compilation database is found."""
79 result = os.path.realpath(
"./")
80 while not os.path.isfile(os.path.join(result, path)):
81 parent = os.path.dirname(result)
83 print(
"Error: could not find compilation database.")
91 clang_tidy_binary: str,
93 tmpdir: Optional[str],
95 header_filter: Optional[str],
96 allow_enabling_alpha_checkers: bool,
98 extra_arg_before: List[str],
100 config_file_path: str,
102 line_filter: Optional[str],
105 warnings_as_errors: Optional[str],
106 exclude_header_filter: Optional[str],
107 allow_no_checks: bool,
109 """Gets a command line for clang-tidy."""
110 start = [clang_tidy_binary]
111 if allow_enabling_alpha_checkers:
112 start.append(
"-allow-enabling-analyzer-alpha-checkers")
113 if exclude_header_filter
is not None:
114 start.append(f
"--exclude-header-filter={exclude_header_filter}")
115 if header_filter
is not None:
116 start.append(f
"-header-filter={header_filter}")
117 if line_filter
is not None:
118 start.append(f
"-line-filter={line_filter}")
119 if use_color
is not None:
121 start.append(
"--use-color")
123 start.append(
"--use-color=false")
125 start.append(f
"-checks={checks}")
126 if tmpdir
is not None:
127 start.append(
"-export-fixes")
130 (handle, name) = tempfile.mkstemp(suffix=
".yaml", dir=tmpdir)
133 for arg
in extra_arg:
134 start.append(f
"-extra-arg={arg}")
135 for arg
in extra_arg_before:
136 start.append(f
"-extra-arg-before={arg}")
137 start.append(f
"-p={build_path}")
139 start.append(
"-quiet")
141 start.append(f
"--config-file={config_file_path}")
143 start.append(f
"-config={config}")
144 for plugin
in plugins:
145 start.append(f
"-load={plugin}")
146 if warnings_as_errors:
147 start.append(f
"--warnings-as-errors={warnings_as_errors}")
149 start.append(
"--allow-no-checks")
155 """Merge all replacement files in a directory into a single file"""
159 mergekey =
"Diagnostics"
161 for replacefile
in glob.iglob(os.path.join(tmpdir,
"*.yaml")):
162 content = yaml.safe_load(open(replacefile,
"r"))
165 merged.extend(content.get(mergekey, []))
172 output = {
"MainSourceFile":
"", mergekey: merged}
173 with open(mergefile,
"w")
as out:
174 yaml.safe_dump(output, out)
177 open(mergefile,
"w").close()
181 """Get the path for a binary or exit"""
183 if shutil.which(arg):
187 f
"error: passed binary '{arg}' was not found or is not executable"
190 built_path = os.path.join(build_path,
"bin", name)
191 binary = shutil.which(name)
or shutil.which(built_path)
195 raise SystemExit(f
"error: failed to find {name} in $PATH or at {built_path}")
199 args: argparse.Namespace, clang_apply_replacements_binary: str, tmpdir: str
201 """Calls clang-apply-fixes on a given directory."""
202 invocation = [clang_apply_replacements_binary]
203 invocation.append(
"-ignore-insert-conflict")
205 invocation.append(
"-format")
207 invocation.append(f
"-style={args.style}")
208 invocation.append(tmpdir)
209 subprocess.call(invocation)
217 semaphore: asyncio.Semaphore,
218 f: Callable[..., Awaitable[T]],
222 async with semaphore:
223 return await f(*args, **kwargs)
229 invocation: List[str]
237 args: argparse.Namespace,
239 clang_tidy_binary: str,
244 Runs clang-tidy on a single file and returns the result.
253 args.allow_enabling_alpha_checkers,
255 args.extra_arg_before,
262 args.warnings_as_errors,
263 args.exclude_header_filter,
264 args.allow_no_checks,
268 process = await asyncio.create_subprocess_exec(
269 *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
272 stdout, stderr = await process.communicate()
274 except asyncio.CancelledError:
279 assert process.returncode
is not None
284 stdout.decode(
"UTF-8"),
285 stderr.decode(
"UTF-8"),
291 parser = argparse.ArgumentParser(
292 description=
"Runs clang-tidy over all files "
293 "in a compilation database. Requires "
294 "clang-tidy and clang-apply-replacements in "
295 "$PATH or in your build directory."
298 "-allow-enabling-alpha-checkers",
300 help=
"Allow alpha checkers from clang-analyzer.",
303 "-clang-tidy-binary", metavar=
"PATH", help=
"Path to clang-tidy binary."
306 "-clang-apply-replacements-binary",
308 help=
"Path to clang-apply-replacements binary.",
313 help=
"Checks filter, when not specified, use clang-tidy default.",
315 config_group = parser.add_mutually_exclusive_group()
316 config_group.add_argument(
319 help=
"Specifies a configuration in YAML/JSON format: "
320 " -config=\"{Checks: '*', "
321 ' CheckOptions: {x: y}}" '
322 "When the value is empty, clang-tidy will "
323 "attempt to find a file named .clang-tidy for "
324 "each source file in its parent directories.",
326 config_group.add_argument(
329 help=
"Specify the path of .clang-tidy or custom config "
330 "file: e.g. -config-file=/some/path/myTidyConfigFile. "
331 "This option internally works exactly the same way as "
332 "-config option after reading specified config file. "
333 "Use either -config-file or -config, not both.",
336 "-exclude-header-filter",
338 help=
"Regular expression matching the names of the "
339 "headers to exclude diagnostics from. Diagnostics from "
340 "the main file of each translation unit are always "
346 help=
"Regular expression matching the names of the "
347 "headers to output diagnostics from. Diagnostics from "
348 "the main file of each translation unit are always "
354 help=
"Regular expression matching the names of the "
355 "source files from compilation database to output "
361 help=
"List of files with line ranges to filter the warnings.",
366 metavar=
"file_or_directory",
368 help=
"A directory or a yaml file to store suggested fixes in, "
369 "which can be applied with clang-apply-replacements. If the "
370 "parameter is a directory, the fixes of each compilation unit are "
371 "stored in individual yaml files in the directory.",
378 help=
"A directory to store suggested fixes in, which can be applied "
379 "with clang-apply-replacements. The fixes of each compilation unit are "
380 "stored in individual yaml files in the directory.",
386 help=
"Number of tidy instances to be run in parallel.",
392 help=
"Files to be processed (regex on path).",
394 parser.add_argument(
"-fix", action=
"store_true", help=
"apply fix-its.")
396 "-format", action=
"store_true", help=
"Reformat code after applying fixes."
401 help=
"The style of reformat code after applying fixes.",
408 help=
"Use colors in diagnostics, overriding clang-tidy's"
409 " default behavior. This option overrides the 'UseColor"
410 "' option in .clang-tidy file, if any.",
413 "-p", dest=
"build_path", help=
"Path used to read a compile command database."
420 help=
"Additional argument to append to the compiler command line.",
424 dest=
"extra_arg_before",
427 help=
"Additional argument to prepend to the compiler command line.",
430 "-quiet", action=
"store_true", help=
"Run clang-tidy in quiet mode."
437 help=
"Load the specified plugin in clang-tidy.",
440 "-warnings-as-errors",
442 help=
"Upgrades warnings to errors. Same format as '-checks'.",
447 help=
"Allow empty enabled checks.",
449 args = parser.parse_args()
451 db_path =
"compile_commands.json"
453 if args.build_path
is not None:
454 build_path = args.build_path
459 clang_tidy_binary =
find_binary(args.clang_tidy_binary,
"clang-tidy", build_path)
463 args.clang_apply_replacements_binary,
"clang-apply-replacements", build_path
466 combine_fixes =
False
467 export_fixes_dir: Optional[str] =
None
468 delete_fixes_dir =
False
469 if args.export_fixes
is not None:
471 if args.export_fixes.endswith(os.path.sep)
and not os.path.isdir(
474 os.makedirs(args.export_fixes)
476 if not os.path.isdir(args.export_fixes):
479 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
484 if os.path.isdir(args.export_fixes):
485 export_fixes_dir = args.export_fixes
487 if export_fixes_dir
is None and (args.fix
or combine_fixes):
488 export_fixes_dir = tempfile.mkdtemp()
489 delete_fixes_dir =
True
499 args.allow_enabling_alpha_checkers,
501 args.extra_arg_before,
508 args.warnings_as_errors,
509 args.exclude_header_filter,
510 args.allow_no_checks,
512 invocation.append(
"-list-checks")
513 invocation.append(
"-")
515 subprocess.check_call(
516 invocation, stdout=subprocess.DEVNULL
if args.quiet
else None
519 print(
"Unable to run clang-tidy.", file=sys.stderr)
523 with open(os.path.join(build_path, db_path))
as f:
524 database = json.load(f)
525 files = {os.path.abspath(os.path.join(e[
"directory"], e[
"file"]))
for e
in database}
526 number_files_in_database = len(files)
529 if args.source_filter:
531 source_filter_re = re.compile(args.source_filter)
534 "Error: unable to compile regex from arg -source-filter:",
537 traceback.print_exc()
539 files = {f
for f
in files
if source_filter_re.match(f)}
543 max_task = multiprocessing.cpu_count()
546 file_name_re = re.compile(
"|".join(args.files))
547 files = {f
for f
in files
if file_name_re.search(f)}
550 "Running clang-tidy for",
553 number_files_in_database,
554 "in compilation database ...",
558 semaphore = asyncio.Semaphore(max_task)
575 for i, coro
in enumerate(asyncio.as_completed(tasks)):
577 if result.returncode != 0:
579 if result.returncode < 0:
580 result.stderr += f
"{result.filename}: terminated by signal {-result.returncode}\n"
581 progress = f
"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]"
582 runtime = f
"[{result.elapsed:.1f}s]"
583 print(f
"{progress}{runtime} {' '.join(result.invocation)}")
585 print(result.stdout, end=(
"" if result.stderr
else "\n"))
588 except asyncio.CancelledError:
589 print(
"\nCtrl-C detected, goodbye.")
593 assert export_fixes_dir
594 shutil.rmtree(export_fixes_dir)
598 print(f
"Writing fixes to {args.export_fixes} ...")
600 assert export_fixes_dir
603 print(
"Error exporting fixes.\n", file=sys.stderr)
604 traceback.print_exc()
608 print(
"Applying fixes ...")
610 assert export_fixes_dir
611 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
613 print(
"Error applying fixes.\n", file=sys.stderr)
614 traceback.print_exc()
618 assert export_fixes_dir
619 shutil.rmtree(export_fixes_dir)
623if __name__ ==
"__main__":
626 except KeyboardInterrupt:
List[str] get_tidy_invocation(str f, str clang_tidy_binary, str checks, Optional[str] tmpdir, str build_path, Optional[str] header_filter, bool allow_enabling_alpha_checkers, List[str] extra_arg, List[str] extra_arg_before, bool quiet, str config_file_path, str config, Optional[str] line_filter, bool use_color, List[str] plugins, Optional[str] warnings_as_errors, Optional[str] exclude_header_filter, bool allow_no_checks)
str find_compilation_database(str path)
str find_binary(str arg, str name, str build_path)
None apply_fixes(argparse.Namespace args, str clang_apply_replacements_binary, str tmpdir)
None merge_replacement_files(str tmpdir, str mergefile)
ClangTidyResult run_tidy(argparse.Namespace args, str name, str clang_tidy_binary, str tmpdir, str build_path)
T run_with_semaphore(asyncio.Semaphore semaphore, Callable[..., Awaitable[T]] f, *Any args, **Any kwargs)