clang-tools 19.0.0git
run-clang-tidy.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# ===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===#
4#
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
8#
9# ===-----------------------------------------------------------------------===#
10# FIXME: Integrate with clang-tidy-diff.py
11
12
13"""
14Parallel clang-tidy runner
15==========================
16
17Runs clang-tidy over all files in a compilation database. Requires clang-tidy
18and clang-apply-replacements in $PATH.
19
20Example 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
24
25- Fix all header guards.
26 run-clang-tidy.py -fix -checks=-*,llvm-header-guard
27
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
32
33Compilation database setup:
34http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
35"""
36
37from __future__ import print_function
38
39import argparse
40import glob
41import json
42import multiprocessing
43import os
44import queue
45import re
46import shutil
47import subprocess
48import sys
49import tempfile
50import threading
51import traceback
52
53try:
54 import yaml
55except ImportError:
56 yaml = None
57
58
59def strtobool(val):
60 """Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
61
62 val = val.lower()
63 if val in ["", "true", "1"]:
64 return True
65 elif val in ["false", "0"]:
66 return False
67
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)
71 )
72
73
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
84
85
86def make_absolute(f, directory):
87 if os.path.isabs(f):
88 return f
89 return os.path.normpath(os.path.join(directory, f))
90
91
93 f,
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,
109):
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
149
150
151def 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, []))
162
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()
174
175
176def 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(
184 arg
185 )
186 )
187
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)
195 )
196
197
198def 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)
208
209
210def 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,
231 )
232
233 proc = subprocess.Popen(
234 invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE
235 )
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()
248
249
250def 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."
256 )
257 parser.add_argument(
258 "-allow-enabling-alpha-checkers",
259 action="store_true",
260 help="allow alpha checkers from clang-analyzer.",
261 )
262 parser.add_argument(
263 "-clang-tidy-binary", metavar="PATH", help="path to clang-tidy binary"
264 )
265 parser.add_argument(
266 "-clang-apply-replacements-binary",
267 metavar="PATH",
268 help="path to clang-apply-replacements binary",
269 )
270 parser.add_argument(
271 "-checks",
272 default=None,
273 help="checks filter, when not specified, use clang-tidy default",
274 )
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.",
285 )
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.",
294 )
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.",
302 )
303 parser.add_argument(
304 "-source-filter",
305 default=None,
306 help="Regular expression matching the names of the "
307 "source files from compilation database to output "
308 "diagnostics from.",
309 )
310 parser.add_argument(
311 "-line-filter",
312 default=None,
313 help="List of files with line ranges to filter the warnings.",
314 )
315 if yaml:
316 parser.add_argument(
317 "-export-fixes",
318 metavar="file_or_directory",
319 dest="export_fixes",
320 help="A directory or a yaml file to store suggested fixes in, "
321 "which can be applied with clang-apply-replacements. If the "
322 "parameter is a directory, the fixes of each compilation unit are "
323 "stored in individual yaml files in the directory.",
324 )
325 else:
326 parser.add_argument(
327 "-export-fixes",
328 metavar="directory",
329 dest="export_fixes",
330 help="A directory to store suggested fixes in, which can be applied "
331 "with clang-apply-replacements. The fixes of each compilation unit are "
332 "stored in individual yaml files in the directory.",
333 )
334 parser.add_argument(
335 "-j",
336 type=int,
337 default=0,
338 help="number of tidy instances to be run in parallel.",
339 )
340 parser.add_argument(
341 "files", nargs="*", default=[".*"], help="files to be processed (regex on path)"
342 )
343 parser.add_argument("-fix", action="store_true", help="apply fix-its")
344 parser.add_argument(
345 "-format", action="store_true", help="Reformat code after applying fixes"
346 )
347 parser.add_argument(
348 "-style",
349 default="file",
350 help="The style of reformat code after applying fixes",
351 )
352 parser.add_argument(
353 "-use-color",
354 type=strtobool,
355 nargs="?",
356 const=True,
357 help="Use colors in diagnostics, overriding clang-tidy's"
358 " default behavior. This option overrides the 'UseColor"
359 "' option in .clang-tidy file, if any.",
360 )
361 parser.add_argument(
362 "-p", dest="build_path", help="Path used to read a compile command database."
363 )
364 parser.add_argument(
365 "-extra-arg",
366 dest="extra_arg",
367 action="append",
368 default=[],
369 help="Additional argument to append to the compiler command line.",
370 )
371 parser.add_argument(
372 "-extra-arg-before",
373 dest="extra_arg_before",
374 action="append",
375 default=[],
376 help="Additional argument to prepend to the compiler command line.",
377 )
378 parser.add_argument(
379 "-quiet", action="store_true", help="Run clang-tidy in quiet mode"
380 )
381 parser.add_argument(
382 "-load",
383 dest="plugins",
384 action="append",
385 default=[],
386 help="Load the specified plugin in clang-tidy.",
387 )
388 parser.add_argument(
389 "-warnings-as-errors",
390 default=None,
391 help="Upgrades warnings to errors. Same format as '-checks'",
392 )
393 args = parser.parse_args()
394
395 db_path = "compile_commands.json"
396
397 if args.build_path is not None:
398 build_path = args.build_path
399 else:
400 # Find our database
401 build_path = find_compilation_database(db_path)
402
403 clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy", build_path)
404
405 if args.fix:
406 clang_apply_replacements_binary = find_binary(
407 args.clang_apply_replacements_binary, "clang-apply-replacements", build_path
408 )
409
410 combine_fixes = False
411 export_fixes_dir = None
412 delete_fixes_dir = False
413 if args.export_fixes is not None:
414 # if a directory is given, create it if it does not exist
415 if args.export_fixes.endswith(os.path.sep) and not os.path.isdir(
416 args.export_fixes
417 ):
418 os.makedirs(args.export_fixes)
419
420 if not os.path.isdir(args.export_fixes):
421 if not yaml:
422 raise RuntimeError(
423 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
424 )
425
426 combine_fixes = True
427
428 if os.path.isdir(args.export_fixes):
429 export_fixes_dir = args.export_fixes
430
431 if export_fixes_dir is None and (args.fix or combine_fixes):
432 export_fixes_dir = tempfile.mkdtemp()
433 delete_fixes_dir = True
434
435 try:
436 invocation = get_tidy_invocation(
437 "",
438 clang_tidy_binary,
439 args.checks,
440 None,
441 build_path,
442 args.header_filter,
443 args.allow_enabling_alpha_checkers,
444 args.extra_arg,
445 args.extra_arg_before,
446 args.quiet,
447 args.config_file,
448 args.config,
449 args.line_filter,
450 args.use_color,
451 args.plugins,
452 args.warnings_as_errors,
453 )
454 invocation.append("-list-checks")
455 invocation.append("-")
456 if args.quiet:
457 # Even with -quiet we still want to check if we can call clang-tidy.
458 with open(os.devnull, "w") as dev_null:
459 subprocess.check_call(invocation, stdout=dev_null)
460 else:
461 subprocess.check_call(invocation)
462 except:
463 print("Unable to run clang-tidy.", file=sys.stderr)
464 sys.exit(1)
465
466 # Load the database and extract all files.
467 database = json.load(open(os.path.join(build_path, db_path)))
468 files = set(
469 [make_absolute(entry["file"], entry["directory"]) for entry in database]
470 )
471
472 # Filter source files from compilation database.
473 if args.source_filter:
474 try:
475 source_filter_re = re.compile(args.source_filter)
476 except:
477 print(
478 "Error: unable to compile regex from arg -source-filter:",
479 file=sys.stderr,
480 )
481 traceback.print_exc()
482 sys.exit(1)
483 files = {f for f in files if source_filter_re.match(f)}
484
485 max_task = args.j
486 if max_task == 0:
487 max_task = multiprocessing.cpu_count()
488
489 # Build up a big regexy filter from all command line arguments.
490 file_name_re = re.compile("|".join(args.files))
491
492 return_code = 0
493 try:
494 # Spin up a bunch of tidy-launching threads.
495 task_queue = queue.Queue(max_task)
496 # List of files with a non-zero return code.
497 failed_files = []
498 lock = threading.Lock()
499 for _ in range(max_task):
500 t = threading.Thread(
501 target=run_tidy,
502 args=(
503 args,
504 clang_tidy_binary,
505 export_fixes_dir,
506 build_path,
507 task_queue,
508 lock,
509 failed_files,
510 ),
511 )
512 t.daemon = True
513 t.start()
514
515 # Fill the queue with files.
516 for name in files:
517 if file_name_re.search(name):
518 task_queue.put(name)
519
520 # Wait for all threads to be done.
521 task_queue.join()
522 if len(failed_files):
523 return_code = 1
524
525 except KeyboardInterrupt:
526 # This is a sad hack. Unfortunately subprocess goes
527 # bonkers with ctrl-c and we start forking merrily.
528 print("\nCtrl-C detected, goodbye.")
529 if delete_fixes_dir:
530 shutil.rmtree(export_fixes_dir)
531 os.kill(0, 9)
532
533 if combine_fixes:
534 print("Writing fixes to " + args.export_fixes + " ...")
535 try:
536 merge_replacement_files(export_fixes_dir, args.export_fixes)
537 except:
538 print("Error exporting fixes.\n", file=sys.stderr)
539 traceback.print_exc()
540 return_code = 1
541
542 if args.fix:
543 print("Applying fixes ...")
544 try:
545 apply_fixes(args, clang_apply_replacements_binary, export_fixes_dir)
546 except:
547 print("Error applying fixes.\n", file=sys.stderr)
548 traceback.print_exc()
549 return_code = 1
550
551 if delete_fixes_dir:
552 shutil.rmtree(export_fixes_dir)
553 sys.exit(return_code)
554
555
556if __name__ == "__main__":
557 main()
def make_absolute(f, directory)
def apply_fixes(args, clang_apply_replacements_binary, tmpdir)
def strtobool(val)
def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files)
def find_compilation_database(path)
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, header_filter, allow_enabling_alpha_checkers, extra_arg, extra_arg_before, quiet, config_file_path, config, line_filter, use_color, plugins, warnings_as_errors)
def merge_replacement_files(tmpdir, mergefile)
def find_binary(arg, name, build_path)