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],
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,
108 store_check_profile: Optional[str],
110 """Gets a command line for clang-tidy."""
111 start = [clang_tidy_binary]
112 if allow_enabling_alpha_checkers:
113 start.append(
"-allow-enabling-analyzer-alpha-checkers")
114 if exclude_header_filter
is not None:
115 start.append(f
"--exclude-header-filter={exclude_header_filter}")
116 if header_filter
is not None:
117 start.append(f
"-header-filter={header_filter}")
118 if line_filter
is not None:
119 start.append(f
"-line-filter={line_filter}")
120 if use_color
is not None:
122 start.append(
"--use-color")
124 start.append(
"--use-color=false")
126 start.append(f
"-checks={checks}")
127 if tmpdir
is not None:
128 start.append(
"-export-fixes")
131 (handle, name) = tempfile.mkstemp(suffix=
".yaml", dir=tmpdir)
134 for arg
in extra_arg:
135 start.append(f
"-extra-arg={arg}")
136 for arg
in extra_arg_before:
137 start.append(f
"-extra-arg-before={arg}")
138 start.append(f
"-p={build_path}")
140 start.append(
"-quiet")
142 start.append(f
"--config-file={config_file_path}")
144 start.append(f
"-config={config}")
145 for plugin
in plugins:
146 start.append(f
"-load={plugin}")
147 if warnings_as_errors:
148 start.append(f
"--warnings-as-errors={warnings_as_errors}")
150 start.append(
"--allow-no-checks")
151 if store_check_profile:
152 start.append(
"--enable-check-profile")
153 start.append(f
"--store-check-profile={store_check_profile}")
160 """Merge all replacement files in a directory into a single file"""
164 mergekey =
"Diagnostics"
166 for replacefile
in glob.iglob(os.path.join(tmpdir,
"*.yaml")):
167 content = yaml.safe_load(open(replacefile,
"r"))
170 merged.extend(content.get(mergekey, []))
177 output = {
"MainSourceFile":
"", mergekey: merged}
178 with open(mergefile,
"w")
as out:
179 yaml.safe_dump(output, out)
182 open(mergefile,
"w").close()
186 """Aggregate timing data from multiple profile JSON files"""
187 aggregated: Dict[str, float] = {}
189 for profile_file
in glob.iglob(os.path.join(profile_dir,
"*.json")):
191 with open(profile_file,
"r", encoding=
"utf-8")
as f:
193 profile_data: Dict[str, float] = data.get(
"profile", {})
195 for key, value
in profile_data.items():
196 if key.startswith(
"time.clang-tidy."):
197 if key
in aggregated:
198 aggregated[key] += value
200 aggregated[key] = value
201 except (json.JSONDecodeError, KeyError, IOError)
as e:
202 print(f
"Error: invalid json file {profile_file}: {e}", file=sys.stderr)
209 """Print aggregated checks profile data in the same format as clang-tidy"""
210 if not aggregated_data:
214 checkers: Dict[str, Dict[str, float]] = {}
215 for key, value
in aggregated_data.items():
216 parts = key.split(
".")
217 if len(parts) >= 4
and parts[0] ==
"time" and parts[1] ==
"clang-tidy":
218 checker_name =
".".join(
221 timing_type = parts[-1]
223 if checker_name
not in checkers:
224 checkers[checker_name] = {
"wall": 0.0,
"user": 0.0,
"sys": 0.0}
226 checkers[checker_name][timing_type] = value
231 total_user = sum(data[
"user"]
for data
in checkers.values())
232 total_sys = sum(data[
"sys"]
for data
in checkers.values())
233 total_wall = sum(data[
"wall"]
for data
in checkers.values())
235 sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted(
236 checkers.items(), key=
lambda x: x[1][
"user"] + x[1][
"sys"], reverse=
True
239 def print_stderr(*args: Any, **kwargs: Any) ->
None:
240 print(*args, file=sys.stderr, **kwargs)
243 "===-------------------------------------------------------------------------==="
245 print_stderr(
" clang-tidy checks profiling")
247 "===-------------------------------------------------------------------------==="
250 f
" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n"
254 total_combined = total_user + total_sys
255 user_width = len(f
"{total_user:.4f}")
256 sys_width = len(f
"{total_sys:.4f}")
257 combined_width = len(f
"{total_combined:.4f}")
258 wall_width = len(f
"{total_wall:.4f}")
262 user_header =
"---User Time---".center(user_width + additional_width)
263 sys_header =
"--System Time--".center(sys_width + additional_width)
264 combined_header =
"--User+System--".center(combined_width + additional_width)
265 wall_header =
"---Wall Time---".center(wall_width + additional_width)
268 f
" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---"
271 for checker_name, data
in sorted_checkers:
272 user_time = data[
"user"]
273 sys_time = data[
"sys"]
274 wall_time = data[
"wall"]
275 combined_time = user_time + sys_time
277 user_percent = (user_time / total_user * 100)
if total_user > 0
else 0
278 sys_percent = (sys_time / total_sys * 100)
if total_sys > 0
else 0
280 (combined_time / total_combined * 100)
if total_combined > 0
else 0
282 wall_percent = (wall_time / total_wall * 100)
if total_wall > 0
else 0
284 user_str = f
"{user_time:{user_width}.4f} ({user_percent:5.1f}%)"
285 sys_str = f
"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)"
286 combined_str = f
"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)"
287 wall_str = f
"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)"
290 f
" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}"
293 user_total_str = f
"{total_user:{user_width}.4f} (100.0%)"
294 sys_total_str = f
"{total_sys:{sys_width}.4f} (100.0%)"
295 combined_total_str = f
"{total_combined:{combined_width}.4f} (100.0%)"
296 wall_total_str = f
"{total_wall:{wall_width}.4f} (100.0%)"
299 f
" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total"
360 args: argparse.Namespace,
362 clang_tidy_binary: str,
365 store_check_profile: Optional[str],
368 Runs clang-tidy on a single file and returns the result.
377 args.allow_enabling_alpha_checkers,
379 args.extra_arg_before,
386 args.warnings_as_errors,
387 args.exclude_header_filter,
388 args.allow_no_checks,
393 process = await asyncio.create_subprocess_exec(
394 *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
397 stdout, stderr = await process.communicate()
399 except asyncio.CancelledError:
404 assert process.returncode
is not None
409 stdout.decode(
"UTF-8"),
410 stderr.decode(
"UTF-8"),
416 parser = argparse.ArgumentParser(
417 description=
"Runs clang-tidy over all files "
418 "in a compilation database. Requires "
419 "clang-tidy and clang-apply-replacements in "
420 "$PATH or in your build directory."
423 "-allow-enabling-alpha-checkers",
425 help=
"Allow alpha checkers from clang-analyzer.",
428 "-clang-tidy-binary", metavar=
"PATH", help=
"Path to clang-tidy binary."
431 "-clang-apply-replacements-binary",
433 help=
"Path to clang-apply-replacements binary.",
438 help=
"Checks filter, when not specified, use clang-tidy default.",
440 config_group = parser.add_mutually_exclusive_group()
441 config_group.add_argument(
444 help=
"Specifies a configuration in YAML/JSON format: "
445 " -config=\"{Checks: '*', "
446 ' CheckOptions: {x: y}}" '
447 "When the value is empty, clang-tidy will "
448 "attempt to find a file named .clang-tidy for "
449 "each source file in its parent directories.",
451 config_group.add_argument(
454 help=
"Specify the path of .clang-tidy or custom config "
455 "file: e.g. -config-file=/some/path/myTidyConfigFile. "
456 "This option internally works exactly the same way as "
457 "-config option after reading specified config file. "
458 "Use either -config-file or -config, not both.",
461 "-exclude-header-filter",
463 help=
"Regular expression matching the names of the "
464 "headers to exclude diagnostics from. Diagnostics from "
465 "the main file of each translation unit are always "
471 help=
"Regular expression matching the names of the "
472 "headers to output diagnostics from. Diagnostics from "
473 "the main file of each translation unit are always "
479 help=
"Regular expression matching the names of the "
480 "source files from compilation database to output "
486 help=
"List of files and line ranges to output diagnostics from.",
491 metavar=
"file_or_directory",
493 help=
"A directory or a yaml file to store suggested fixes in, "
494 "which can be applied with clang-apply-replacements. If the "
495 "parameter is a directory, the fixes of each compilation unit are "
496 "stored in individual yaml files in the directory.",
503 help=
"A directory to store suggested fixes in, which can be applied "
504 "with clang-apply-replacements. The fixes of each compilation unit are "
505 "stored in individual yaml files in the directory.",
511 help=
"Number of tidy instances to be run in parallel.",
517 help=
"Files to be processed (regex on path).",
519 parser.add_argument(
"-fix", action=
"store_true", help=
"apply fix-its.")
521 "-format", action=
"store_true", help=
"Reformat code after applying fixes."
526 help=
"The style of reformat code after applying fixes.",
533 help=
"Use colors in diagnostics, overriding clang-tidy's"
534 " default behavior. This option overrides the 'UseColor"
535 "' option in .clang-tidy file, if any.",
538 "-p", dest=
"build_path", help=
"Path used to read a compile command database."
545 help=
"Additional argument to append to the compiler command line.",
549 dest=
"extra_arg_before",
552 help=
"Additional argument to prepend to the compiler command line.",
555 "-quiet", action=
"store_true", help=
"Run clang-tidy in quiet mode."
562 help=
"Load the specified plugin in clang-tidy.",
565 "-warnings-as-errors",
567 help=
"Upgrades warnings to errors. Same format as '-checks'.",
572 help=
"Allow empty enabled checks.",
575 "-enable-check-profile",
577 help=
"Enable per-check timing profiles, and print a report",
582 help=
"Hide progress",
584 args = parser.parse_args()
586 db_path =
"compile_commands.json"
588 if args.build_path
is not None:
589 build_path = args.build_path
594 clang_tidy_binary =
find_binary(args.clang_tidy_binary,
"clang-tidy", build_path)
598 args.clang_apply_replacements_binary,
"clang-apply-replacements", build_path
601 combine_fixes =
False
602 export_fixes_dir: Optional[str] =
None
603 delete_fixes_dir =
False
604 if args.export_fixes
is not None:
606 if args.export_fixes.endswith(os.path.sep)
and not os.path.isdir(
609 os.makedirs(args.export_fixes)
611 if not os.path.isdir(args.export_fixes):
614 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
619 if os.path.isdir(args.export_fixes):
620 export_fixes_dir = args.export_fixes
622 if export_fixes_dir
is None and (args.fix
or combine_fixes):
623 export_fixes_dir = tempfile.mkdtemp()
624 delete_fixes_dir =
True
626 profile_dir: Optional[str] =
None
627 if args.enable_check_profile:
628 profile_dir = tempfile.mkdtemp()
638 args.allow_enabling_alpha_checkers,
640 args.extra_arg_before,
647 args.warnings_as_errors,
648 args.exclude_header_filter,
649 args.allow_no_checks,
652 invocation.append(
"-list-checks")
653 invocation.append(
"-")
655 subprocess.check_call(
656 invocation, stdout=subprocess.DEVNULL
if args.quiet
else None
659 print(
"Unable to run clang-tidy.", file=sys.stderr)
663 with open(os.path.join(build_path, db_path))
as f:
664 database = json.load(f)
665 files = {os.path.abspath(os.path.join(e[
"directory"], e[
"file"]))
for e
in database}
666 number_files_in_database = len(files)
669 if args.source_filter:
671 source_filter_re = re.compile(args.source_filter)
674 "Error: unable to compile regex from arg -source-filter:",
677 traceback.print_exc()
679 files = {f
for f
in files
if source_filter_re.match(f)}
683 max_task = multiprocessing.cpu_count()
686 file_name_re = re.compile(
"|".join(args.files))
687 files = {f
for f
in files
if file_name_re.search(f)}
689 if not args.hide_progress:
691 f
"Running clang-tidy in {max_task} threads for {len(files)} files "
692 f
"out of {number_files_in_database} in compilation database ..."
696 semaphore = asyncio.Semaphore(max_task)
714 for i, coro
in enumerate(asyncio.as_completed(tasks)):
716 if result.returncode != 0:
718 if result.returncode < 0:
719 result.stderr += f
"{result.filename}: terminated by signal {-result.returncode}\n"
720 progress = f
"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]"
721 runtime = f
"[{result.elapsed:.1f}s]"
722 if not args.hide_progress:
723 print(f
"{progress}{runtime} {' '.join(result.invocation)}")
725 print(result.stdout, end=(
"" if result.stderr
else "\n"))
728 except asyncio.CancelledError:
729 if not args.hide_progress:
730 print(
"\nCtrl-C detected, goodbye.")
734 assert export_fixes_dir
735 shutil.rmtree(export_fixes_dir)
737 shutil.rmtree(profile_dir)
740 if args.enable_check_profile
and profile_dir:
747 print(
"No profiling data found.")
750 if not args.hide_progress:
751 print(f
"Writing fixes to {args.export_fixes} ...")
753 assert export_fixes_dir
756 print(
"Error exporting fixes.\n", file=sys.stderr)
757 traceback.print_exc()
761 if not args.hide_progress:
762 print(
"Applying fixes ...")
764 assert export_fixes_dir
765 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
767 print(
"Error applying fixes.\n", file=sys.stderr)
768 traceback.print_exc()
772 assert export_fixes_dir
773 shutil.rmtree(export_fixes_dir)
775 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, 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)