[clang][modules] Don't prevent translation of FW_Private includes when explicitly...
[llvm-project.git] / clang-tools-extra / clang-tidy / tool / run-clang-tidy.py
blob70f8cbcdcb2f11f26191d96e70632f11d1067c21
1 #!/usr/bin/env python3
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
13 """
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.
20 Example invocations.
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
35 """
37 from __future__ import print_function
39 import argparse
40 import glob
41 import json
42 import multiprocessing
43 import os
44 import queue
45 import re
46 import shutil
47 import subprocess
48 import sys
49 import tempfile
50 import threading
51 import traceback
53 try:
54 import yaml
55 except ImportError:
56 yaml = None
59 def strtobool(val):
60 """Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
62 val = val.lower()
63 if val in ["", "true", "1"]:
64 return True
65 elif val in ["false", "0"]:
66 return False
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)
79 if result == parent:
80 print("Error: could not find compilation database.")
81 sys.exit(1)
82 result = parent
83 return result
86 def make_absolute(f, directory):
87 if os.path.isabs(f):
88 return f
89 return os.path.normpath(os.path.join(directory, f))
92 def get_tidy_invocation(
94 clang_tidy_binary,
95 checks,
96 tmpdir,
97 build_path,
98 header_filter,
99 allow_enabling_alpha_checkers,
100 extra_arg,
101 extra_arg_before,
102 quiet,
103 config_file_path,
104 config,
105 line_filter,
106 use_color,
107 plugins,
108 warnings_as_errors,
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:
119 if use_color:
120 start.append("--use-color")
121 else:
122 start.append("--use-color=false")
123 if checks:
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
128 # overwrite it.
129 (handle, name) = tempfile.mkstemp(suffix=".yaml", dir=tmpdir)
130 os.close(handle)
131 start.append(name)
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)
137 if quiet:
138 start.append("-quiet")
139 if config_file_path:
140 start.append("--config-file=" + config_file_path)
141 elif config:
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)
147 start.append(f)
148 return start
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"
156 merged = []
157 for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
158 content = yaml.safe_load(open(replacefile, "r"))
159 if not content:
160 continue # Skip empty files.
161 merged.extend(content.get(mergekey, []))
163 if merged:
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)
171 else:
172 # Empty the file:
173 open(mergefile, "w").close()
176 def find_binary(arg, name, build_path):
177 """Get the path for a binary or exit"""
178 if arg:
179 if shutil.which(arg):
180 return arg
181 else:
182 raise SystemExit(
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)
190 if binary:
191 return binary
192 else:
193 raise SystemExit(
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")
202 if args.format:
203 invocation.append("-format")
204 if args.style:
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."""
212 while True:
213 name = queue.get()
214 invocation = get_tidy_invocation(
215 name,
216 clang_tidy_binary,
217 args.checks,
218 tmpdir,
219 build_path,
220 args.header_filter,
221 args.allow_enabling_alpha_checkers,
222 args.extra_arg,
223 args.extra_arg_before,
224 args.quiet,
225 args.config_file,
226 args.config,
227 args.line_filter,
228 args.use_color,
229 args.plugins,
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)
242 with lock:
243 sys.stdout.write(" ".join(invocation) + "\n" + output.decode("utf-8"))
244 if len(err) > 0:
245 sys.stdout.flush()
246 sys.stderr.write(err.decode("utf-8"))
247 queue.task_done()
250 def main():
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."
257 parser.add_argument(
258 "-allow-enabling-alpha-checkers",
259 action="store_true",
260 help="allow alpha checkers from clang-analyzer.",
262 parser.add_argument(
263 "-clang-tidy-binary", metavar="PATH", help="path to clang-tidy binary"
265 parser.add_argument(
266 "-clang-apply-replacements-binary",
267 metavar="PATH",
268 help="path to clang-apply-replacements binary",
270 parser.add_argument(
271 "-checks",
272 default=None,
273 help="checks filter, when not specified, use clang-tidy default",
275 config_group = parser.add_mutually_exclusive_group()
276 config_group.add_argument(
277 "-config",
278 default=None,
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(
287 "-config-file",
288 default=None,
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.",
295 parser.add_argument(
296 "-header-filter",
297 default=None,
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 "
301 "displayed.",
303 parser.add_argument(
304 "-line-filter",
305 default=None,
306 help="List of files with line ranges to filter the warnings.",
308 if yaml:
309 parser.add_argument(
310 "-export-fixes",
311 metavar="file_or_directory",
312 dest="export_fixes",
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.",
318 else:
319 parser.add_argument(
320 "-export-fixes",
321 metavar="directory",
322 dest="export_fixes",
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.",
327 parser.add_argument(
328 "-j",
329 type=int,
330 default=0,
331 help="number of tidy instances to be run in parallel.",
333 parser.add_argument(
334 "files", nargs="*", default=[".*"], help="files to be processed (regex on path)"
336 parser.add_argument("-fix", action="store_true", help="apply fix-its")
337 parser.add_argument(
338 "-format", action="store_true", help="Reformat code after applying fixes"
340 parser.add_argument(
341 "-style",
342 default="file",
343 help="The style of reformat code after applying fixes",
345 parser.add_argument(
346 "-use-color",
347 type=strtobool,
348 nargs="?",
349 const=True,
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.",
354 parser.add_argument(
355 "-p", dest="build_path", help="Path used to read a compile command database."
357 parser.add_argument(
358 "-extra-arg",
359 dest="extra_arg",
360 action="append",
361 default=[],
362 help="Additional argument to append to the compiler command line.",
364 parser.add_argument(
365 "-extra-arg-before",
366 dest="extra_arg_before",
367 action="append",
368 default=[],
369 help="Additional argument to prepend to the compiler command line.",
371 parser.add_argument(
372 "-quiet", action="store_true", help="Run clang-tidy in quiet mode"
374 parser.add_argument(
375 "-load",
376 dest="plugins",
377 action="append",
378 default=[],
379 help="Load the specified plugin in clang-tidy.",
381 parser.add_argument(
382 "-warnings-as-errors",
383 default=None,
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
392 else:
393 # Find our database
394 build_path = find_compilation_database(db_path)
396 clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path)
398 if args.fix:
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(
409 args.export_fixes
411 os.makedirs(args.export_fixes)
413 if not os.path.isdir(args.export_fixes):
414 if not yaml:
415 raise RuntimeError(
416 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
419 combine_fixes = True
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
428 try:
429 invocation = get_tidy_invocation(
431 clang_tidy_binary,
432 args.checks,
433 None,
434 build_path,
435 args.header_filter,
436 args.allow_enabling_alpha_checkers,
437 args.extra_arg,
438 args.extra_arg_before,
439 args.quiet,
440 args.config_file,
441 args.config,
442 args.line_filter,
443 args.use_color,
444 args.plugins,
445 args.warnings_as_errors,
447 invocation.append("-list-checks")
448 invocation.append("-")
449 if args.quiet:
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)
453 else:
454 subprocess.check_call(invocation)
455 except:
456 print("Unable to run clang-tidy.", file=sys.stderr)
457 sys.exit(1)
459 # Load the database and extract all files.
460 database = json.load(open(os.path.join(build_path, db_path)))
461 files = set(
462 [make_absolute(entry["file"], entry["directory"]) for entry in database]
465 max_task = args.j
466 if max_task == 0:
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))
472 return_code = 0
473 try:
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.
477 failed_files = []
478 lock = threading.Lock()
479 for _ in range(max_task):
480 t = threading.Thread(
481 target=run_tidy,
482 args=(
483 args,
484 clang_tidy_binary,
485 export_fixes_dir,
486 build_path,
487 task_queue,
488 lock,
489 failed_files,
492 t.daemon = True
493 t.start()
495 # Fill the queue with files.
496 for name in files:
497 if file_name_re.search(name):
498 task_queue.put(name)
500 # Wait for all threads to be done.
501 task_queue.join()
502 if len(failed_files):
503 return_code = 1
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.")
509 if delete_fixes_dir:
510 shutil.rmtree(export_fixes_dir)
511 os.kill(0, 9)
513 if combine_fixes:
514 print("Writing fixes to " + args.export_fixes + " ...")
515 try:
516 merge_replacement_files(export_fixes_dir, args.export_fixes)
517 except:
518 print("Error exporting fixes.\n", file=sys.stderr)
519 traceback.print_exc()
520 return_code = 1
522 if args.fix:
523 print("Applying fixes ...")
524 try:
525 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
526 except:
527 print("Error applying fixes.\n", file=sys.stderr)
528 traceback.print_exc()
529 return_code = 1
531 if delete_fixes_dir:
532 shutil.rmtree(export_fixes_dir)
533 sys.exit(return_code)
536 if __name__ == "__main__":
537 main()