clang-tools 10.0.0

clang-tidy-diff.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 #===- clang-tidy-diff.py - ClangTidy Diff Checker ------------*- 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
11 r"""
12 ClangTidy Diff Checker
13 ======================
14
15 This script reads input from a unified diff, runs clang-tidy on all changed
16 files and outputs clang-tidy warnings in changed lines only. This is useful to
17 detect clang-tidy regressions in the lines touched by a specific patch.
18 Example 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
26 import argparse
27 import glob
28 import json
29 import multiprocessing
30 import os
31 import re
32 import shutil
33 import subprocess
34 import sys
35 import tempfile
36 import threading
37 import traceback
38
39 try:
40 import yaml
41 except ImportError:
42 yaml = None
43
44 is_py2 = sys.version[0] == '2'
45
46 if is_py2:
47 import Queue as queue
48 else:
49 import queue as queue
50
51
52 def run_tidy(task_queue, lock, timeout):
53 watchdog = None
54 while True:
55 command = task_queue.get()
56 try:
57 proc = subprocess.Popen(command,
58 stdout=subprocess.PIPE,
59 stderr=subprocess.PIPE)
60
61 if timeout is not None:
62 watchdog = threading.Timer(timeout, proc.kill)
63 watchdog.start()
64
65 stdout, stderr = proc.communicate()
66
67 with lock:
68 sys.stdout.write(stdout.decode('utf-8') + '\n')
69 sys.stdout.flush()
70 if stderr:
71 sys.stderr.write(stderr.decode('utf-8') + '\n')
72 sys.stderr.flush()
73 except Exception as e:
74 with lock:
75 sys.stderr.write('Failed: ' + str(e) + ': '.join(command) + '\n')
76 finally:
77 with lock:
78 if (not timeout is None) and (not watchdog is None):
79 if not watchdog.is_alive():
80 sys.stderr.write('Terminated by timeout: ' +
81 ' '.join(command) + '\n')
82 watchdog.cancel()
83 task_queue.task_done()
84
85
86 def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout):
87 for _ in range(max_tasks):
88 t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout))
89 t.daemon = True
90 t.start()
91
92 def merge_replacement_files(tmpdir, mergefile):
93 """Merge all replacement files in a directory into a single file"""
94 # The fixes suggested by clang-tidy >= 4.0.0 are given under
95 # the top level key 'Diagnostics' in the output yaml files
96 mergekey = "Diagnostics"
97 merged = []
98 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
99 content = yaml.safe_load(open(replacefile, 'r'))
100 if not content:
101 continue # Skip empty files.
102 merged.extend(content.get(mergekey, []))
103
104 if merged:
105 # MainSourceFile: The key is required by the definition inside
106 # include/clang/Tooling/ReplacementsYaml.h, but the value
107 # is actually never used inside clang-apply-replacements,
108 # so we set it to '' here.
109 output = { 'MainSourceFile': '', mergekey: merged }
110 with open(mergefile, 'w') as out:
111 yaml.safe_dump(output, out)
112 else:
113 # Empty the file:
114 open(mergefile, 'w').close()
115
116
117 def main():
118 parser = argparse.ArgumentParser(description=
119 'Run clang-tidy against changed files, and '
120 'output diagnostics only for modified '
121 'lines.')
122 parser.add_argument('-clang-tidy-binary', metavar='PATH',
123 default='clang-tidy',
124 help='path to clang-tidy binary')
125 parser.add_argument('-p', metavar='NUM', default=0,
126 help='strip the smallest prefix containing P slashes')
127 parser.add_argument('-regex', metavar='PATTERN', default=None,
128 help='custom pattern selecting file paths to check '
129 '(case sensitive, overrides -iregex)')
130 parser.add_argument('-iregex', metavar='PATTERN', default=
131 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)',
132 help='custom pattern selecting file paths to check '
133 '(case insensitive, overridden by -regex)')
134 parser.add_argument('-j', type=int, default=1,
135 help='number of tidy instances to be run in parallel.')
136 parser.add_argument('-timeout', type=int, default=None,
137 help='timeout per each file in seconds.')
138 parser.add_argument('-fix', action='store_true', default=False,
139 help='apply suggested fixes')
140 parser.add_argument('-checks',
141 help='checks filter, when not specified, use clang-tidy '
142 'default',
143 default='')
144 parser.add_argument('-path', dest='build_path',
145 help='Path used to read a compile command database.')
146 if yaml:
147 parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes',
148 help='Create a yaml file to store suggested fixes in, '
149 'which can be applied with clang-apply-replacements.')
150 parser.add_argument('-extra-arg', dest='extra_arg',
151 action='append', default=[],
152 help='Additional argument to append to the compiler '
153 'command line.')
154 parser.add_argument('-extra-arg-before', dest='extra_arg_before',
155 action='append', default=[],
156 help='Additional argument to prepend to the compiler '
157 'command line.')
158 parser.add_argument('-quiet', action='store_true', default=False,
159 help='Run clang-tidy in quiet mode')
160 clang_tidy_args = []
161 argv = sys.argv[1:]
162 if '--' in argv:
163 clang_tidy_args.extend(argv[argv.index('--'):])
164 argv = argv[:argv.index('--')]
165
166 args = parser.parse_args(argv)
167
168 # Extract changed lines for each file.
169 filename = None
170 lines_by_file = {}
171 for line in sys.stdin:
172 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line)
173 if match:
174 filename = match.group(2)
175 if filename is None:
176 continue
177
178 if args.regex is not None:
179 if not re.match('^%s$' % args.regex, filename):
180 continue
181 else:
182 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
183 continue
184
185 match = re.search('^@@.*\+(\d+)(,(\d+))?', line)
186 if match:
187 start_line = int(match.group(1))
188 line_count = 1
189 if match.group(3):
190 line_count = int(match.group(3))
191 if line_count == 0:
192 continue
193 end_line = start_line + line_count - 1
194 lines_by_file.setdefault(filename, []).append([start_line, end_line])
195
196 if not any(lines_by_file):
197 print("No relevant changes found.")
198 sys.exit(0)
199
200 max_task_count = args.j
201 if max_task_count == 0:
202 max_task_count = multiprocessing.cpu_count()
203 max_task_count = min(len(lines_by_file), max_task_count)
204
205 tmpdir = None
206 if yaml and args.export_fixes:
207 tmpdir = tempfile.mkdtemp()
208
209 # Tasks for clang-tidy.
210 task_queue = queue.Queue(max_task_count)
211 # A lock for console output.
212 lock = threading.Lock()
213
214 # Run a pool of clang-tidy workers.
215 start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout)
216
217 # Form the common args list.
218 common_clang_tidy_args = []
219 if args.fix:
220 common_clang_tidy_args.append('-fix')
221 if args.checks != '':
222 common_clang_tidy_args.append('-checks=' + args.checks)
223 if args.quiet:
224 common_clang_tidy_args.append('-quiet')
225 if args.build_path is not None:
226 common_clang_tidy_args.append('-p=%s' % args.build_path)
227 for arg in args.extra_arg:
228 common_clang_tidy_args.append('-extra-arg=%s' % arg)
229 for arg in args.extra_arg_before:
230 common_clang_tidy_args.append('-extra-arg-before=%s' % arg)
231
232 for name in lines_by_file:
233 line_filter_json = json.dumps(
234 [{"name": name, "lines": lines_by_file[name]}],
235 separators=(',', ':'))
236
237 # Run clang-tidy on files containing changes.
238 command = [args.clang_tidy_binary]
239 command.append('-line-filter=' + line_filter_json)
240 if yaml and args.export_fixes:
241 # Get a temporary file. We immediately close the handle so clang-tidy can
242 # overwrite it.
243 (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
244 os.close(handle)
245 command.append('-export-fixes=' + tmp_name)
246 command.extend(common_clang_tidy_args)
247 command.append(name)
248 command.extend(clang_tidy_args)
249
250 task_queue.put(command)
251
252 # Wait for all threads to be done.
253 task_queue.join()
254
255 if yaml and args.export_fixes:
256 print('Writing fixes to ' + args.export_fixes + ' ...')
257 try:
258 merge_replacement_files(tmpdir, args.export_fixes)
259 except:
260 sys.stderr.write('Error exporting fixes.\n')
261 traceback.print_exc()
262
263 if tmpdir:
264 shutil.rmtree(tmpdir)
265
266
267 if __name__ == '__main__':
268 main()
clang-tidy-diff.merge_replacement_files
def merge_replacement_files(tmpdir, mergefile)
Definition: clang-tidy-diff.py:92
clang-tidy-diff.main
def main()
Definition: clang-tidy-diff.py:117
clang-tidy-diff.start_workers
def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout)
Definition: clang-tidy-diff.py:86
clang-tidy-diff.run_tidy
def run_tidy(task_queue, lock, timeout)
Definition: clang-tidy-diff.py:52
clang::tidy::cppcoreguidelines::join
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
Definition: SpecialMemberFunctionsCheck.cpp:81