52from typing
import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar
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],
99 removed_arg: List[str],
101 config_file_path: str,
103 line_filter: Optional[str],
106 warnings_as_errors: Optional[str],
107 exclude_header_filter: Optional[str],
108 allow_no_checks: bool,
109 store_check_profile: Optional[str],
111 """Gets a command line for clang-tidy."""
112 start = [clang_tidy_binary]
113 if allow_enabling_alpha_checkers:
114 start.append(
"-allow-enabling-analyzer-alpha-checkers")
115 if exclude_header_filter
is not None:
116 start.append(f
"--exclude-header-filter={exclude_header_filter}")
117 if header_filter
is not None:
118 start.append(f
"-header-filter={header_filter}")
119 if line_filter
is not None:
120 start.append(f
"-line-filter={line_filter}")
121 if use_color
is not None:
123 start.append(
"--use-color")
125 start.append(
"--use-color=false")
127 start.append(f
"-checks={checks}")
128 if tmpdir
is not None:
129 start.append(
"-export-fixes")
132 (handle, name) = tempfile.mkstemp(suffix=
".yaml", dir=tmpdir)
135 for arg
in extra_arg:
136 start.append(f
"-extra-arg={arg}")
137 for arg
in extra_arg_before:
138 start.append(f
"-extra-arg-before={arg}")
139 for arg
in removed_arg:
140 start.append(f
"-removed-arg={arg}")
141 start.append(f
"-p={build_path}")
143 start.append(
"-quiet")
145 start.append(f
"--config-file={config_file_path}")
147 start.append(f
"-config={config}")
148 for plugin
in plugins:
149 start.append(f
"-load={plugin}")
150 if warnings_as_errors:
151 start.append(f
"--warnings-as-errors={warnings_as_errors}")
153 start.append(
"--allow-no-checks")
154 if store_check_profile:
155 start.append(
"--enable-check-profile")
156 start.append(f
"--store-check-profile={store_check_profile}")
163 """Merge all replacement files in a directory into a single file"""
167 mergekey =
"Diagnostics"
169 for replacefile
in glob.iglob(os.path.join(tmpdir,
"*.yaml")):
170 content = yaml.safe_load(open(replacefile,
"r"))
173 merged.extend(content.get(mergekey, []))
180 output = {
"MainSourceFile":
"", mergekey: merged}
181 with open(mergefile,
"w")
as out:
182 yaml.safe_dump(output, out)
185 open(mergefile,
"w").close()
189 """Aggregate timing data from multiple profile JSON files"""
190 aggregated: Dict[str, float] = {}
192 for profile_file
in glob.iglob(os.path.join(profile_dir,
"*.json")):
194 with open(profile_file,
"r", encoding=
"utf-8")
as f:
196 profile_data: Dict[str, float] = data.get(
"profile", {})
198 for key, value
in profile_data.items():
199 if key.startswith(
"time.clang-tidy."):
200 if key
in aggregated:
201 aggregated[key] += value
203 aggregated[key] = value
204 except (json.JSONDecodeError, KeyError, IOError)
as e:
205 print(f
"Error: invalid json file {profile_file}: {e}", file=sys.stderr)
212 """Print aggregated checks profile data in the same format as clang-tidy"""
213 if not aggregated_data:
217 checkers: Dict[str, Dict[str, float]] = {}
218 for key, value
in aggregated_data.items():
219 parts = key.split(
".")
220 if len(parts) >= 4
and parts[0] ==
"time" and parts[1] ==
"clang-tidy":
221 checker_name =
".".join(
224 timing_type = parts[-1]
226 if checker_name
not in checkers:
227 checkers[checker_name] = {
"wall": 0.0,
"user": 0.0,
"sys": 0.0}
229 checkers[checker_name][timing_type] = value
234 total_user = sum(data[
"user"]
for data
in checkers.values())
235 total_sys = sum(data[
"sys"]
for data
in checkers.values())
236 total_wall = sum(data[
"wall"]
for data
in checkers.values())
238 sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted(
239 checkers.items(), key=
lambda x: x[1][
"user"] + x[1][
"sys"], reverse=
True
242 def print_stderr(*args: Any, **kwargs: Any) ->
None:
243 print(*args, file=sys.stderr, **kwargs)
246 "===-------------------------------------------------------------------------==="
248 print_stderr(
" clang-tidy checks profiling")
250 "===-------------------------------------------------------------------------==="
253 f
" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n"
257 total_combined = total_user + total_sys
258 user_width = len(f
"{total_user:.4f}")
259 sys_width = len(f
"{total_sys:.4f}")
260 combined_width = len(f
"{total_combined:.4f}")
261 wall_width = len(f
"{total_wall:.4f}")
265 user_header =
"---User Time---".center(user_width + additional_width)
266 sys_header =
"--System Time--".center(sys_width + additional_width)
267 combined_header =
"--User+System--".center(combined_width + additional_width)
268 wall_header =
"---Wall Time---".center(wall_width + additional_width)
271 f
" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---"
274 for checker_name, data
in sorted_checkers:
275 user_time = data[
"user"]
276 sys_time = data[
"sys"]
277 wall_time = data[
"wall"]
278 combined_time = user_time + sys_time
280 user_percent = (user_time / total_user * 100)
if total_user > 0
else 0
281 sys_percent = (sys_time / total_sys * 100)
if total_sys > 0
else 0
283 (combined_time / total_combined * 100)
if total_combined > 0
else 0
285 wall_percent = (wall_time / total_wall * 100)
if total_wall > 0
else 0
287 user_str = f
"{user_time:{user_width}.4f} ({user_percent:5.1f}%)"
288 sys_str = f
"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)"
289 combined_str = f
"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)"
290 wall_str = f
"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)"
293 f
" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}"
296 user_total_str = f
"{total_user:{user_width}.4f} (100.0%)"
297 sys_total_str = f
"{total_sys:{sys_width}.4f} (100.0%)"
298 combined_total_str = f
"{total_combined:{combined_width}.4f} (100.0%)"
299 wall_total_str = f
"{total_wall:{wall_width}.4f} (100.0%)"
302 f
" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total"
363 args: argparse.Namespace,
365 clang_tidy_binary: str,
368 store_check_profile: Optional[str],
371 Runs clang-tidy on a single file and returns the result.
380 args.allow_enabling_alpha_checkers,
382 args.extra_arg_before,
390 args.warnings_as_errors,
391 args.exclude_header_filter,
392 args.allow_no_checks,
397 process = await asyncio.create_subprocess_exec(
398 *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
401 stdout, stderr = await process.communicate()
403 except asyncio.CancelledError:
408 assert process.returncode
is not None
413 stdout.decode(
"UTF-8"),
414 stderr.decode(
"UTF-8"),
420 parser = argparse.ArgumentParser(
421 description=
"Runs clang-tidy over all files "
422 "in a compilation database. Requires "
423 "clang-tidy and clang-apply-replacements in "
424 "$PATH or in your build directory."
427 "-allow-enabling-alpha-checkers",
429 help=
"Allow alpha checkers from clang-analyzer.",
432 "-clang-tidy-binary", metavar=
"PATH", help=
"Path to clang-tidy binary."
435 "-clang-apply-replacements-binary",
437 help=
"Path to clang-apply-replacements binary.",
442 help=
"Checks filter, when not specified, use clang-tidy default.",
444 config_group = parser.add_mutually_exclusive_group()
445 config_group.add_argument(
448 help=
"Specifies a configuration in YAML/JSON format: "
449 " -config=\"{Checks: '*', "
450 ' CheckOptions: {x: y}}" '
451 "When the value is empty, clang-tidy will "
452 "attempt to find a file named .clang-tidy for "
453 "each source file in its parent directories.",
455 config_group.add_argument(
458 help=
"Specify the path of .clang-tidy or custom config "
459 "file: e.g. -config-file=/some/path/myTidyConfigFile. "
460 "This option internally works exactly the same way as "
461 "-config option after reading specified config file. "
462 "Use either -config-file or -config, not both.",
465 "-exclude-header-filter",
467 help=
"Regular expression matching the names of the "
468 "headers to exclude diagnostics from. Diagnostics from "
469 "the main file of each translation unit are always "
475 help=
"Regular expression matching the names of the "
476 "headers to output diagnostics from. Diagnostics from "
477 "the main file of each translation unit are always "
483 help=
"Regular expression matching the names of the "
484 "source files from compilation database to output "
490 help=
"List of files and line ranges to output diagnostics from.",
495 metavar=
"file_or_directory",
497 help=
"A directory or a yaml file to store suggested fixes in, "
498 "which can be applied with clang-apply-replacements. If the "
499 "parameter is a directory, the fixes of each compilation unit are "
500 "stored in individual yaml files in the directory.",
507 help=
"A directory to store suggested fixes in, which can be applied "
508 "with clang-apply-replacements. The fixes of each compilation unit are "
509 "stored in individual yaml files in the directory.",
515 help=
"Number of tidy instances to be run in parallel.",
521 help=
"Files to be processed (regex on path).",
523 parser.add_argument(
"-fix", action=
"store_true", help=
"apply fix-its.")
525 "-format", action=
"store_true", help=
"Reformat code after applying fixes."
530 help=
"The style of reformat code after applying fixes.",
537 help=
"Use colors in diagnostics, overriding clang-tidy's"
538 " default behavior. This option overrides the 'UseColor"
539 "' option in .clang-tidy file, if any.",
542 "-p", dest=
"build_path", help=
"Path used to read a compile command database."
549 help=
"Additional argument to append to the compiler command line.",
553 dest=
"extra_arg_before",
556 help=
"Additional argument to prepend to the compiler command line.",
563 help=
"Arguments to remove from the compiler command line.",
566 "-quiet", action=
"store_true", help=
"Run clang-tidy in quiet mode."
573 help=
"Load the specified plugin in clang-tidy.",
576 "-warnings-as-errors",
578 help=
"Upgrades warnings to errors. Same format as '-checks'.",
583 help=
"Allow empty enabled checks.",
586 "-enable-check-profile",
588 help=
"Enable per-check timing profiles, and print a report",
593 help=
"Hide progress",
595 args = parser.parse_args()
597 db_path =
"compile_commands.json"
599 if args.build_path
is not None:
600 build_path = args.build_path
605 clang_tidy_binary =
find_binary(args.clang_tidy_binary,
"clang-tidy", build_path)
609 args.clang_apply_replacements_binary,
"clang-apply-replacements", build_path
612 combine_fixes =
False
613 export_fixes_dir: Optional[str] =
None
614 delete_fixes_dir =
False
615 if args.export_fixes
is not None:
617 if args.export_fixes.endswith(os.path.sep)
and not os.path.isdir(
620 os.makedirs(args.export_fixes)
622 if not os.path.isdir(args.export_fixes):
625 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
630 if os.path.isdir(args.export_fixes):
631 export_fixes_dir = args.export_fixes
633 if export_fixes_dir
is None and (args.fix
or combine_fixes):
634 export_fixes_dir = tempfile.mkdtemp()
635 delete_fixes_dir =
True
637 profile_dir: Optional[str] =
None
638 if args.enable_check_profile:
639 profile_dir = tempfile.mkdtemp()
649 args.allow_enabling_alpha_checkers,
651 args.extra_arg_before,
659 args.warnings_as_errors,
660 args.exclude_header_filter,
661 args.allow_no_checks,
664 invocation.append(
"-list-checks")
665 invocation.append(
"-")
667 subprocess.check_call(
668 invocation, stdout=subprocess.DEVNULL
if args.quiet
else None
671 print(
"Unable to run clang-tidy.", file=sys.stderr)
675 with open(os.path.join(build_path, db_path))
as f:
676 database = json.load(f)
677 files = {os.path.abspath(os.path.join(e[
"directory"], e[
"file"]))
for e
in database}
678 number_files_in_database = len(files)
681 if args.source_filter:
683 source_filter_re = re.compile(args.source_filter)
686 "Error: unable to compile regex from arg -source-filter:",
689 traceback.print_exc()
691 files = {f
for f
in files
if source_filter_re.match(f)}
695 max_task = multiprocessing.cpu_count()
698 file_name_re = re.compile(
"|".join(args.files))
699 files = {f
for f
in files
if file_name_re.search(f)}
701 if not args.hide_progress:
703 f
"Running clang-tidy in {max_task} threads for {len(files)} files "
704 f
"out of {number_files_in_database} in compilation database ..."
708 semaphore = asyncio.Semaphore(max_task)
726 for i, coro
in enumerate(asyncio.as_completed(tasks)):
728 if result.returncode != 0:
730 if result.returncode < 0:
731 result.stderr += f
"{result.filename}: terminated by signal {-result.returncode}\n"
732 progress = f
"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]"
733 runtime = f
"[{result.elapsed:.1f}s]"
734 if not args.hide_progress:
735 print(f
"{progress}{runtime} {' '.join(result.invocation)}")
737 print(result.stdout, end=(
"" if result.stderr
else "\n"))
740 except asyncio.CancelledError:
741 if not args.hide_progress:
742 print(
"\nCtrl-C detected, goodbye.")
746 assert export_fixes_dir
747 shutil.rmtree(export_fixes_dir)
749 shutil.rmtree(profile_dir)
752 if args.enable_check_profile
and profile_dir:
759 print(
"No profiling data found.")
762 if not args.hide_progress:
763 print(f
"Writing fixes to {args.export_fixes} ...")
765 assert export_fixes_dir
768 print(
"Error exporting fixes.\n", file=sys.stderr)
769 traceback.print_exc()
773 if not args.hide_progress:
774 print(
"Applying fixes ...")
776 assert export_fixes_dir
777 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
779 print(
"Error applying fixes.\n", file=sys.stderr)
780 traceback.print_exc()
784 assert export_fixes_dir
785 shutil.rmtree(export_fixes_dir)
787 shutil.rmtree(profile_dir)
List[str] get_tidy_invocation(Optional[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, List[str] removed_arg, 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, Optional[str] store_check_profile)