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 
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 
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:
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()
def run_tidy(args, tmpdir, build_path, queue, lock, failed_files)
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, header_filter, extra_arg, extra_arg_before, quiet, config)
def make_absolute(f, directory)
def find_compilation_database(path)
def check_clang_apply_replacements_binary(args)
def merge_replacement_files(tmpdir, mergefile)
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
def apply_fixes(args, tmpdir)