clang-tools  10.0.0svn
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 
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 
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):
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()
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)