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
37 from __future__
import print_function
42 import multiprocessing
60 """Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
63 if val
in ["", "true", "1"]:
65 elif val
in ["false", "0"]:
68 # Return ArgumentTypeError so that argparse does not substitute its own error message
69 raise argparse
.ArgumentTypeError(
70 "'{}' is invalid value for boolean argument! Try 0 or 1.".format(val
)
74 def find_compilation_database(path
):
75 """Adjusts the directory until a compilation database is found."""
76 result
= os
.path
.realpath("./")
77 while not os
.path
.isfile(os
.path
.join(result
, path
)):
78 parent
= os
.path
.dirname(result
)
80 print("Error: could not find compilation database.")
86 def make_absolute(f
, directory
):
89 return os
.path
.normpath(os
.path
.join(directory
, f
))
92 def get_tidy_invocation(
99 allow_enabling_alpha_checkers
,
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 header_filter
is not None:
115 start
.append("-header-filter=" + header_filter
)
116 if line_filter
is not None:
117 start
.append("-line-filter=" + line_filter
)
118 if use_color
is not None:
120 start
.append("--use-color")
122 start
.append("--use-color=false")
124 start
.append("-checks=" + checks
)
125 if tmpdir
is not None:
126 start
.append("-export-fixes")
127 # Get a temporary file. We immediately close the handle so clang-tidy can
129 (handle
, name
) = tempfile
.mkstemp(suffix
=".yaml", dir=tmpdir
)
132 for arg
in extra_arg
:
133 start
.append("-extra-arg=%s" % arg
)
134 for arg
in extra_arg_before
:
135 start
.append("-extra-arg-before=%s" % arg
)
136 start
.append("-p=" + build_path
)
138 start
.append("-quiet")
140 start
.append("--config-file=" + config_file_path
)
142 start
.append("-config=" + config
)
143 for plugin
in plugins
:
144 start
.append("-load=" + plugin
)
145 if warnings_as_errors
:
146 start
.append("--warnings-as-errors=" + warnings_as_errors
)
151 def merge_replacement_files(tmpdir
, mergefile
):
152 """Merge all replacement files in a directory into a single file"""
153 # The fixes suggested by clang-tidy >= 4.0.0 are given under
154 # the top level key 'Diagnostics' in the output yaml files
155 mergekey
= "Diagnostics"
157 for replacefile
in glob
.iglob(os
.path
.join(tmpdir
, "*.yaml")):
158 content
= yaml
.safe_load(open(replacefile
, "r"))
160 continue # Skip empty files.
161 merged
.extend(content
.get(mergekey
, []))
164 # MainSourceFile: The key is required by the definition inside
165 # include/clang/Tooling/ReplacementsYaml.h, but the value
166 # is actually never used inside clang-apply-replacements,
167 # so we set it to '' here.
168 output
= {"MainSourceFile": "", mergekey
: merged
}
169 with
open(mergefile
, "w") as out
:
170 yaml
.safe_dump(output
, out
)
173 open(mergefile
, "w").close()
176 def find_binary(arg
, name
, build_path
):
177 """Get the path for a binary or exit"""
179 if shutil
.which(arg
):
183 "error: passed binary '{}' was not found or is not executable".format(
188 built_path
= os
.path
.join(build_path
, "bin", name
)
189 binary
= shutil
.which(name
) or shutil
.which(built_path
)
194 "error: failed to find {} in $PATH or at {}".format(name
, built_path
)
198 def apply_fixes(args
, clang_apply_replacements_binary
, tmpdir
):
199 """Calls clang-apply-fixes on a given directory."""
200 invocation
= [clang_apply_replacements_binary
]
201 invocation
.append("-ignore-insert-conflict")
203 invocation
.append("-format")
205 invocation
.append("-style=" + args
.style
)
206 invocation
.append(tmpdir
)
207 subprocess
.call(invocation
)
210 def run_tidy(args
, clang_tidy_binary
, tmpdir
, build_path
, queue
, lock
, failed_files
):
211 """Takes filenames out of queue and runs clang-tidy on them."""
214 invocation
= get_tidy_invocation(
221 args
.allow_enabling_alpha_checkers
,
223 args
.extra_arg_before
,
230 args
.warnings_as_errors
,
233 proc
= subprocess
.Popen(
234 invocation
, stdout
=subprocess
.PIPE
, stderr
=subprocess
.PIPE
236 output
, err
= proc
.communicate()
237 if proc
.returncode
!= 0:
238 if proc
.returncode
< 0:
239 msg
= "%s: terminated by signal %d\n" % (name
, -proc
.returncode
)
240 err
+= msg
.encode("utf-8")
241 failed_files
.append(name
)
243 sys
.stdout
.write(" ".join(invocation
) + "\n" + output
.decode("utf-8"))
246 sys
.stderr
.write(err
.decode("utf-8"))
251 parser
= argparse
.ArgumentParser(
252 description
="Runs clang-tidy over all files "
253 "in a compilation database. Requires "
254 "clang-tidy and clang-apply-replacements in "
255 "$PATH or in your build directory."
258 "-allow-enabling-alpha-checkers",
260 help="allow alpha checkers from clang-analyzer.",
263 "-clang-tidy-binary", metavar
="PATH", help="path to clang-tidy binary"
266 "-clang-apply-replacements-binary",
268 help="path to clang-apply-replacements binary",
273 help="checks filter, when not specified, use clang-tidy default",
275 config_group
= parser
.add_mutually_exclusive_group()
276 config_group
.add_argument(
279 help="Specifies a configuration in YAML/JSON format: "
280 " -config=\"{Checks: '*', "
281 ' CheckOptions: {x: y}}" '
282 "When the value is empty, clang-tidy will "
283 "attempt to find a file named .clang-tidy for "
284 "each source file in its parent directories.",
286 config_group
.add_argument(
289 help="Specify the path of .clang-tidy or custom config "
290 "file: e.g. -config-file=/some/path/myTidyConfigFile. "
291 "This option internally works exactly the same way as "
292 "-config option after reading specified config file. "
293 "Use either -config-file or -config, not both.",
298 help="regular expression matching the names of the "
299 "headers to output diagnostics from. Diagnostics from "
300 "the main file of each translation unit are always "
306 help="List of files with line ranges to filter the warnings.",
311 metavar
="file_or_directory",
313 help="A directory or a yaml file to store suggested fixes in, "
314 "which can be applied with clang-apply-replacements. If the "
315 "parameter is a directory, the fixes of each compilation unit are "
316 "stored in individual yaml files in the directory.",
323 help="A directory to store suggested fixes in, which can be applied "
324 "with clang-apply-replacements. The fixes of each compilation unit are "
325 "stored in individual yaml files in the directory.",
331 help="number of tidy instances to be run in parallel.",
334 "files", nargs
="*", default
=[".*"], help="files to be processed (regex on path)"
336 parser
.add_argument("-fix", action
="store_true", help="apply fix-its")
338 "-format", action
="store_true", help="Reformat code after applying fixes"
343 help="The style of reformat code after applying fixes",
350 help="Use colors in diagnostics, overriding clang-tidy's"
351 " default behavior. This option overrides the 'UseColor"
352 "' option in .clang-tidy file, if any.",
355 "-p", dest
="build_path", help="Path used to read a compile command database."
362 help="Additional argument to append to the compiler command line.",
366 dest
="extra_arg_before",
369 help="Additional argument to prepend to the compiler command line.",
372 "-quiet", action
="store_true", help="Run clang-tidy in quiet mode"
379 help="Load the specified plugin in clang-tidy.",
382 "-warnings-as-errors",
384 help="Upgrades warnings to errors. Same format as '-checks'",
386 args
= parser
.parse_args()
388 db_path
= "compile_commands.json"
390 if args
.build_path
is not None:
391 build_path
= args
.build_path
394 build_path
= find_compilation_database(db_path
)
396 clang_tidy_binary
= find_binary(args
.clang_tidy_binary
, "clang-tidy", build_path
)
399 clang_apply_replacements_binary
= find_binary(
400 args
.clang_apply_replacements_binary
, "clang-apply-replacements", build_path
403 combine_fixes
= False
404 export_fixes_dir
= None
405 delete_fixes_dir
= False
406 if args
.export_fixes
is not None:
407 # if a directory is given, create it if it does not exist
408 if args
.export_fixes
.endswith(os
.path
.sep
) and not os
.path
.isdir(
411 os
.makedirs(args
.export_fixes
)
413 if not os
.path
.isdir(args
.export_fixes
):
416 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
421 if os
.path
.isdir(args
.export_fixes
):
422 export_fixes_dir
= args
.export_fixes
424 if export_fixes_dir
is None and (args
.fix
or combine_fixes
):
425 export_fixes_dir
= tempfile
.mkdtemp()
426 delete_fixes_dir
= True
429 invocation
= get_tidy_invocation(
436 args
.allow_enabling_alpha_checkers
,
438 args
.extra_arg_before
,
445 args
.warnings_as_errors
,
447 invocation
.append("-list-checks")
448 invocation
.append("-")
450 # Even with -quiet we still want to check if we can call clang-tidy.
451 with
open(os
.devnull
, "w") as dev_null
:
452 subprocess
.check_call(invocation
, stdout
=dev_null
)
454 subprocess
.check_call(invocation
)
456 print("Unable to run clang-tidy.", file=sys
.stderr
)
459 # Load the database and extract all files.
460 database
= json
.load(open(os
.path
.join(build_path
, db_path
)))
462 [make_absolute(entry
["file"], entry
["directory"]) for entry
in database
]
467 max_task
= multiprocessing
.cpu_count()
469 # Build up a big regexy filter from all command line arguments.
470 file_name_re
= re
.compile("|".join(args
.files
))
474 # Spin up a bunch of tidy-launching threads.
475 task_queue
= queue
.Queue(max_task
)
476 # List of files with a non-zero return code.
478 lock
= threading
.Lock()
479 for _
in range(max_task
):
480 t
= threading
.Thread(
495 # Fill the queue with files.
497 if file_name_re
.search(name
):
500 # Wait for all threads to be done.
502 if len(failed_files
):
505 except KeyboardInterrupt:
506 # This is a sad hack. Unfortunately subprocess goes
507 # bonkers with ctrl-c and we start forking merrily.
508 print("\nCtrl-C detected, goodbye.")
510 shutil
.rmtree(export_fixes_dir
)
514 print("Writing fixes to " + args
.export_fixes
+ " ...")
516 merge_replacement_files(export_fixes_dir
, args
.export_fixes
)
518 print("Error exporting fixes.\n", file=sys
.stderr
)
519 traceback
.print_exc()
523 print("Applying fixes ...")
525 apply_fixes(args
, clang_apply_replacements_binary
, export_fixes_dir
)
527 print("Error applying fixes.\n", file=sys
.stderr
)
528 traceback
.print_exc()
532 shutil
.rmtree(export_fixes_dir
)
533 sys
.exit(return_code
)
536 if __name__
== "__main__":