clang-tools  12.0.0git
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 """
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 
52 try:
53  import yaml
54 except ImportError:
55  yaml = None
56 
57 is_py2 = sys.version[0] == '2'
58 
59 if is_py2:
60  import Queue as queue
61 else:
62  import queue as queue
63 
64 
66  """Adjusts the directory until a compilation database is found."""
67  result = './'
68  while not os.path.isfile(os.path.join(result, path)):
69  if os.path.realpath(result) == '/':
70  print('Error: could not find compilation database.')
71  sys.exit(1)
72  result += '../'
73  return os.path.realpath(result)
74 
75 
76 def make_absolute(f, directory):
77  if os.path.isabs(f):
78  return f
79  return os.path.normpath(os.path.join(directory, f))
80 
81 
82 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
83  header_filter, allow_enabling_alpha_checkers,
84  extra_arg, extra_arg_before, quiet, config):
85  """Gets a command line for clang-tidy."""
86  start = [clang_tidy_binary]
87  if allow_enabling_alpha_checkers is not None:
88  start.append('-allow-enabling-analyzer-alpha-checkers')
89  if header_filter is not None:
90  start.append('-header-filter=' + header_filter)
91  if checks:
92  start.append('-checks=' + checks)
93  if tmpdir is not None:
94  start.append('-export-fixes')
95  # Get a temporary file. We immediately close the handle so clang-tidy can
96  # overwrite it.
97  (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
98  os.close(handle)
99  start.append(name)
100  for arg in extra_arg:
101  start.append('-extra-arg=%s' % arg)
102  for arg in extra_arg_before:
103  start.append('-extra-arg-before=%s' % arg)
104  start.append('-p=' + build_path)
105  if quiet:
106  start.append('-quiet')
107  if config:
108  start.append('-config=' + config)
109  start.append(f)
110  return start
111 
112 
113 def merge_replacement_files(tmpdir, mergefile):
114  """Merge all replacement files in a directory into a single file"""
115  # The fixes suggested by clang-tidy >= 4.0.0 are given under
116  # the top level key 'Diagnostics' in the output yaml files
117  mergekey = "Diagnostics"
118  merged=[]
119  for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
120  content = yaml.safe_load(open(replacefile, 'r'))
121  if not content:
122  continue # Skip empty files.
123  merged.extend(content.get(mergekey, []))
124 
125  if merged:
126  # MainSourceFile: The key is required by the definition inside
127  # include/clang/Tooling/ReplacementsYaml.h, but the value
128  # is actually never used inside clang-apply-replacements,
129  # so we set it to '' here.
130  output = {'MainSourceFile': '', mergekey: merged}
131  with open(mergefile, 'w') as out:
132  yaml.safe_dump(output, out)
133  else:
134  # Empty the file:
135  open(mergefile, 'w').close()
136 
137 
139  """Checks if invoking supplied clang-apply-replacements binary works."""
140  try:
141  subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
142  except:
143  print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
144  'binary correctly specified?', file=sys.stderr)
145  traceback.print_exc()
146  sys.exit(1)
147 
148 
149 def apply_fixes(args, tmpdir):
150  """Calls clang-apply-fixes on a given directory."""
151  invocation = [args.clang_apply_replacements_binary]
152  if args.format:
153  invocation.append('-format')
154  if args.style:
155  invocation.append('-style=' + args.style)
156  invocation.append(tmpdir)
157  subprocess.call(invocation)
158 
159 
160 def run_tidy(args, tmpdir, build_path, queue, lock, failed_files):
161  """Takes filenames out of queue and runs clang-tidy on them."""
162  while True:
163  name = queue.get()
164  invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
165  tmpdir, build_path, args.header_filter,
166  args.allow_enabling_alpha_checkers,
167  args.extra_arg, args.extra_arg_before,
168  args.quiet, args.config)
169 
170  proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
171  output, err = proc.communicate()
172  if proc.returncode != 0:
173  failed_files.append(name)
174  with lock:
175  sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8'))
176  if len(err) > 0:
177  sys.stdout.flush()
178  sys.stderr.write(err.decode('utf-8'))
179  queue.task_done()
180 
181 
182 def main():
183  parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
184  'in a compilation database. Requires '
185  'clang-tidy and clang-apply-replacements in '
186  '$PATH.')
187  parser.add_argument('-allow-enabling-alpha-checkers',
188  action='store_true', help='allow alpha checkers from '
189  'clang-analyzer.')
190  parser.add_argument('-clang-tidy-binary', metavar='PATH',
191  default='clang-tidy',
192  help='path to clang-tidy binary')
193  parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
194  default='clang-apply-replacements',
195  help='path to clang-apply-replacements binary')
196  parser.add_argument('-checks', default=None,
197  help='checks filter, when not specified, use clang-tidy '
198  'default')
199  parser.add_argument('-config', default=None,
200  help='Specifies a configuration in YAML/JSON format: '
201  ' -config="{Checks: \'*\', '
202  ' CheckOptions: [{key: x, '
203  ' value: y}]}" '
204  'When the value is empty, clang-tidy will '
205  'attempt to find a file named .clang-tidy for '
206  'each source file in its parent directories.')
207  parser.add_argument('-header-filter', default=None,
208  help='regular expression matching the names of the '
209  'headers to output diagnostics from. Diagnostics from '
210  'the main file of each translation unit are always '
211  'displayed.')
212  if yaml:
213  parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
214  help='Create a yaml file to store suggested fixes in, '
215  'which can be applied with clang-apply-replacements.')
216  parser.add_argument('-j', type=int, default=0,
217  help='number of tidy instances to be run in parallel.')
218  parser.add_argument('files', nargs='*', default=['.*'],
219  help='files to be processed (regex on path)')
220  parser.add_argument('-fix', action='store_true', help='apply fix-its')
221  parser.add_argument('-format', action='store_true', help='Reformat code '
222  'after applying fixes')
223  parser.add_argument('-style', default='file', help='The style of reformat '
224  'code after applying fixes')
225  parser.add_argument('-p', dest='build_path',
226  help='Path used to read a compile command database.')
227  parser.add_argument('-extra-arg', dest='extra_arg',
228  action='append', default=[],
229  help='Additional argument to append to the compiler '
230  'command line.')
231  parser.add_argument('-extra-arg-before', dest='extra_arg_before',
232  action='append', default=[],
233  help='Additional argument to prepend to the compiler '
234  'command line.')
235  parser.add_argument('-quiet', action='store_true',
236  help='Run clang-tidy in quiet mode')
237  args = parser.parse_args()
238 
239  db_path = 'compile_commands.json'
240 
241  if args.build_path is not None:
242  build_path = args.build_path
243  else:
244  # Find our database
245  build_path = find_compilation_database(db_path)
246 
247  try:
248  invocation = [args.clang_tidy_binary, '-list-checks']
249  if args.allow_enabling_alpha_checkers:
250  invocation.append('-allow-enabling-analyzer-alpha-checkers')
251  invocation.append('-p=' + build_path)
252  if args.checks:
253  invocation.append('-checks=' + args.checks)
254  invocation.append('-')
255  if args.quiet:
256  # Even with -quiet we still want to check if we can call clang-tidy.
257  with open(os.devnull, 'w') as dev_null:
258  subprocess.check_call(invocation, stdout=dev_null)
259  else:
260  subprocess.check_call(invocation)
261  except:
262  print("Unable to run clang-tidy.", file=sys.stderr)
263  sys.exit(1)
264 
265  # Load the database and extract all files.
266  database = json.load(open(os.path.join(build_path, db_path)))
267  files = [make_absolute(entry['file'], entry['directory'])
268  for entry in database]
269 
270  max_task = args.j
271  if max_task == 0:
272  max_task = multiprocessing.cpu_count()
273 
274  tmpdir = None
275  if args.fix or (yaml and args.export_fixes):
277  tmpdir = tempfile.mkdtemp()
278 
279  # Build up a big regexy filter from all command line arguments.
280  file_name_re = re.compile('|'.join(args.files))
281 
282  return_code = 0
283  try:
284  # Spin up a bunch of tidy-launching threads.
285  task_queue = queue.Queue(max_task)
286  # List of files with a non-zero return code.
287  failed_files = []
288  lock = threading.Lock()
289  for _ in range(max_task):
290  t = threading.Thread(target=run_tidy,
291  args=(args, tmpdir, build_path, task_queue, lock, failed_files))
292  t.daemon = True
293  t.start()
294 
295  # Fill the queue with files.
296  for name in files:
297  if file_name_re.search(name):
298  task_queue.put(name)
299 
300  # Wait for all threads to be done.
301  task_queue.join()
302  if len(failed_files):
303  return_code = 1
304 
305  except KeyboardInterrupt:
306  # This is a sad hack. Unfortunately subprocess goes
307  # bonkers with ctrl-c and we start forking merrily.
308  print('\nCtrl-C detected, goodbye.')
309  if tmpdir:
310  shutil.rmtree(tmpdir)
311  os.kill(0, 9)
312 
313  if yaml and args.export_fixes:
314  print('Writing fixes to ' + args.export_fixes + ' ...')
315  try:
316  merge_replacement_files(tmpdir, args.export_fixes)
317  except:
318  print('Error exporting fixes.\n', file=sys.stderr)
319  traceback.print_exc()
320  return_code=1
321 
322  if args.fix:
323  print('Applying fixes ...')
324  try:
325  apply_fixes(args, tmpdir)
326  except:
327  print('Error applying fixes.\n', file=sys.stderr)
328  traceback.print_exc()
329  return_code = 1
330 
331  if tmpdir:
332  shutil.rmtree(tmpdir)
333  sys.exit(return_code)
334 
335 
336 if __name__ == '__main__':
337  main()
def run_tidy(args, tmpdir, build_path, queue, lock, failed_files)
def make_absolute(f, directory)
def find_compilation_database(path)
def check_clang_apply_replacements_binary(args)
def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, header_filter, allow_enabling_alpha_checkers, extra_arg, extra_arg_before, quiet, config)
def merge_replacement_files(tmpdir, mergefile)
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
def apply_fixes(args, tmpdir)