clang-tools  6.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  """Gets a command line for clang-tidy."""
80  start = [clang_tidy_binary]
81  if header_filter is not None:
82  start.append('-header-filter=' + header_filter)
83  else:
84  # Show warnings in all in-project headers by default.
85  start.append('-header-filter=^' + build_path + '/.*')
86  if checks:
87  start.append('-checks=' + checks)
88  if tmpdir is not None:
89  start.append('-export-fixes')
90  # Get a temporary file. We immediately close the handle so clang-tidy can
91  # overwrite it.
92  (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
93  os.close(handle)
94  start.append(name)
95  for arg in extra_arg:
96  start.append('-extra-arg=%s' % arg)
97  for arg in extra_arg_before:
98  start.append('-extra-arg-before=%s' % arg)
99  start.append('-p=' + build_path)
100  if quiet:
101  start.append('-quiet')
102  start.append(f)
103  return start
104 
105 
106 def merge_replacement_files(tmpdir, mergefile):
107  """Merge all replacement files in a directory into a single file"""
108  # The fixes suggested by clang-tidy >= 4.0.0 are given under
109  # the top level key 'Diagnostics' in the output yaml files
110  mergekey="Diagnostics"
111  merged=[]
112  for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
113  content = yaml.safe_load(open(replacefile, 'r'))
114  if not content:
115  continue # Skip empty files.
116  merged.extend(content.get(mergekey, []))
117 
118  if merged:
119  # MainSourceFile: The key is required by the definition inside
120  # include/clang/Tooling/ReplacementsYaml.h, but the value
121  # is actually never used inside clang-apply-replacements,
122  # so we set it to '' here.
123  output = { 'MainSourceFile': '', mergekey: merged }
124  with open(mergefile, 'w') as out:
125  yaml.safe_dump(output, out)
126  else:
127  # Empty the file:
128  open(mergefile, 'w').close()
129 
130 
132  """Checks if invoking supplied clang-apply-replacements binary works."""
133  try:
134  subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
135  except:
136  print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
137  'binary correctly specified?', file=sys.stderr)
138  traceback.print_exc()
139  sys.exit(1)
140 
141 
142 def apply_fixes(args, tmpdir):
143  """Calls clang-apply-fixes on a given directory."""
144  invocation = [args.clang_apply_replacements_binary]
145  if args.format:
146  invocation.append('-format')
147  if args.style:
148  invocation.append('-style=' + args.style)
149  invocation.append(tmpdir)
150  subprocess.call(invocation)
151 
152 
153 def run_tidy(args, tmpdir, build_path, queue):
154  """Takes filenames out of queue and runs clang-tidy on them."""
155  while True:
156  name = queue.get()
157  invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
158  tmpdir, build_path, args.header_filter,
159  args.extra_arg, args.extra_arg_before,
160  args.quiet)
161  sys.stdout.write(' '.join(invocation) + '\n')
162  subprocess.call(invocation)
163  queue.task_done()
164 
165 
166 def main():
167  parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
168  'in a compilation database. Requires '
169  'clang-tidy and clang-apply-replacements in '
170  '$PATH.')
171  parser.add_argument('-clang-tidy-binary', metavar='PATH',
172  default='clang-tidy',
173  help='path to clang-tidy binary')
174  parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
175  default='clang-apply-replacements',
176  help='path to clang-apply-replacements binary')
177  parser.add_argument('-checks', default=None,
178  help='checks filter, when not specified, use clang-tidy '
179  'default')
180  parser.add_argument('-header-filter', default=None,
181  help='regular expression matching the names of the '
182  'headers to output diagnostics from. Diagnostics from '
183  'the main file of each translation unit are always '
184  'displayed.')
185  parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
186  help='Create a yaml file to store suggested fixes in, '
187  'which can be applied with clang-apply-replacements.')
188  parser.add_argument('-j', type=int, default=0,
189  help='number of tidy instances to be run in parallel.')
190  parser.add_argument('files', nargs='*', default=['.*'],
191  help='files to be processed (regex on path)')
192  parser.add_argument('-fix', action='store_true', help='apply fix-its')
193  parser.add_argument('-format', action='store_true', help='Reformat code '
194  'after applying fixes')
195  parser.add_argument('-style', default='file', help='The style of reformat '
196  'code after applying fixes')
197  parser.add_argument('-p', dest='build_path',
198  help='Path used to read a compile command database.')
199  parser.add_argument('-extra-arg', dest='extra_arg',
200  action='append', default=[],
201  help='Additional argument to append to the compiler '
202  'command line.')
203  parser.add_argument('-extra-arg-before', dest='extra_arg_before',
204  action='append', default=[],
205  help='Additional argument to prepend to the compiler '
206  'command line.')
207  parser.add_argument('-quiet', action='store_true',
208  help='Run clang-tidy in quiet mode')
209  args = parser.parse_args()
210 
211  db_path = 'compile_commands.json'
212 
213  if args.build_path is not None:
214  build_path = args.build_path
215  else:
216  # Find our database
217  build_path = find_compilation_database(db_path)
218 
219  try:
220  invocation = [args.clang_tidy_binary, '-list-checks']
221  invocation.append('-p=' + build_path)
222  if args.checks:
223  invocation.append('-checks=' + args.checks)
224  invocation.append('-')
225  subprocess.check_call(invocation)
226  except:
227  print("Unable to run clang-tidy.", file=sys.stderr)
228  sys.exit(1)
229 
230  # Load the database and extract all files.
231  database = json.load(open(os.path.join(build_path, db_path)))
232  files = [make_absolute(entry['file'], entry['directory'])
233  for entry in database]
234 
235  max_task = args.j
236  if max_task == 0:
237  max_task = multiprocessing.cpu_count()
238 
239  tmpdir = None
240  if args.fix or args.export_fixes:
242  tmpdir = tempfile.mkdtemp()
243 
244  # Build up a big regexy filter from all command line arguments.
245  file_name_re = re.compile('|'.join(args.files))
246 
247  try:
248  # Spin up a bunch of tidy-launching threads.
249  task_queue = queue.Queue(max_task)
250  for _ in range(max_task):
251  t = threading.Thread(target=run_tidy,
252  args=(args, tmpdir, build_path, task_queue))
253  t.daemon = True
254  t.start()
255 
256  # Fill the queue with files.
257  for name in files:
258  if file_name_re.search(name):
259  task_queue.put(name)
260 
261  # Wait for all threads to be done.
262  task_queue.join()
263 
264  except KeyboardInterrupt:
265  # This is a sad hack. Unfortunately subprocess goes
266  # bonkers with ctrl-c and we start forking merrily.
267  print('\nCtrl-C detected, goodbye.')
268  if tmpdir:
269  shutil.rmtree(tmpdir)
270  os.kill(0, 9)
271 
272  return_code = 0
273  if args.export_fixes:
274  print('Writing fixes to ' + args.export_fixes + ' ...')
275  try:
276  merge_replacement_files(tmpdir, args.export_fixes)
277  except:
278  print('Error exporting fixes.\n', file=sys.stderr)
279  traceback.print_exc()
280  return_code=1
281 
282  if args.fix:
283  print('Applying fixes ...')
284  try:
285  apply_fixes(args, tmpdir)
286  except:
287  print('Error applying fixes.\n', file=sys.stderr)
288  traceback.print_exc()
289  return_code=1
290 
291  if tmpdir:
292  shutil.rmtree(tmpdir)
293  sys.exit(return_code)
294 
295 if __name__ == '__main__':
296  main()
def make_absolute(f, directory)
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, header_filter, extra_arg, extra_arg_before, quiet)
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)
def run_tidy(args, tmpdir, build_path, queue)