clang-tools 8.0.0

run-clang-tidy.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 #===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===#
4 #
5 # The LLVM Compiler Infrastructure
6 #
7 # This file is distributed under the University of Illinois Open Source
8 # License. See LICENSE.TXT for details.
9 #
10 #===------------------------------------------------------------------------===#
11 # FIXME: Integrate with clang-tidy-diff.py
12
13 """
14 Parallel clang-tidy runner
15 ==========================
16
17 Runs clang-tidy over all files in a compilation database. Requires clang-tidy
18 and clang-apply-replacements in $PATH.
19
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
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
33 Compilation database setup:
34 http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
35 """
36
37 from __future__ import print_function
38
39 import argparse
40 import glob
41 import json
42 import multiprocessing
43 import os
44 import re
45 import shutil
46 import subprocess
47 import sys
48 import tempfile
49 import threading
50 import traceback
51 import yaml
52
53 is_py2 = sys.version[0] == '2'
54
55 if is_py2:
56 import Queue as queue
57 else:
58 import queue as queue
59
60 def find_compilation_database(path):
61 """Adjusts the directory until a compilation database is found."""
62 result = './'
63 while not os.path.isfile(os.path.join(result, path)):
64 if os.path.realpath(result) == '/':
65 print('Error: could not find compilation database.')
66 sys.exit(1)
67 result += '../'
68 return os.path.realpath(result)
69
70
71 def make_absolute(f, directory):
72 if os.path.isabs(f):
73 return f
74 return os.path.normpath(os.path.join(directory, f))
75
76
77 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
78 header_filter, extra_arg, extra_arg_before, quiet,
79 config):
80 """Gets a command line for clang-tidy."""
81 start = [clang_tidy_binary]
82 if header_filter is not None:
83 start.append('-header-filter=' + header_filter)
84 else:
85 # Show warnings in all in-project headers by default.
86 start.append('-header-filter=^' + build_path + '/.*')
87 if checks:
88 start.append('-checks=' + checks)
89 if tmpdir is not None:
90 start.append('-export-fixes')
91 # Get a temporary file. We immediately close the handle so clang-tidy can
92 # overwrite it.
93 (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
94 os.close(handle)
95 start.append(name)
96 for arg in extra_arg:
97 start.append('-extra-arg=%s' % arg)
98 for arg in extra_arg_before:
99 start.append('-extra-arg-before=%s' % arg)
100 start.append('-p=' + build_path)
101 if quiet:
102 start.append('-quiet')
103 if config:
104 start.append('-config=' + config)
105 start.append(f)
106 return start
107
108
109 def merge_replacement_files(tmpdir, mergefile):
110 """Merge all replacement files in a directory into a single file"""
111 # The fixes suggested by clang-tidy >= 4.0.0 are given under
112 # the top level key 'Diagnostics' in the output yaml files
113 mergekey="Diagnostics"
114 merged=[]
115 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
116 content = yaml.safe_load(open(replacefile, 'r'))
117 if not content:
118 continue # Skip empty files.
119 merged.extend(content.get(mergekey, []))
120
121 if merged:
122 # MainSourceFile: The key is required by the definition inside
123 # include/clang/Tooling/ReplacementsYaml.h, but the value
124 # is actually never used inside clang-apply-replacements,
125 # so we set it to '' here.
126 output = { 'MainSourceFile': '', mergekey: merged }
127 with open(mergefile, 'w') as out:
128 yaml.safe_dump(output, out)
129 else:
130 # Empty the file:
131 open(mergefile, 'w').close()
132
133
134 def check_clang_apply_replacements_binary(args):
135 """Checks if invoking supplied clang-apply-replacements binary works."""
136 try:
137 subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
138 except:
139 print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
140 'binary correctly specified?', file=sys.stderr)
141 traceback.print_exc()
142 sys.exit(1)
143
144
145 def apply_fixes(args, tmpdir):
146 """Calls clang-apply-fixes on a given directory."""
147 invocation = [args.clang_apply_replacements_binary]
148 if args.format:
149 invocation.append('-format')
150 if args.style:
151 invocation.append('-style=' + args.style)
152 invocation.append(tmpdir)
153 subprocess.call(invocation)
154
155
156 def run_tidy(args, tmpdir, build_path, queue, lock, failed_files):
157 """Takes filenames out of queue and runs clang-tidy on them."""
158 while True:
159 name = queue.get()
160 invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
161 tmpdir, build_path, args.header_filter,
162 args.extra_arg, args.extra_arg_before,
163 args.quiet, args.config)
164
165 proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
166 output, err = proc.communicate()
167 if proc.returncode != 0:
168 failed_files.append(name)
169 with lock:
170 sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8') + '\n')
171 if len(err) > 0:
172 sys.stderr.write(err.decode('utf-8') + '\n')
173 queue.task_done()
174
175
176 def main():
177 parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
178 'in a compilation database. Requires '
179 'clang-tidy and clang-apply-replacements in '
180 '$PATH.')
181 parser.add_argument('-clang-tidy-binary', metavar='PATH',
182 default='clang-tidy',
183 help='path to clang-tidy binary')
184 parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
185 default='clang-apply-replacements',
186 help='path to clang-apply-replacements binary')
187 parser.add_argument('-checks', default=None,
188 help='checks filter, when not specified, use clang-tidy '
189 'default')
190 parser.add_argument('-config', default=None,
191 help='Specifies a configuration in YAML/JSON format: '
192 ' -config="{Checks: \'*\', '
193 ' CheckOptions: [{key: x, '
194 ' value: y}]}" '
195 'When the value is empty, clang-tidy will '
196 'attempt to find a file named .clang-tidy for '
197 'each source file in its parent directories.')
198 parser.add_argument('-header-filter', default=None,
199 help='regular expression matching the names of the '
200 'headers to output diagnostics from. Diagnostics from '
201 'the main file of each translation unit are always '
202 'displayed.')
203 parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
204 help='Create a yaml file to store suggested fixes in, '
205 'which can be applied with clang-apply-replacements.')
206 parser.add_argument('-j', type=int, default=0,
207 help='number of tidy instances to be run in parallel.')
208 parser.add_argument('files', nargs='*', default=['.*'],
209 help='files to be processed (regex on path)')
210 parser.add_argument('-fix', action='store_true', help='apply fix-its')
211 parser.add_argument('-format', action='store_true', help='Reformat code '
212 'after applying fixes')
213 parser.add_argument('-style', default='file', help='The style of reformat '
214 'code after applying fixes')
215 parser.add_argument('-p', dest='build_path',
216 help='Path used to read a compile command database.')
217 parser.add_argument('-extra-arg', dest='extra_arg',
218 action='append', default=[],
219 help='Additional argument to append to the compiler '
220 'command line.')
221 parser.add_argument('-extra-arg-before', dest='extra_arg_before',
222 action='append', default=[],
223 help='Additional argument to prepend to the compiler '
224 'command line.')
225 parser.add_argument('-quiet', action='store_true',
226 help='Run clang-tidy in quiet mode')
227 args = parser.parse_args()
228
229 db_path = 'compile_commands.json'
230
231 if args.build_path is not None:
232 build_path = args.build_path
233 else:
234 # Find our database
235 build_path = find_compilation_database(db_path)
236
237 try:
238 invocation = [args.clang_tidy_binary, '-list-checks']
239 invocation.append('-p=' + build_path)
240 if args.checks:
241 invocation.append('-checks=' + args.checks)
242 invocation.append('-')
243 subprocess.check_call(invocation)
244 except:
245 print("Unable to run clang-tidy.", file=sys.stderr)
246 sys.exit(1)
247
248 # Load the database and extract all files.
249 database = json.load(open(os.path.join(build_path, db_path)))
250 files = [make_absolute(entry['file'], entry['directory'])
251 for entry in database]
252
253 max_task = args.j
254 if max_task == 0:
255 max_task = multiprocessing.cpu_count()
256
257 tmpdir = None
258 if args.fix or args.export_fixes:
259 check_clang_apply_replacements_binary(args)
260 tmpdir = tempfile.mkdtemp()
261
262 # Build up a big regexy filter from all command line arguments.
263 file_name_re = re.compile('|'.join(args.files))
264
265 return_code = 0
266 try:
267 # Spin up a bunch of tidy-launching threads.
268 task_queue = queue.Queue(max_task)
269 # List of files with a non-zero return code.
270 failed_files = []
271 lock = threading.Lock()
272 for _ in range(max_task):
273 t = threading.Thread(target=run_tidy,
274 args=(args, tmpdir, build_path, task_queue, lock, failed_files))
275 t.daemon = True
276 t.start()
277
278 # Fill the queue with files.
279 for name in files:
280 if file_name_re.search(name):
281 task_queue.put(name)
282
283 # Wait for all threads to be done.
284 task_queue.join()
285 if len(failed_files):
286 return_code = 1
287
288 except KeyboardInterrupt:
289 # This is a sad hack. Unfortunately subprocess goes
290 # bonkers with ctrl-c and we start forking merrily.
291 print('\nCtrl-C detected, goodbye.')
292 if tmpdir:
293 shutil.rmtree(tmpdir)
294 os.kill(0, 9)
295
296 if args.export_fixes:
297 print('Writing fixes to ' + args.export_fixes + ' ...')
298 try:
299 merge_replacement_files(tmpdir, args.export_fixes)
300 except:
301 print('Error exporting fixes.\n', file=sys.stderr)
302 traceback.print_exc()
303 return_code=1
304
305 if args.fix:
306 print('Applying fixes ...')
307 try:
308 apply_fixes(args, tmpdir)
309 except:
310 print('Error applying fixes.\n', file=sys.stderr)
311 traceback.print_exc()
312 return_code=1
313
314 if tmpdir:
315 shutil.rmtree(tmpdir)
316 sys.exit(return_code)
317
318 if __name__ == '__main__':
319 main()
run-clang-tidy.run_tidy
def run_tidy(args, tmpdir, build_path, queue, lock, failed_files)
Definition: run-clang-tidy.py:156
run-clang-tidy.get_tidy_invocation
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, header_filter, extra_arg, extra_arg_before, quiet, config)
Definition: run-clang-tidy.py:79
run-clang-tidy.make_absolute
def make_absolute(f, directory)
Definition: run-clang-tidy.py:71
run-clang-tidy.find_compilation_database
def find_compilation_database(path)
Definition: run-clang-tidy.py:60
run-clang-tidy.check_clang_apply_replacements_binary
def check_clang_apply_replacements_binary(args)
Definition: run-clang-tidy.py:134
run-clang-tidy.main
def main()
Definition: run-clang-tidy.py:176
run-clang-tidy.merge_replacement_files
def merge_replacement_files(tmpdir, mergefile)
Definition: run-clang-tidy.py:109
clang::tidy::cppcoreguidelines::join
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
Definition: SpecialMemberFunctionsCheck.cpp:82
run-clang-tidy.apply_fixes
def apply_fixes(args, tmpdir)
Definition: run-clang-tidy.py:145