53from typing
import Any, Awaitable, Callable, Dict, List, Optional, Tuple, TypeVar
92 clang_tidy_binary: str,
94 tmpdir: Optional[str],
96 header_filter: Optional[str],
97 allow_enabling_alpha_checkers: bool,
99 extra_arg_before: List[str],
100 removed_arg: List[str],
102 config_file_path: str,
104 line_filter: Optional[str],
107 warnings_as_errors: Optional[str],
108 exclude_header_filter: Optional[str],
109 allow_no_checks: bool,
110 store_check_profile: Optional[str],
112 """Gets a command line for clang-tidy."""
113 start = [clang_tidy_binary]
114 if allow_enabling_alpha_checkers:
115 start.append(
"-allow-enabling-analyzer-alpha-checkers")
116 if exclude_header_filter
is not None:
117 start.append(f
"--exclude-header-filter={exclude_header_filter}")
118 if header_filter
is not None:
119 start.append(f
"-header-filter={header_filter}")
120 if line_filter
is not None:
121 start.append(f
"-line-filter={line_filter}")
122 if use_color
is not None:
124 start.append(
"--use-color")
126 start.append(
"--use-color=false")
128 start.append(f
"-checks={checks}")
129 if tmpdir
is not None:
130 start.append(
"-export-fixes")
133 (handle, name) = tempfile.mkstemp(suffix=
".yaml", dir=tmpdir)
136 for arg
in extra_arg:
137 start.append(f
"-extra-arg={arg}")
138 for arg
in extra_arg_before:
139 start.append(f
"-extra-arg-before={arg}")
140 for arg
in removed_arg:
141 start.append(f
"-removed-arg={arg}")
142 start.append(f
"-p={build_path}")
144 start.append(
"-quiet")
146 start.append(f
"--config-file={config_file_path}")
148 start.append(f
"-config={config}")
149 for plugin
in plugins:
150 start.append(f
"-load={plugin}")
151 if warnings_as_errors:
152 start.append(f
"--warnings-as-errors={warnings_as_errors}")
154 start.append(
"--allow-no-checks")
155 if store_check_profile:
156 start.append(
"--enable-check-profile")
157 start.append(f
"--store-check-profile={store_check_profile}")
164 """Merge all replacement files in a directory into a single file"""
168 mergekey =
"Diagnostics"
170 for replacefile
in glob.iglob(os.path.join(tmpdir,
"*.yaml")):
171 content = yaml.safe_load(open(replacefile,
"r"))
174 merged.extend(content.get(mergekey, []))
181 output = {
"MainSourceFile":
"", mergekey: merged}
182 with open(mergefile,
"w")
as out:
183 yaml.safe_dump(output, out)
186 open(mergefile,
"w").close()
190 """Aggregate timing data from multiple profile JSON files"""
191 aggregated: Dict[str, float] = {}
193 for profile_file
in glob.iglob(os.path.join(profile_dir,
"*.json")):
195 with open(profile_file,
"r", encoding=
"utf-8")
as f:
197 profile_data: Dict[str, float] = data.get(
"profile", {})
199 for key, value
in profile_data.items():
200 if key.startswith(
"time.clang-tidy."):
201 if key
in aggregated:
202 aggregated[key] += value
204 aggregated[key] = value
205 except (json.JSONDecodeError, KeyError, IOError)
as e:
206 print(f
"Error: invalid json file {profile_file}: {e}", file=sys.stderr)
213 """Print aggregated checks profile data in the same format as clang-tidy"""
214 if not aggregated_data:
218 checkers: Dict[str, Dict[str, float]] = {}
219 for key, value
in aggregated_data.items():
220 parts = key.split(
".")
221 if len(parts) >= 4
and parts[0] ==
"time" and parts[1] ==
"clang-tidy":
222 checker_name =
".".join(
225 timing_type = parts[-1]
227 if checker_name
not in checkers:
228 checkers[checker_name] = {
"wall": 0.0,
"user": 0.0,
"sys": 0.0}
230 checkers[checker_name][timing_type] = value
235 total_user = sum(data[
"user"]
for data
in checkers.values())
236 total_sys = sum(data[
"sys"]
for data
in checkers.values())
237 total_wall = sum(data[
"wall"]
for data
in checkers.values())
239 sorted_checkers: List[Tuple[str, Dict[str, float]]] = sorted(
240 checkers.items(), key=
lambda x: x[1][
"user"] + x[1][
"sys"], reverse=
True
243 def print_stderr(*args: Any, **kwargs: Any) ->
None:
244 print(*args, file=sys.stderr, **kwargs)
247 "===-------------------------------------------------------------------------==="
249 print_stderr(
" clang-tidy checks profiling")
251 "===-------------------------------------------------------------------------==="
254 f
" Total Execution Time: {total_user + total_sys:.4f} seconds ({total_wall:.4f} wall clock)\n"
258 total_combined = total_user + total_sys
259 user_width = len(f
"{total_user:.4f}")
260 sys_width = len(f
"{total_sys:.4f}")
261 combined_width = len(f
"{total_combined:.4f}")
262 wall_width = len(f
"{total_wall:.4f}")
266 user_header =
"---User Time---".center(user_width + additional_width)
267 sys_header =
"--System Time--".center(sys_width + additional_width)
268 combined_header =
"--User+System--".center(combined_width + additional_width)
269 wall_header =
"---Wall Time---".center(wall_width + additional_width)
272 f
" {user_header} {sys_header} {combined_header} {wall_header} --- Name ---"
275 for checker_name, data
in sorted_checkers:
276 user_time = data[
"user"]
277 sys_time = data[
"sys"]
278 wall_time = data[
"wall"]
279 combined_time = user_time + sys_time
281 user_percent = (user_time / total_user * 100)
if total_user > 0
else 0
282 sys_percent = (sys_time / total_sys * 100)
if total_sys > 0
else 0
284 (combined_time / total_combined * 100)
if total_combined > 0
else 0
286 wall_percent = (wall_time / total_wall * 100)
if total_wall > 0
else 0
288 user_str = f
"{user_time:{user_width}.4f} ({user_percent:5.1f}%)"
289 sys_str = f
"{sys_time:{sys_width}.4f} ({sys_percent:5.1f}%)"
290 combined_str = f
"{combined_time:{combined_width}.4f} ({combined_percent:5.1f}%)"
291 wall_str = f
"{wall_time:{wall_width}.4f} ({wall_percent:5.1f}%)"
294 f
" {user_str} {sys_str} {combined_str} {wall_str} {checker_name}"
297 user_total_str = f
"{total_user:{user_width}.4f} (100.0%)"
298 sys_total_str = f
"{total_sys:{sys_width}.4f} (100.0%)"
299 combined_total_str = f
"{total_combined:{combined_width}.4f} (100.0%)"
300 wall_total_str = f
"{total_wall:{wall_width}.4f} (100.0%)"
303 f
" {user_total_str} {sys_total_str} {combined_total_str} {wall_total_str} Total"
364 args: argparse.Namespace,
366 clang_tidy_binary: str,
369 store_check_profile: Optional[str],
372 Runs clang-tidy on a single file and returns the result.
381 args.allow_enabling_alpha_checkers,
383 args.extra_arg_before,
391 args.warnings_as_errors,
392 args.exclude_header_filter,
393 args.allow_no_checks,
398 process = await asyncio.create_subprocess_exec(
399 *invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
402 stdout, stderr = await process.communicate()
404 except asyncio.CancelledError:
409 assert process.returncode
is not None
414 stdout.decode(
"UTF-8"),
415 stderr.decode(
"UTF-8"),
421 parser = argparse.ArgumentParser(
422 description=
"Runs clang-tidy over all files "
423 "in a compilation database. Requires "
424 "clang-tidy and clang-apply-replacements in "
425 "$PATH or in your build directory."
428 "-allow-enabling-alpha-checkers",
430 help=
"Allow alpha checkers from clang-analyzer.",
433 "-clang-tidy-binary", metavar=
"PATH", help=
"Path to clang-tidy binary."
436 "-clang-apply-replacements-binary",
438 help=
"Path to clang-apply-replacements binary.",
443 help=
"Checks filter, when not specified, use clang-tidy default.",
445 config_group = parser.add_mutually_exclusive_group()
446 config_group.add_argument(
449 help=
"Specifies a configuration in YAML/JSON format: "
450 " -config=\"{Checks: '*', "
451 ' CheckOptions: {x: y}}" '
452 "When the value is empty, clang-tidy will "
453 "attempt to find a file named .clang-tidy for "
454 "each source file in its parent directories.",
456 config_group.add_argument(
459 help=
"Specify the path of .clang-tidy or custom config "
460 "file: e.g. -config-file=/some/path/myTidyConfigFile. "
461 "This option internally works exactly the same way as "
462 "-config option after reading specified config file. "
463 "Use either -config-file or -config, not both.",
466 "-exclude-header-filter",
468 help=
"Regular expression matching the names of the "
469 "headers to exclude diagnostics from. Diagnostics from "
470 "the main file of each translation unit are always "
476 help=
"Regular expression matching the names of the "
477 "headers to output diagnostics from. Diagnostics from "
478 "the main file of each translation unit are always "
484 help=
"Regular expression matching the names of the "
485 "source files from compilation database to output "
491 help=
"List of files and line ranges to output diagnostics from.",
496 metavar=
"file_or_directory",
498 help=
"A directory or a yaml file to store suggested fixes in, "
499 "which can be applied with clang-apply-replacements. If the "
500 "parameter is a directory, the fixes of each compilation unit are "
501 "stored in individual yaml files in the directory.",
508 help=
"A directory to store suggested fixes in, which can be applied "
509 "with clang-apply-replacements. The fixes of each compilation unit are "
510 "stored in individual yaml files in the directory.",
516 help=
"Number of tidy instances to be run in parallel.",
522 help=
"Files to be processed (regex on path).",
524 parser.add_argument(
"-fix", action=
"store_true", help=
"apply fix-its.")
526 "-format", action=
"store_true", help=
"Reformat code after applying fixes."
531 help=
"The style of reformat code after applying fixes.",
538 help=
"Use colors in diagnostics, overriding clang-tidy's"
539 " default behavior. This option overrides the 'UseColor"
540 "' option in .clang-tidy file, if any.",
543 "-p", dest=
"build_path", help=
"Path used to read a compile command database."
550 help=
"Additional argument to append to the compiler command line.",
554 dest=
"extra_arg_before",
557 help=
"Additional argument to prepend to the compiler command line.",
564 help=
"Arguments to remove from the compiler command line.",
567 "-quiet", action=
"store_true", help=
"Run clang-tidy in quiet mode."
574 help=
"Load the specified plugin in clang-tidy.",
577 "-warnings-as-errors",
579 help=
"Upgrades warnings to errors. Same format as '-checks'.",
584 help=
"Allow empty enabled checks.",
587 "-enable-check-profile",
589 help=
"Enable per-check timing profiles, and print a report",
594 help=
"Hide progress",
596 args = parser.parse_args()
598 db_path =
"compile_commands.json"
600 if args.build_path
is not None:
601 build_path = args.build_path
606 clang_tidy_binary =
find_binary(args.clang_tidy_binary,
"clang-tidy", build_path)
610 args.clang_apply_replacements_binary,
"clang-apply-replacements", build_path
613 combine_fixes =
False
614 export_fixes_dir: Optional[str] =
None
615 delete_fixes_dir =
False
616 if args.export_fixes
is not None:
618 if args.export_fixes.endswith(os.path.sep)
and not os.path.isdir(
621 os.makedirs(args.export_fixes)
623 if not os.path.isdir(args.export_fixes):
626 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
631 if os.path.isdir(args.export_fixes):
632 export_fixes_dir = args.export_fixes
634 if export_fixes_dir
is None and (args.fix
or combine_fixes):
635 export_fixes_dir = tempfile.mkdtemp()
636 delete_fixes_dir =
True
638 profile_dir: Optional[str] =
None
639 if args.enable_check_profile:
640 profile_dir = tempfile.mkdtemp()
650 args.allow_enabling_alpha_checkers,
652 args.extra_arg_before,
660 args.warnings_as_errors,
661 args.exclude_header_filter,
662 args.allow_no_checks,
665 invocation.append(
"-list-checks")
666 invocation.append(
"-")
668 subprocess.check_call(
669 invocation, stdout=subprocess.DEVNULL
if args.quiet
else None
672 print(
"Unable to run clang-tidy.", file=sys.stderr)
676 with open(os.path.join(build_path, db_path))
as f:
677 database = json.load(f)
678 files = {os.path.abspath(os.path.join(e[
"directory"], e[
"file"]))
for e
in database}
679 number_files_in_database = len(files)
682 if args.source_filter:
684 source_filter_re = re.compile(args.source_filter)
687 "Error: unable to compile regex from arg -source-filter:",
690 traceback.print_exc()
692 files = {f
for f
in files
if source_filter_re.match(f)}
696 max_task = multiprocessing.cpu_count()
699 file_name_re = re.compile(
"|".join(args.files))
700 files = {f
for f
in files
if file_name_re.search(f)}
702 if not args.hide_progress:
704 f
"Running clang-tidy in {max_task} threads for {len(files)} files "
705 f
"out of {number_files_in_database} in compilation database ..."
709 semaphore = asyncio.Semaphore(max_task)
727 for i, coro
in enumerate(asyncio.as_completed(tasks)):
729 if result.returncode != 0:
731 if result.returncode < 0:
732 result.stderr += f
"{result.filename}: terminated by signal {-result.returncode}\n"
733 progress = f
"[{i + 1: >{len(f'{len(files)}')}}/{len(files)}]"
734 runtime = f
"[{result.elapsed:.1f}s]"
735 if not args.hide_progress:
736 print(f
"{progress}{runtime} {shlex.join(result.invocation)}")
738 print(result.stdout, end=(
"" if result.stderr
else "\n"))
741 except asyncio.CancelledError:
742 if not args.hide_progress:
743 print(
"\nCtrl-C detected, goodbye.")
747 assert export_fixes_dir
748 shutil.rmtree(export_fixes_dir)
750 shutil.rmtree(profile_dir)
753 if args.enable_check_profile
and profile_dir:
760 print(
"No profiling data found.")
763 if not args.hide_progress:
764 print(f
"Writing fixes to {args.export_fixes} ...")
766 assert export_fixes_dir
769 print(
"Error exporting fixes.\n", file=sys.stderr)
770 traceback.print_exc()
774 if not args.hide_progress:
775 print(
"Applying fixes ...")
777 assert export_fixes_dir
778 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
780 print(
"Error applying fixes.\n", file=sys.stderr)
781 traceback.print_exc()
785 assert export_fixes_dir
786 shutil.rmtree(export_fixes_dir)
788 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)