File indexing completed on 2024-04-21 14:47:31

0001 #!/usr/bin/env python
0002 #
0003 #===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===#
0004 #
0005 #                     The LLVM Compiler Infrastructure
0006 #
0007 # This file is distributed under the University of Illinois Open Source
0008 # License. See LICENSE.TXT for details.
0009 #
0010 #===------------------------------------------------------------------------===#
0011 # FIXME: Integrate with clang-tidy-diff.py
0012 
0013 """
0014 Parallel clang-tidy runner
0015 ==========================
0016 
0017 Runs clang-tidy over all files in a compilation database. Requires clang-tidy
0018 and clang-apply-replacements in $PATH.
0019 
0020 Example invocations.
0021 - Run clang-tidy on all files in the current working directory with a default
0022   set of checks and show warnings in the cpp files and all project headers.
0023     run-clang-tidy.py $PWD
0024 
0025 - Fix all header guards.
0026     run-clang-tidy.py -fix -checks=-*,llvm-header-guard
0027 
0028 - Fix all header guards included from clang-tidy and header guards
0029   for clang-tidy headers.
0030     run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \
0031                       -header-filter=extra/clang-tidy
0032 
0033 Compilation database setup:
0034 https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html
0035 """
0036 
0037 from __future__ import print_function
0038 
0039 import argparse
0040 import glob
0041 import json
0042 import multiprocessing
0043 import os
0044 import re
0045 import shutil
0046 import subprocess
0047 import sys
0048 import tempfile
0049 import threading
0050 import traceback
0051 import yaml
0052 
0053 is_py2 = sys.version[0] == '2'
0054 
0055 if is_py2:
0056     import Queue as queue
0057 else:
0058     import queue as queue
0059 
0060 def find_compilation_database(path):
0061   """Adjusts the directory until a compilation database is found."""
0062   result = './'
0063   while not os.path.isfile(os.path.join(result, path)):
0064     if os.path.realpath(result) == '/':
0065       print('Error: could not find compilation database.')
0066       sys.exit(1)
0067     result += '../'
0068   return os.path.realpath(result)
0069 
0070 
0071 def make_absolute(f, directory):
0072   if os.path.isabs(f):
0073     return f
0074   return os.path.normpath(os.path.join(directory, f))
0075 
0076 
0077 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
0078                         header_filter, extra_arg, extra_arg_before, quiet):
0079   """Gets a command line for clang-tidy."""
0080   start = [clang_tidy_binary]
0081   if header_filter is not None:
0082     start.append('-header-filter=' + header_filter)
0083   else:
0084     # Show warnings in all in-project headers by default.
0085     start.append('-header-filter=^' + build_path + '/.*')
0086   if checks:
0087     start.append('-checks=' + checks)
0088   if tmpdir is not None:
0089     start.append('-export-fixes')
0090     # Get a temporary file. We immediately close the handle so clang-tidy can
0091     # overwrite it.
0092     (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
0093     os.close(handle)
0094     start.append(name)
0095   for arg in extra_arg:
0096       start.append('-extra-arg=%s' % arg)
0097   for arg in extra_arg_before:
0098       start.append('-extra-arg-before=%s' % arg)
0099   start.append('-p=' + build_path)
0100   if quiet:
0101       start.append('-quiet')
0102   start.append(f)
0103   return start
0104 
0105 
0106 def merge_replacement_files(tmpdir, mergefile):
0107   """Merge all replacement files in a directory into a single file"""
0108   # The fixes suggested by clang-tidy >= 4.0.0 are given under
0109   # the top level key 'Diagnostics' in the output yaml files
0110   mergekey="Diagnostics"
0111   merged=[]
0112   for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')):
0113     content = yaml.safe_load(open(replacefile, 'r'))
0114     if not content:
0115       continue # Skip empty files.
0116     merged.extend(content.get(mergekey, []))
0117 
0118   if merged:
0119     # MainSourceFile: The key is required by the definition inside
0120     # include/clang/Tooling/ReplacementsYaml.h, but the value
0121     # is actually never used inside clang-apply-replacements,
0122     # so we set it to '' here.
0123     output = { 'MainSourceFile': '', mergekey: merged }
0124     with open(mergefile, 'w') as out:
0125       yaml.safe_dump(output, out)
0126   else:
0127     # Empty the file:
0128     open(mergefile, 'w').close()
0129 
0130 
0131 def check_clang_apply_replacements_binary(args):
0132   """Checks if invoking supplied clang-apply-replacements binary works."""
0133   try:
0134     subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
0135   except:
0136     print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
0137           'binary correctly specified?', file=sys.stderr)
0138     traceback.print_exc()
0139     sys.exit(1)
0140 
0141 
0142 def apply_fixes(args, tmpdir):
0143   """Calls clang-apply-fixes on a given directory."""
0144   invocation = [args.clang_apply_replacements_binary]
0145   if args.format:
0146     invocation.append('-format')
0147   if args.style:
0148     invocation.append('-style=' + args.style)
0149   invocation.append(tmpdir)
0150   subprocess.call(invocation)
0151 
0152 
0153 def run_tidy(args, tmpdir, build_path, queue):
0154   """Takes filenames out of queue and runs clang-tidy on them."""
0155   while True:
0156     name = queue.get()
0157     invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
0158                                      tmpdir, build_path, args.header_filter,
0159                                      args.extra_arg, args.extra_arg_before,
0160                                      args.quiet)
0161     sys.stdout.write(' '.join(invocation) + '\n')
0162     subprocess.call(invocation)
0163     queue.task_done()
0164 
0165 
0166 def main():
0167   parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
0168                                    'in a compilation database. Requires '
0169                                    'clang-tidy and clang-apply-replacements in '
0170                                    '$PATH.')
0171   parser.add_argument('-clang-tidy-binary', metavar='PATH',
0172                       default='clang-tidy',
0173                       help='path to clang-tidy binary')
0174   parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
0175                       default='clang-apply-replacements',
0176                       help='path to clang-apply-replacements binary')
0177   parser.add_argument('-checks', default=None,
0178                       help='checks filter, when not specified, use clang-tidy '
0179                       'default')
0180   parser.add_argument('-header-filter', default=None,
0181                       help='regular expression matching the names of the '
0182                       'headers to output diagnostics from. Diagnostics from '
0183                       'the main file of each translation unit are always '
0184                       'displayed.')
0185   parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes',
0186                       help='Create a yaml file to store suggested fixes in, '
0187                       'which can be applied with clang-apply-replacements.')
0188   parser.add_argument('-j', type=int, default=0,
0189                       help='number of tidy instances to be run in parallel.')
0190   parser.add_argument('files', nargs='*', default=['.*'],
0191                       help='files to be processed (regex on path)')
0192   parser.add_argument('-fix', action='store_true', help='apply fix-its')
0193   parser.add_argument('-format', action='store_true', help='Reformat code '
0194                       'after applying fixes')
0195   parser.add_argument('-style', default='file', help='The style of reformat '
0196                       'code after applying fixes')
0197   parser.add_argument('-p', dest='build_path',
0198                       help='Path used to read a compile command database.')
0199   parser.add_argument('-extra-arg', dest='extra_arg',
0200                       action='append', default=[],
0201                       help='Additional argument to append to the compiler '
0202                       'command line.')
0203   parser.add_argument('-extra-arg-before', dest='extra_arg_before',
0204                       action='append', default=[],
0205                       help='Additional argument to prepend to the compiler '
0206                       'command line.')
0207   parser.add_argument('-quiet', action='store_true',
0208                       help='Run clang-tidy in quiet mode')
0209   args = parser.parse_args()
0210 
0211   db_path = 'compile_commands.json'
0212 
0213   if args.build_path is not None:
0214     build_path = args.build_path
0215   else:
0216     # Find our database
0217     build_path = find_compilation_database(db_path)
0218 
0219   try:
0220     invocation = [args.clang_tidy_binary, '-list-checks']
0221     invocation.append('-p=' + build_path)
0222     if args.checks:
0223       invocation.append('-checks=' + args.checks)
0224     invocation.append('-')
0225     print(subprocess.check_output(invocation))
0226   except:
0227     print("Unable to run clang-tidy.", file=sys.stderr)
0228     sys.exit(1)
0229 
0230   # Load the database and extract all files.
0231   database = json.load(open(os.path.join(build_path, db_path)))
0232   files = [make_absolute(entry['file'], entry['directory'])
0233            for entry in database]
0234 
0235   max_task = args.j
0236   if max_task == 0:
0237     max_task = multiprocessing.cpu_count()
0238 
0239   tmpdir = None
0240   if args.fix or args.export_fixes:
0241     check_clang_apply_replacements_binary(args)
0242     tmpdir = tempfile.mkdtemp()
0243 
0244   # Build up a big regexy filter from all command line arguments.
0245   file_name_re = re.compile('|'.join(args.files))
0246 
0247   try:
0248     # Spin up a bunch of tidy-launching threads.
0249     task_queue = queue.Queue(max_task)
0250     for _ in range(max_task):
0251       t = threading.Thread(target=run_tidy,
0252                            args=(args, tmpdir, build_path, task_queue))
0253       t.daemon = True
0254       t.start()
0255 
0256     # Fill the queue with files.
0257     for name in files:
0258       if file_name_re.search(name):
0259         task_queue.put(name)
0260 
0261     # Wait for all threads to be done.
0262     task_queue.join()
0263 
0264   except KeyboardInterrupt:
0265     # This is a sad hack. Unfortunately subprocess goes
0266     # bonkers with ctrl-c and we start forking merrily.
0267     print('\nCtrl-C detected, goodbye.')
0268     if tmpdir:
0269       shutil.rmtree(tmpdir)
0270     os.kill(0, 9)
0271 
0272   return_code = 0
0273   if args.export_fixes:
0274     print('Writing fixes to ' + args.export_fixes + ' ...')
0275     try:
0276       merge_replacement_files(tmpdir, args.export_fixes)
0277     except:
0278       print('Error exporting fixes.\n', file=sys.stderr)
0279       traceback.print_exc()
0280       return_code=1
0281 
0282   if args.fix:
0283     print('Applying fixes ...')
0284     try:
0285       apply_fixes(args, tmpdir)
0286     except:
0287       print('Error applying fixes.\n', file=sys.stderr)
0288       traceback.print_exc()
0289       return_code=1
0290 
0291   if tmpdir:
0292     shutil.rmtree(tmpdir)
0293   sys.exit(return_code)
0294 
0295 if __name__ == '__main__':
0296   main()
0297