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