clang-tools 22.0.0git
clang-tidy-diff.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# ===-----------------------------------------------------------------------===#
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
11r"""
12ClangTidy Diff Checker
13======================
14
15This script reads input from a unified diff, runs clang-tidy on all changed
16files and outputs clang-tidy warnings in changed lines only. This is useful to
17detect clang-tidy regressions in the lines touched by a specific patch.
18Example usage for git/svn users:
19
20 git diff -U0 HEAD^ | clang-tidy-diff.py -p1
21 svn diff --diff-cmd=diff -x-U0 | \
22 clang-tidy-diff.py -fix -checks=-*,modernize-use-override
23
24"""
25
26import argparse
27import glob
28import json
29import multiprocessing
30import os
31import queue
32import re
33import shutil
34import subprocess
35import sys
36import tempfile
37import threading
38import traceback
39from pathlib import Path
40
41try:
42 import yaml
43except ImportError:
44 yaml = None
45
46
47def run_tidy(task_queue, lock, timeout, failed_files):
48 watchdog = None
49 while True:
50 command = task_queue.get()
51 try:
52 proc = subprocess.Popen(
53 command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
54 )
55
56 if timeout is not None:
57 watchdog = threading.Timer(timeout, proc.kill)
58 watchdog.start()
59
60 stdout, stderr = proc.communicate()
61 if proc.returncode != 0:
62 if proc.returncode < 0:
63 msg = "Terminated by signal %d : %s\n" % (
64 -proc.returncode,
65 " ".join(command),
66 )
67 stderr += msg.encode("utf-8")
68 failed_files.append(command)
69
70 with lock:
71 sys.stdout.write(stdout.decode("utf-8") + "\n")
72 sys.stdout.flush()
73 if stderr:
74 sys.stderr.write(stderr.decode("utf-8") + "\n")
75 sys.stderr.flush()
76 except Exception as e:
77 with lock:
78 sys.stderr.write("Failed: " + str(e) + ": ".join(command) + "\n")
79 finally:
80 with lock:
81 if not (timeout is None or watchdog is None):
82 if not watchdog.is_alive():
83 sys.stderr.write(
84 "Terminated by timeout: " + " ".join(command) + "\n"
85 )
86 watchdog.cancel()
87 task_queue.task_done()
88
89
90def start_workers(max_tasks, tidy_caller, arguments):
91 for _ in range(max_tasks):
92 t = threading.Thread(target=tidy_caller, args=arguments)
93 t.daemon = True
94 t.start()
95
96
97def merge_replacement_files(tmpdir, mergefile):
98 """Merge all replacement files in a directory into a single file"""
99 # The fixes suggested by clang-tidy >= 4.0.0 are given under
100 # the top level key 'Diagnostics' in the output yaml files
101 mergekey = "Diagnostics"
102 merged = []
103 for replacefile in glob.iglob(os.path.join(tmpdir, "*.yaml")):
104 content = yaml.safe_load(open(replacefile, "r"))
105 if not content:
106 continue # Skip empty files.
107 merged.extend(content.get(mergekey, []))
108
109 if merged:
110 # MainSourceFile: The key is required by the definition inside
111 # include/clang/Tooling/ReplacementsYaml.h, but the value
112 # is actually never used inside clang-apply-replacements,
113 # so we set it to '' here.
114 output = {"MainSourceFile": "", mergekey: merged}
115 with open(mergefile, "w") as out:
116 yaml.safe_dump(output, out)
117 else:
118 # Empty the file:
119 open(mergefile, "w").close()
120
121
123 """Read a compile_commands.json database and return a set of file paths"""
124 current_dir = Path.cwd()
125 compile_commands_json = (
126 (current_dir / args.build_path) if args.build_path else current_dir
127 )
128 compile_commands_json = compile_commands_json / "compile_commands.json"
129 files = set()
130 with open(compile_commands_json) as db_file:
131 db_json = json.load(db_file)
132 for entry in db_json:
133 if "file" not in entry:
134 continue
135 files.add(Path(entry["file"]))
136 return files
137
138
139def main():
140 parser = argparse.ArgumentParser(
141 description="Run clang-tidy against changed files, and "
142 "output diagnostics only for modified "
143 "lines."
144 )
145 parser.add_argument(
146 "-clang-tidy-binary",
147 metavar="PATH",
148 default="clang-tidy",
149 help="path to clang-tidy binary",
150 )
151 parser.add_argument(
152 "-p",
153 metavar="NUM",
154 default=0,
155 help="strip the smallest prefix containing P slashes",
156 )
157 parser.add_argument(
158 "-regex",
159 metavar="PATTERN",
160 default=None,
161 help="custom pattern selecting file paths to check "
162 "(case sensitive, overrides -iregex)",
163 )
164 parser.add_argument(
165 "-iregex",
166 metavar="PATTERN",
167 default=r".*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)",
168 help="custom pattern selecting file paths to check "
169 "(case insensitive, overridden by -regex)",
170 )
171 parser.add_argument(
172 "-j",
173 type=int,
174 default=0,
175 help="number of tidy instances to be run in parallel.",
176 )
177 parser.add_argument(
178 "-timeout", type=int, default=None, help="timeout per each file in seconds."
179 )
180 parser.add_argument(
181 "-fix", action="store_true", default=False, help="apply suggested fixes"
182 )
183 parser.add_argument(
184 "-checks",
185 help="checks filter, when not specified, use clang-tidy " "default",
186 default="",
187 )
188 parser.add_argument(
189 "-config-file",
190 dest="config_file",
191 help="Specify the path of .clang-tidy or custom config file",
192 default="",
193 )
194 parser.add_argument("-use-color", action="store_true", help="Use colors in output")
195 parser.add_argument(
196 "-path", dest="build_path", help="Path used to read a compile command database."
197 )
198 if yaml:
199 parser.add_argument(
200 "-export-fixes",
201 metavar="FILE_OR_DIRECTORY",
202 dest="export_fixes",
203 help="A directory or a yaml file to store suggested fixes in, "
204 "which can be applied with clang-apply-replacements. If the "
205 "parameter is a directory, the fixes of each compilation unit are "
206 "stored in individual yaml files in the directory.",
207 )
208 else:
209 parser.add_argument(
210 "-export-fixes",
211 metavar="DIRECTORY",
212 dest="export_fixes",
213 help="A directory to store suggested fixes in, which can be applied "
214 "with clang-apply-replacements. The fixes of each compilation unit are "
215 "stored in individual yaml files in the directory.",
216 )
217 parser.add_argument(
218 "-extra-arg",
219 dest="extra_arg",
220 action="append",
221 default=[],
222 help="Additional argument to append to the compiler " "command line.",
223 )
224 parser.add_argument(
225 "-extra-arg-before",
226 dest="extra_arg_before",
227 action="append",
228 default=[],
229 help="Additional argument to prepend to the compiler " "command line.",
230 )
231 parser.add_argument(
232 "-removed-arg",
233 dest="removed_arg",
234 action="append",
235 default=[],
236 help="Arguments to remove from the compiler command line.",
237 )
238 parser.add_argument(
239 "-quiet",
240 action="store_true",
241 default=False,
242 help="Run clang-tidy in quiet mode",
243 )
244 parser.add_argument(
245 "-load",
246 dest="plugins",
247 action="append",
248 default=[],
249 help="Load the specified plugin in clang-tidy.",
250 )
251 parser.add_argument(
252 "-allow-no-checks",
253 action="store_true",
254 help="Allow empty enabled checks.",
255 )
256 parser.add_argument(
257 "-only-check-in-db",
258 dest="skip_non_compiling",
259 default=False,
260 action="store_true",
261 help="Only check files in the compilation database",
262 )
263 parser.add_argument(
264 "-warnings-as-errors",
265 help="Upgrades clang-tidy warnings to errors. Same format as '-checks'.",
266 default="",
267 )
268 parser.add_argument(
269 "-hide-progress",
270 action="store_true",
271 help="Hide progress",
272 )
273
274 clang_tidy_args = []
275 argv = sys.argv[1:]
276 if "--" in argv:
277 clang_tidy_args.extend(argv[argv.index("--") :])
278 argv = argv[: argv.index("--")]
279
280 args = parser.parse_args(argv)
281
282 compiling_files = get_compiling_files(args) if args.skip_non_compiling else None
283
284 # Extract changed lines for each file.
285 filename = None
286 lines_by_file = {}
287 for line in sys.stdin:
288 match = re.search(r'^\+\+\+\ "?(.*?/){%s}([^ \t\n"]*)' % args.p, line)
289 if match:
290 filename = match.group(2)
291 if filename is None:
292 continue
293
294 if args.regex is not None:
295 if not re.match("^%s$" % args.regex, filename):
296 continue
297 else:
298 if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE):
299 continue
300
301 # Skip any files not in the compiling list
302 if (
303 compiling_files is not None
304 and (Path.cwd() / filename) not in compiling_files
305 ):
306 continue
307
308 match = re.search(r"^@@.*\+(\d+)(,(\d+))?", line)
309 if match:
310 start_line = int(match.group(1))
311 line_count = 1
312 if match.group(3):
313 line_count = int(match.group(3))
314 if line_count == 0:
315 continue
316 end_line = start_line + line_count - 1
317 lines_by_file.setdefault(filename, []).append([start_line, end_line])
318
319 if not any(lines_by_file):
320 print("No relevant changes found.")
321 sys.exit(0)
322
323 max_task_count = args.j
324 if max_task_count == 0:
325 max_task_count = multiprocessing.cpu_count()
326 max_task_count = min(len(lines_by_file), max_task_count)
327 if not args.hide_progress:
328 print(f"Running clang-tidy in {max_task_count} threads...")
329
330 combine_fixes = False
331 export_fixes_dir = None
332 delete_fixes_dir = False
333 if args.export_fixes is not None:
334 # if a directory is given, create it if it does not exist
335 if args.export_fixes.endswith(os.path.sep) and not os.path.isdir(
336 args.export_fixes
337 ):
338 os.makedirs(args.export_fixes)
339
340 if not os.path.isdir(args.export_fixes):
341 if not yaml:
342 raise RuntimeError(
343 "Cannot combine fixes in one yaml file. Either install PyYAML or specify an output directory."
344 )
345
346 combine_fixes = True
347
348 if os.path.isdir(args.export_fixes):
349 export_fixes_dir = args.export_fixes
350
351 if combine_fixes:
352 export_fixes_dir = tempfile.mkdtemp()
353 delete_fixes_dir = True
354
355 # Tasks for clang-tidy.
356 task_queue = queue.Queue(max_task_count)
357 # A lock for console output.
358 lock = threading.Lock()
359
360 # List of files with a non-zero return code.
361 failed_files = []
362
363 # Run a pool of clang-tidy workers.
365 max_task_count, run_tidy, (task_queue, lock, args.timeout, failed_files)
366 )
367
368 # Form the common args list.
369 common_clang_tidy_args = []
370 if args.fix:
371 common_clang_tidy_args.append("-fix")
372 if args.checks != "":
373 common_clang_tidy_args.append("-checks=" + args.checks)
374 if args.config_file != "":
375 common_clang_tidy_args.append("-config-file=" + args.config_file)
376 if args.quiet:
377 common_clang_tidy_args.append("-quiet")
378 if args.build_path is not None:
379 common_clang_tidy_args.append("-p=%s" % args.build_path)
380 if args.use_color:
381 common_clang_tidy_args.append("--use-color")
382 if args.allow_no_checks:
383 common_clang_tidy_args.append("--allow-no-checks")
384 for arg in args.extra_arg:
385 common_clang_tidy_args.append("-extra-arg=%s" % arg)
386 for arg in args.extra_arg_before:
387 common_clang_tidy_args.append("-extra-arg-before=%s" % arg)
388 for arg in args.removed_arg:
389 common_clang_tidy_args.append("-removed-arg=%s" % arg)
390 for plugin in args.plugins:
391 common_clang_tidy_args.append("-load=%s" % plugin)
392 if args.warnings_as_errors:
393 common_clang_tidy_args.append("-warnings-as-errors=" + args.warnings_as_errors)
394
395 for name in lines_by_file:
396 line_filter_json = json.dumps(
397 [{"name": name, "lines": lines_by_file[name]}], separators=(",", ":")
398 )
399
400 # Run clang-tidy on files containing changes.
401 command = [args.clang_tidy_binary]
402 command.append("-line-filter=" + line_filter_json)
403 if args.export_fixes is not None:
404 # Get a temporary file. We immediately close the handle so clang-tidy can
405 # overwrite it.
406 (handle, tmp_name) = tempfile.mkstemp(suffix=".yaml", dir=export_fixes_dir)
407 os.close(handle)
408 command.append("-export-fixes=" + tmp_name)
409 command.extend(common_clang_tidy_args)
410 command.append(name)
411 command.extend(clang_tidy_args)
412
413 task_queue.put(command)
414
415 # Application return code
416 return_code = 0
417
418 # Wait for all threads to be done.
419 task_queue.join()
420 # Application return code
421 return_code = 0
422 if failed_files:
423 return_code = 1
424
425 if combine_fixes:
426 if not args.hide_progress:
427 print(f"Writing fixes to {args.export_fixes} ...")
428 try:
429 merge_replacement_files(export_fixes_dir, args.export_fixes)
430 except:
431 sys.stderr.write("Error exporting fixes.\n")
432 traceback.print_exc()
433 return_code = 1
434
435 if delete_fixes_dir:
436 shutil.rmtree(export_fixes_dir)
437 sys.exit(return_code)
438
439
440if __name__ == "__main__":
441 main()
run_tidy(task_queue, lock, timeout, failed_files)
start_workers(max_tasks, tidy_caller, arguments)
merge_replacement_files(tmpdir, mergefile)