clang-tools 10.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 # 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 Parallel clang-tidy runner
14 ==========================
15
16 Runs clang-tidy over all files in a compilation database. Requires clang-tidy
17 and clang-apply-replacements in $PATH.
18
19 Example invocations.
20 - Run clang-tidy on all files in the current working directory with a default
21 set of checks and show warnings in the cpp files and all project headers.
22 run-clang-tidy.py $PWD
23
24 - Fix all header guards.
25 run-clang-tidy.py -fix -checks=-*,llvm-header-guard
26
27 - Fix all header guards included from clang-tidy and header guards
28 for clang-tidy headers.
29 run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \
30 -header-filter=extra/clang-tidy
31
32 Compilation database setup:
33 http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
34 """
35
36 from __future__ import print_function
37
38 import argparse
39 import glob
40 import json
41 import multiprocessing
42 import os
43 import re
44 import shutil
45 import subprocess
46 import sys
47 import tempfile
48 import threading
49 import traceback
50
51 try:
52 import yaml
53 except ImportError:
54 yaml = None
55
56 is_py2 = sys.version[0] == '2'
57
58 if is_py2:
59 import Queue as queue
60 else:
61 import queue as queue
62
63 def find_compilation_database(path):
64 """Adjusts the directory until a compilation database is found."""
65 result = './'
66 while not os.path.isfile(os.path.join(result, path)):
67 if os.path.realpath(result) == '/':
68 print('Error: could not find compilation database.')
69 sys.exit(1)
70 result += '../'
71 return os.path.realpath(result)
72
73
74 def make_absolute(f, directory):
75 if os.path.isabs(f):
76 return f
77 return os.path.normpath(os.path.join(directory, f))
78
79
80 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
81 header_filter, extra_arg, extra_arg_before, quiet,
82 config):
83 """Gets a command line for clang-tidy."""
84 start = [clang_tidy_binary]
85 if header_filter is not None:
86 start.append('-header-filter=' + header_filter)
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'))
171 if len(err) > 0:
172 sys.stdout.flush()
173 sys.stderr.write(err.decode('utf-8'))
174 queue.task_done()
175
176
177 def main():
178 parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
179 'in a compilation database. Requires '
180 'clang-tidy and clang-apply-replacements in '
181 '$PATH.')
182 parser.add_argument('-clang-tidy-binary', metavar='PATH',
183 default='clang-tidy',
184 help='path to clang-tidy binary')
185 parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
186 default='clang-apply-replacements',
187 help='path to clang-apply-replacements binary')
188 parser.add_argument('-checks', default=None,
189 help='checks filter, when not specified, use clang-tidy '
190 'default')
191 parser.add_argument('-config', default=None,
192 help='Specifies a configuration in YAML/JSON format: '
193 ' -config="{Checks: \'*\', '
194 ' CheckOptions: [{key: x, '
195 ' value: y}]}" '
196 'When the value is empty, clang-tidy will '
197 'attempt to find a file named .clang-tidy for '
198 'each source file in its parent directories.')
199 parser.add_argument('-header-filter', default=None,
200 help='regular expression matching the names of the '
201 'headers to output diagnostics from. Diagnostics from '
202 'the main file of each translation unit are always '
203 'displayed.')
204 if yaml:
205 parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
206 help='Create a yaml file to store suggested fixes in, '
207 'which can be applied with clang-apply-replacements.')
208 parser.add_argument('-j', type=int, default=0,
209 help='number of tidy instances to be run in parallel.')
210 parser.add_argument('files', nargs='*', default=['.*'],
211 help='files to be processed (regex on path)')
212 parser.add_argument('-fix', action='store_true', help='apply fix-its')
213 parser.add_argument('-format', action='store_true', help='Reformat code '
214 'after applying fixes')
215 parser.add_argument('-style', default='file', help='The style of reformat '
216 'code after applying fixes')
217 parser.add_argument('-p', dest='build_path',
218 help='Path used to read a compile command database.')
219 parser.add_argument('-extra-arg', dest='extra_arg',
220 action='append', default=[],
221 help='Additional argument to append to the compiler '
222 'command line.')
223 parser.add_argument('-extra-arg-before', dest='extra_arg_before',
224 action='append', default=[],
225 help='Additional argument to prepend to the compiler '
226 'command line.')
227 parser.add_argument('-quiet', action='store_true',
228 help='Run clang-tidy in quiet mode')
229 args = parser.parse_args()
230
231 db_path = 'compile_commands.json'
232
233 if args.build_path is not None:
234 build_path = args.build_path
235 else:
236 # Find our database
237 build_path = find_compilation_database(db_path)
238
239 try:
240 invocation = [args.clang_tidy_binary, '-list-checks']
241 invocation.append('-p=' + build_path)
242 if args.checks:
243 invocation.append('-checks=' + args.checks)
244 invocation.append('-')
245 if args.quiet:
246 # Even with -quiet we still want to check if we can call clang-tidy.
247 with open(os.devnull, 'w') as dev_null:
248 subprocess.check_call(invocation, stdout=dev_null)
249 else:
250 subprocess.check_call(invocation)
251 except:
252 print("Unable to run clang-tidy.", file=sys.stderr)
253 sys.exit(1)
254
255 # Load the database and extract all files.
256 database = json.load(open(os.path.join(build_path, db_path)))
257 files = [make_absolute(entry['file'], entry['directory'])
258 for entry in database]
259
260 max_task = args.j
261 if max_task == 0:
262 max_task = multiprocessing.cpu_count()
263
264 tmpdir = None
265 if args.fix or (yaml and args.export_fixes):
266 check_clang_apply_replacements_binary(args)
267 tmpdir = tempfile.mkdtemp()
268
269 # Build up a big regexy filter from all command line arguments.
270 file_name_re = re.compile('|'.join(args.files))
271
272 return_code = 0
273 try:
274 # Spin up a bunch of tidy-launching threads.
275 task_queue = queue.Queue(max_task)
276 # List of files with a non-zero return code.
277 failed_files = []
278 lock = threading.Lock()
279 for _ in range(max_task):
280 t = threading.Thread(target=run_tidy,
281 args=(args, tmpdir, build_path, task_queue, lock, failed_files))
282 t.daemon = True
283 t.start()
284
285 # Fill the queue with files.
286 for name in files:
287 if file_name_re.search(name):
288 task_queue.put(name)
289
290 # Wait for all threads to be done.
291 task_queue.join()
292 if len(failed_files):
293 return_code = 1
294
295 except KeyboardInterrupt:
296 # This is a sad hack. Unfortunately subprocess goes
297 # bonkers with ctrl-c and we start forking merrily.
298 print('\nCtrl-C detected, goodbye.')
299 if tmpdir:
300 shutil.rmtree(tmpdir)
301 os.kill(0, 9)
302
303 if yaml and args.export_fixes:
304 print('Writing fixes to ' + args.export_fixes + ' ...')
305 try:
306 merge_replacement_files(tmpdir, args.export_fixes)
307 except:
308 print('Error exporting fixes.\n', file=sys.stderr)
309 traceback.print_exc()
310 return_code=1
311
312 if args.fix:
313 print('Applying fixes ...')
314 try:
315 apply_fixes(args, tmpdir)
316 except:
317 print('Error applying fixes.\n', file=sys.stderr)
318 traceback.print_exc()
319 return_code=1
320
321 if tmpdir:
322 shutil.rmtree(tmpdir)
323 sys.exit(return_code)
324
325 if __name__ == '__main__':
326 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:82
run-clang-tidy.make_absolute
def make_absolute(f, directory)
Definition: run-clang-tidy.py:74
run-clang-tidy.find_compilation_database
def find_compilation_database(path)
Definition: run-clang-tidy.py:63
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:177
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:81
run-clang-tidy.apply_fixes
def apply_fixes(args, tmpdir)
Definition: run-clang-tidy.py:145