clang-tools  7.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, 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  sys.stdout.write(' '.join(invocation) + '\n')
165  return_code = subprocess.call(invocation)
166  if return_code != 0:
167  failed_files.append(name)
168  queue.task_done()
169 
170 
171 def main():
172  parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
173  'in a compilation database. Requires '
174  'clang-tidy and clang-apply-replacements in '
175  '$PATH.')
176  parser.add_argument('-clang-tidy-binary', metavar='PATH',
177  default='clang-tidy',
178  help='path to clang-tidy binary')
179  parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
180  default='clang-apply-replacements',
181  help='path to clang-apply-replacements binary')
182  parser.add_argument('-checks', default=None,
183  help='checks filter, when not specified, use clang-tidy '
184  'default')
185  parser.add_argument('-config', default=None,
186  help='Specifies a configuration in YAML/JSON format: '
187  ' -config="{Checks: \'*\', '
188  ' CheckOptions: [{key: x, '
189  ' value: y}]}" '
190  'When the value is empty, clang-tidy will '
191  'attempt to find a file named .clang-tidy for '
192  'each source file in its parent directories.')
193  parser.add_argument('-header-filter', default=None,
194  help='regular expression matching the names of the '
195  'headers to output diagnostics from. Diagnostics from '
196  'the main file of each translation unit are always '
197  'displayed.')
198  parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
199  help='Create a yaml file to store suggested fixes in, '
200  'which can be applied with clang-apply-replacements.')
201  parser.add_argument('-j', type=int, default=0,
202  help='number of tidy instances to be run in parallel.')
203  parser.add_argument('files', nargs='*', default=['.*'],
204  help='files to be processed (regex on path)')
205  parser.add_argument('-fix', action='store_true', help='apply fix-its')
206  parser.add_argument('-format', action='store_true', help='Reformat code '
207  'after applying fixes')
208  parser.add_argument('-style', default='file', help='The style of reformat '
209  'code after applying fixes')
210  parser.add_argument('-p', dest='build_path',
211  help='Path used to read a compile command database.')
212  parser.add_argument('-extra-arg', dest='extra_arg',
213  action='append', default=[],
214  help='Additional argument to append to the compiler '
215  'command line.')
216  parser.add_argument('-extra-arg-before', dest='extra_arg_before',
217  action='append', default=[],
218  help='Additional argument to prepend to the compiler '
219  'command line.')
220  parser.add_argument('-quiet', action='store_true',
221  help='Run clang-tidy in quiet mode')
222  args = parser.parse_args()
223 
224  db_path = 'compile_commands.json'
225 
226  if args.build_path is not None:
227  build_path = args.build_path
228  else:
229  # Find our database
230  build_path = find_compilation_database(db_path)
231 
232  try:
233  invocation = [args.clang_tidy_binary, '-list-checks']
234  invocation.append('-p=' + build_path)
235  if args.checks:
236  invocation.append('-checks=' + args.checks)
237  invocation.append('-')
238  subprocess.check_call(invocation)
239  except:
240  print("Unable to run clang-tidy.", file=sys.stderr)
241  sys.exit(1)
242 
243  # Load the database and extract all files.
244  database = json.load(open(os.path.join(build_path, db_path)))
245  files = [make_absolute(entry['file'], entry['directory'])
246  for entry in database]
247 
248  max_task = args.j
249  if max_task == 0:
250  max_task = multiprocessing.cpu_count()
251 
252  tmpdir = None
253  if args.fix or args.export_fixes:
255  tmpdir = tempfile.mkdtemp()
256 
257  # Build up a big regexy filter from all command line arguments.
258  file_name_re = re.compile('|'.join(args.files))
259 
260  return_code = 0
261  try:
262  # Spin up a bunch of tidy-launching threads.
263  task_queue = queue.Queue(max_task)
264  # List of files with a non-zero return code.
265  failed_files = []
266  for _ in range(max_task):
267  t = threading.Thread(target=run_tidy,
268  args=(args, tmpdir, build_path, task_queue, failed_files))
269  t.daemon = True
270  t.start()
271 
272  # Fill the queue with files.
273  for name in files:
274  if file_name_re.search(name):
275  task_queue.put(name)
276 
277  # Wait for all threads to be done.
278  task_queue.join()
279  if len(failed_files):
280  return_code = 1
281 
282  except KeyboardInterrupt:
283  # This is a sad hack. Unfortunately subprocess goes
284  # bonkers with ctrl-c and we start forking merrily.
285  print('\nCtrl-C detected, goodbye.')
286  if tmpdir:
287  shutil.rmtree(tmpdir)
288  os.kill(0, 9)
289 
290  if args.export_fixes:
291  print('Writing fixes to ' + args.export_fixes + ' ...')
292  try:
293  merge_replacement_files(tmpdir, args.export_fixes)
294  except:
295  print('Error exporting fixes.\n', file=sys.stderr)
296  traceback.print_exc()
297  return_code=1
298 
299  if args.fix:
300  print('Applying fixes ...')
301  try:
302  apply_fixes(args, tmpdir)
303  except:
304  print('Error applying fixes.\n', file=sys.stderr)
305  traceback.print_exc()
306  return_code=1
307 
308  if tmpdir:
309  shutil.rmtree(tmpdir)
310  sys.exit(return_code)
311 
312 if __name__ == '__main__':
313  main()
def run_tidy(args, tmpdir, build_path, queue, 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)