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