3 # ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===#
5 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6 # See https://llvm.org/LICENSE.txt for license information.
7 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
9 # ===-----------------------------------------------------------------------===#
10 # FIXME: Integrate with clang-tidy-diff.py
14 Parallel clang-tidy runner
15 ==========================
17 Runs clang-tidy over all files in a compilation database. Requires clang-tidy
18 and 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
33 Compilation database setup:
34 http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
39 from dataclasses
import dataclass
42 import multiprocessing
51 from types
import ModuleType
52 from typing
import Any
, Awaitable
, Callable
, List
, Optional
, TypeVar
55 yaml
: Optional
[ModuleType
] = None
62 def strtobool(val
: str) -> bool:
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"]:
71 # Return ArgumentTypeError so that argparse does not substitute its own error message
72 raise argparse
.ArgumentTypeError(
73 f
"'{val}' is invalid value for boolean argument! Try 0 or 1."
77 def find_compilation_database(path
: str) -> str:
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.")
89 def get_tidy_invocation(
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")
128 # Get a temporary file. We immediately close the handle so clang-tidy can
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")
154 def merge_replacement_files(tmpdir
: str, mergefile
: str) -> None:
155 """Merge all replacement files in a directory into a single file"""
157 # The fixes suggested by clang-tidy >= 4.0.0 are given under
158 # the top level key 'Diagnostics' in the output yaml files
159 mergekey
= "Diagnostics"
161 for replacefile
in glob
.iglob(os
.path
.join(tmpdir
, "*.yaml")):
162 content
= yaml
.safe_load(open(replacefile
, "r"))
164 continue # Skip empty files.
165 merged
.extend(content
.get(mergekey
, []))
168 # MainSourceFile: The key is required by the definition inside
169 # include/clang/Tooling/ReplacementsYaml.h, but the value
170 # is actually never used inside clang-apply-replacements,
171 # so we set it to '' here.
172 output
= {"MainSourceFile": "", mergekey
: merged
}
173 with
open(mergefile
, "w") as out
:
174 yaml
.safe_dump(output
, out
)
177 open(mergefile
, "w").close()
180 def find_binary(arg
: str, name
: str, build_path
: str) -> str:
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
)
212 # FIXME Python 3.12: This can be simplified out with run_with_semaphore[T](...).
216 async def run_with_semaphore(
217 semaphore
: asyncio
.Semaphore
,
218 f
: Callable
[..., Awaitable
[T
]],
222 async with semaphore
:
223 return await f(*args
, **kwargs
)
227 class ClangTidyResult
:
229 invocation
: List
[str]
237 args
: argparse
.Namespace
,
239 clang_tidy_binary
: str,
242 ) -> ClangTidyResult
:
244 Runs clang-tidy on a single file and returns the result.
246 invocation
= get_tidy_invocation(
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
280 return ClangTidyResult(
284 stdout
.decode("UTF-8"),
285 stderr
.decode("UTF-8"),
290 async def main() -> None:
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
457 build_path
= find_compilation_database(db_path
)
459 clang_tidy_binary
= find_binary(args
.clang_tidy_binary
, "clang-tidy", build_path
)
462 clang_apply_replacements_binary
= find_binary(
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:
470 # if a directory is given, create it if it does not exist
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
492 invocation
= get_tidy_invocation(
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("-")
514 # Even with -quiet we still want to check if we can call clang-tidy.
515 subprocess
.check_call(
516 invocation
, stdout
=subprocess
.DEVNULL
if args
.quiet
else None
519 print("Unable to run clang-tidy.", file=sys
.stderr
)
522 # Load the database and extract all files.
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
)
528 # Filter source files from compilation database.
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()
545 # Build up a big regexy filter from all command line arguments.
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
601 merge_replacement_files(export_fixes_dir
, args
.export_fixes
)
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
)
623 if __name__
== "__main__":
626 except KeyboardInterrupt: