clang-tools  16.0.0git
run-clang-tidy.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
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 queue
45 import re
46 import shutil
47 import subprocess
48 import sys
49 import tempfile
50 import threading
51 import traceback
52 
53 try:
54  import yaml
55 except ImportError:
56  yaml = None
57 
58 
59 def strtobool(val):
60  """Convert a string representation of truth to a bool following LLVM's CLI argument parsing."""
61 
62  val = val.lower()
63  if val in ['', 'true', '1']:
64  return True
65  elif val in ['false', '0']:
66  return False
67 
68  # Return ArgumentTypeError so that argparse does not substitute its own error message
69  raise argparse.ArgumentTypeError(
70  "'{}' is invalid value for boolean argument! Try 0 or 1.".format(val)
71  )
72 
73 
75  """Adjusts the directory until a compilation database is found."""
76  result = os.path.realpath('./')
77  while not os.path.isfile(os.path.join(result, path)):
78  parent = os.path.dirname(result)
79  if result == parent:
80  print('Error: could not find compilation database.')
81  sys.exit(1)
82  result = parent
83  return result
84 
85 
86 def make_absolute(f, directory):
87  if os.path.isabs(f):
88  return f
89  return os.path.normpath(os.path.join(directory, f))
90 
91 
92 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
93  header_filter, allow_enabling_alpha_checkers,
94  extra_arg, extra_arg_before, quiet, config_file_path,
95  config, line_filter, use_color, plugins):
96  """Gets a command line for clang-tidy."""
97  start = [clang_tidy_binary]
98  if allow_enabling_alpha_checkers:
99  start.append('-allow-enabling-analyzer-alpha-checkers')
100  if header_filter is not None:
101  start.append('-header-filter=' + header_filter)
102  if line_filter is not None:
103  start.append('-line-filter=' + line_filter)
104  if use_color is not None:
105  if use_color:
106  start.append('--use-color')
107  else:
108  start.append('--use-color=false')
109  if checks:
110  start.append('-checks=' + checks)
111  if tmpdir is not None:
112  start.append('-export-fixes')
113  # Get a temporary file. We immediately close the handle so clang-tidy can
114  # overwrite it.
115  (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
116  os.close(handle)
117  start.append(name)
118  for arg in extra_arg:
119  start.append('-extra-arg=%s' % arg)
120  for arg in extra_arg_before:
121  start.append('-extra-arg-before=%s' % arg)
122  start.append('-p=' + build_path)
123  if quiet:
124  start.append('-quiet')
125  if config_file_path:
126  start.append('--config-file=' + config_file_path)
127  elif config:
128  start.append('-config=' + config)
129  for plugin in plugins:
130  start.append('-load=' + plugin)
131  start.append(f)
132  return start
133 
134 
135 def merge_replacement_files(tmpdir, mergefile):
136  """Merge all replacement files in a directory into a single file"""
137  # The fixes suggested by clang-tidy >= 4.0.0 are given under
138  # the top level key 'Diagnostics' in the output yaml files
139  mergekey = "Diagnostics"
140  merged=[]
141  for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
142  content = yaml.safe_load(open(replacefile, 'r'))
143  if not content:
144  continue # Skip empty files.
145  merged.extend(content.get(mergekey, []))
146 
147  if merged:
148  # MainSourceFile: The key is required by the definition inside
149  # include/clang/Tooling/ReplacementsYaml.h, but the value
150  # is actually never used inside clang-apply-replacements,
151  # so we set it to '' here.
152  output = {'MainSourceFile': '', mergekey: merged}
153  with open(mergefile, 'w') as out:
154  yaml.safe_dump(output, out)
155  else:
156  # Empty the file:
157  open(mergefile, 'w').close()
158 
159 
160 def find_binary(arg, name, build_path):
161  """Get the path for a binary or exit"""
162  if arg:
163  if shutil.which(arg):
164  return arg
165  else:
166  raise SystemExit(
167  "error: passed binary '{}' was not found or is not executable"
168  .format(arg))
169 
170  built_path = os.path.join(build_path, "bin", name)
171  binary = shutil.which(name) or shutil.which(built_path)
172  if binary:
173  return binary
174  else:
175  raise SystemExit(
176  "error: failed to find {} in $PATH or at {}"
177  .format(name, built_path))
178 
179 
180 def apply_fixes(args, clang_apply_replacements_binary, tmpdir):
181  """Calls clang-apply-fixes on a given directory."""
182  invocation = [clang_apply_replacements_binary]
183  invocation.append('-ignore-insert-conflict')
184  if args.format:
185  invocation.append('-format')
186  if args.style:
187  invocation.append('-style=' + args.style)
188  invocation.append(tmpdir)
189  subprocess.call(invocation)
190 
191 
192 def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock,
193  failed_files):
194  """Takes filenames out of queue and runs clang-tidy on them."""
195  while True:
196  name = queue.get()
197  invocation = get_tidy_invocation(name, clang_tidy_binary, args.checks,
198  tmpdir, build_path, args.header_filter,
199  args.allow_enabling_alpha_checkers,
200  args.extra_arg, args.extra_arg_before,
201  args.quiet, args.config_file, args.config,
202  args.line_filter, args.use_color,
203  args.plugins)
204 
205  proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
206  output, err = proc.communicate()
207  if proc.returncode != 0:
208  if proc.returncode < 0:
209  msg = "%s: terminated by signal %d\n" % (name, -proc.returncode)
210  err += msg.encode('utf-8')
211  failed_files.append(name)
212  with lock:
213  sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8'))
214  if len(err) > 0:
215  sys.stdout.flush()
216  sys.stderr.write(err.decode('utf-8'))
217  queue.task_done()
218 
219 
220 def main():
221  parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
222  'in a compilation database. Requires '
223  'clang-tidy and clang-apply-replacements in '
224  '$PATH or in your build directory.')
225  parser.add_argument('-allow-enabling-alpha-checkers',
226  action='store_true', help='allow alpha checkers from '
227  'clang-analyzer.')
228  parser.add_argument('-clang-tidy-binary', metavar='PATH',
229  help='path to clang-tidy binary')
230  parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
231  help='path to clang-apply-replacements binary')
232  parser.add_argument('-checks', default=None,
233  help='checks filter, when not specified, use clang-tidy '
234  'default')
235  config_group = parser.add_mutually_exclusive_group()
236  config_group.add_argument('-config', default=None,
237  help='Specifies a configuration in YAML/JSON format: '
238  ' -config="{Checks: \'*\', '
239  ' CheckOptions: {x: y}}" '
240  'When the value is empty, clang-tidy will '
241  'attempt to find a file named .clang-tidy for '
242  'each source file in its parent directories.')
243  config_group.add_argument('-config-file', default=None,
244  help='Specify the path of .clang-tidy or custom config '
245  'file: e.g. -config-file=/some/path/myTidyConfigFile. '
246  'This option internally works exactly the same way as '
247  '-config option after reading specified config file. '
248  'Use either -config-file or -config, not both.')
249  parser.add_argument('-header-filter', default=None,
250  help='regular expression matching the names of the '
251  'headers to output diagnostics from. Diagnostics from '
252  'the main file of each translation unit are always '
253  'displayed.')
254  parser.add_argument('-line-filter', default=None,
255  help='List of files with line ranges to filter the'
256  'warnings.')
257  if yaml:
258  parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
259  help='Create a yaml file to store suggested fixes in, '
260  'which can be applied with clang-apply-replacements.')
261  parser.add_argument('-j', type=int, default=0,
262  help='number of tidy instances to be run in parallel.')
263  parser.add_argument('files', nargs='*', default=['.*'],
264  help='files to be processed (regex on path)')
265  parser.add_argument('-fix', action='store_true', help='apply fix-its')
266  parser.add_argument('-format', action='store_true', help='Reformat code '
267  'after applying fixes')
268  parser.add_argument('-style', default='file', help='The style of reformat '
269  'code after applying fixes')
270  parser.add_argument('-use-color', type=strtobool, nargs='?', const=True,
271  help='Use colors in diagnostics, overriding clang-tidy\'s'
272  ' default behavior. This option overrides the \'UseColor'
273  '\' option in .clang-tidy file, if any.')
274  parser.add_argument('-p', dest='build_path',
275  help='Path used to read a compile command database.')
276  parser.add_argument('-extra-arg', dest='extra_arg',
277  action='append', default=[],
278  help='Additional argument to append to the compiler '
279  'command line.')
280  parser.add_argument('-extra-arg-before', dest='extra_arg_before',
281  action='append', default=[],
282  help='Additional argument to prepend to the compiler '
283  'command line.')
284  parser.add_argument('-quiet', action='store_true',
285  help='Run clang-tidy in quiet mode')
286  parser.add_argument('-load', dest='plugins',
287  action='append', default=[],
288  help='Load the specified plugin in clang-tidy.')
289  args = parser.parse_args()
290 
291  db_path = 'compile_commands.json'
292 
293  if args.build_path is not None:
294  build_path = args.build_path
295  else:
296  # Find our database
297  build_path = find_compilation_database(db_path)
298 
299  clang_tidy_binary = find_binary(args.clang_tidy_binary, "clang-tidy",
300  build_path)
301 
302  tmpdir = None
303  if args.fix or (yaml and args.export_fixes):
304  clang_apply_replacements_binary = find_binary(
305  args.clang_apply_replacements_binary, "clang-apply-replacements",
306  build_path)
307  tmpdir = tempfile.mkdtemp()
308 
309  try:
310  invocation = get_tidy_invocation("", clang_tidy_binary, args.checks,
311  None, build_path, args.header_filter,
312  args.allow_enabling_alpha_checkers,
313  args.extra_arg, args.extra_arg_before,
314  args.quiet, args.config_file, args.config,
315  args.line_filter, args.use_color,
316  args.plugins)
317  invocation.append('-list-checks')
318  invocation.append('-')
319  if args.quiet:
320  # Even with -quiet we still want to check if we can call clang-tidy.
321  with open(os.devnull, 'w') as dev_null:
322  subprocess.check_call(invocation, stdout=dev_null)
323  else:
324  subprocess.check_call(invocation)
325  except:
326  print("Unable to run clang-tidy.", file=sys.stderr)
327  sys.exit(1)
328 
329  # Load the database and extract all files.
330  database = json.load(open(os.path.join(build_path, db_path)))
331  files = set([make_absolute(entry['file'], entry['directory'])
332  for entry in database])
333 
334  max_task = args.j
335  if max_task == 0:
336  max_task = multiprocessing.cpu_count()
337 
338  # Build up a big regexy filter from all command line arguments.
339  file_name_re = re.compile('|'.join(args.files))
340 
341  return_code = 0
342  try:
343  # Spin up a bunch of tidy-launching threads.
344  task_queue = queue.Queue(max_task)
345  # List of files with a non-zero return code.
346  failed_files = []
347  lock = threading.Lock()
348  for _ in range(max_task):
349  t = threading.Thread(target=run_tidy,
350  args=(args, clang_tidy_binary, tmpdir, build_path,
351  task_queue, lock, failed_files))
352  t.daemon = True
353  t.start()
354 
355  # Fill the queue with files.
356  for name in files:
357  if file_name_re.search(name):
358  task_queue.put(name)
359 
360  # Wait for all threads to be done.
361  task_queue.join()
362  if len(failed_files):
363  return_code = 1
364 
365  except KeyboardInterrupt:
366  # This is a sad hack. Unfortunately subprocess goes
367  # bonkers with ctrl-c and we start forking merrily.
368  print('\nCtrl-C detected, goodbye.')
369  if tmpdir:
370  shutil.rmtree(tmpdir)
371  os.kill(0, 9)
372 
373  if yaml and args.export_fixes:
374  print('Writing fixes to ' + args.export_fixes + ' ...')
375  try:
376  merge_replacement_files(tmpdir, args.export_fixes)
377  except:
378  print('Error exporting fixes.\n', file=sys.stderr)
379  traceback.print_exc()
380  return_code=1
381 
382  if args.fix:
383  print('Applying fixes ...')
384  try:
385  apply_fixes(args, clang_apply_replacements_binary, tmpdir)
386  except:
387  print('Error applying fixes.\n', file=sys.stderr)
388  traceback.print_exc()
389  return_code = 1
390 
391  if tmpdir:
392  shutil.rmtree(tmpdir)
393  sys.exit(return_code)
394 
395 
396 if __name__ == '__main__':
397  main()
run-clang-tidy.find_binary
def find_binary(arg, name, build_path)
Definition: run-clang-tidy.py:160
run-clang-tidy.apply_fixes
def apply_fixes(args, clang_apply_replacements_binary, tmpdir)
Definition: run-clang-tidy.py:180
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:86
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_file_path, config, line_filter, use_color, plugins)
Definition: run-clang-tidy.py:92
run-clang-tidy.find_compilation_database
def find_compilation_database(path)
Definition: run-clang-tidy.py:74
run-clang-tidy.merge_replacement_files
def merge_replacement_files(tmpdir, mergefile)
Definition: run-clang-tidy.py:135
run-clang-tidy.strtobool
def strtobool(val)
Definition: run-clang-tidy.py:59
run-clang-tidy.run_tidy
def run_tidy(args, clang_tidy_binary, tmpdir, build_path, queue, lock, failed_files)
Definition: run-clang-tidy.py:192
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:220