Home | History | Annotate | Download | only in clang
      1 #!/usr/bin/env python
      2 #
      3 #===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===#
      4 #
      5 #                     The LLVM Compiler Infrastructure
      6 #
      7 # This file is distributed under the University of Illinois Open Source
      8 # License. See LICENSE.TXT for details.
      9 #
     10 #===------------------------------------------------------------------------===#
     11 # FIXME: Integrate with clang-tidy-diff.py
     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 import argparse
     39 import json
     40 import multiprocessing
     41 import os
     42 import Queue
     43 import re
     44 import shutil
     45 import subprocess
     46 import sys
     47 import tempfile
     48 import threading
     49 import traceback
     50 
     51 
     52 def find_compilation_database(path):
     53   """Adjusts the directory until a compilation database is found."""
     54   result = './'
     55   while not os.path.isfile(os.path.join(result, path)):
     56     if os.path.realpath(result) == '/':
     57       print('Error: could not find compilation database.')
     58       sys.exit(1)
     59     result += '../'
     60   return os.path.realpath(result)
     61 
     62 
     63 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path,
     64                         header_filter, extra_arg, extra_arg_before, quiet):
     65   """Gets a command line for clang-tidy."""
     66   start = [clang_tidy_binary]
     67   if header_filter is not None:
     68     start.append('-header-filter=' + header_filter)
     69   else:
     70     # Show warnings in all in-project headers by default.
     71     start.append('-header-filter=^' + build_path + '/.*')
     72   if checks:
     73     start.append('-checks=' + checks)
     74   if tmpdir is not None:
     75     start.append('-export-fixes')
     76     # Get a temporary file. We immediately close the handle so clang-tidy can
     77     # overwrite it.
     78     (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
     79     os.close(handle)
     80     start.append(name)
     81   for arg in extra_arg:
     82       start.append('-extra-arg=%s' % arg)
     83   for arg in extra_arg_before:
     84       start.append('-extra-arg-before=%s' % arg)
     85   start.append('-p=' + build_path)
     86   if quiet:
     87       start.append('-quiet')
     88   start.append(f)
     89   return start
     90 
     91 
     92 def check_clang_apply_replacements_binary(args):
     93   """Checks if invoking supplied clang-apply-replacements binary works."""
     94   try:
     95     subprocess.check_call([args.clang_apply_replacements_binary, '--version'])
     96   except:
     97     print('Unable to run clang-apply-replacements. Is clang-apply-replacements '
     98           'binary correctly specified?', file=sys.stderr)
     99     traceback.print_exc()
    100     sys.exit(1)
    101 
    102 
    103 def apply_fixes(args, tmpdir):
    104   """Calls clang-apply-fixes on a given directory. Deletes the dir when done."""
    105   invocation = [args.clang_apply_replacements_binary]
    106   if args.format:
    107     invocation.append('-format')
    108   if args.style:
    109     invocation.append('-style=' + args.style)
    110   invocation.append(tmpdir)
    111   subprocess.call(invocation)
    112 
    113 
    114 def run_tidy(args, tmpdir, build_path, queue):
    115   """Takes filenames out of queue and runs clang-tidy on them."""
    116   while True:
    117     name = queue.get()
    118     invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks,
    119                                      tmpdir, build_path, args.header_filter,
    120                                      args.extra_arg, args.extra_arg_before,
    121                                      args.quiet)
    122     sys.stdout.write(' '.join(invocation) + '\n')
    123     subprocess.call(invocation)
    124     queue.task_done()
    125 
    126 
    127 def main():
    128   parser = argparse.ArgumentParser(description='Runs clang-tidy over all files '
    129                                    'in a compilation database. Requires '
    130                                    'clang-tidy and clang-apply-replacements in '
    131                                    '$PATH.')
    132   parser.add_argument('-clang-tidy-binary', metavar='PATH',
    133                       default='clang-tidy',
    134                       help='path to clang-tidy binary')
    135   parser.add_argument('-clang-apply-replacements-binary', metavar='PATH',
    136                       default='clang-apply-replacements',
    137                       help='path to clang-apply-replacements binary')
    138   parser.add_argument('-checks', default=None,
    139                       help='checks filter, when not specified, use clang-tidy '
    140                       'default')
    141   parser.add_argument('-header-filter', default=None,
    142                       help='regular expression matching the names of the '
    143                       'headers to output diagnostics from. Diagnostics from '
    144                       'the main file of each translation unit are always '
    145                       'displayed.')
    146   parser.add_argument('-j', type=int, default=0,
    147                       help='number of tidy instances to be run in parallel.')
    148   parser.add_argument('files', nargs='*', default=['.*'],
    149                       help='files to be processed (regex on path)')
    150   parser.add_argument('-fix', action='store_true', help='apply fix-its')
    151   parser.add_argument('-format', action='store_true', help='Reformat code '
    152                       'after applying fixes')
    153   parser.add_argument('-style', default='file', help='The style of reformat '
    154                       'code after applying fixes')
    155   parser.add_argument('-p', dest='build_path',
    156                       help='Path used to read a compile command database.')
    157   parser.add_argument('-extra-arg', dest='extra_arg',
    158                       action='append', default=[],
    159                       help='Additional argument to append to the compiler '
    160                       'command line.')
    161   parser.add_argument('-extra-arg-before', dest='extra_arg_before',
    162                       action='append', default=[],
    163                       help='Additional argument to prepend to the compiler '
    164                       'command line.')
    165   parser.add_argument('-quiet', action='store_true',
    166                       help='Run clang-tidy in quiet mode')
    167   args = parser.parse_args()
    168 
    169   db_path = 'compile_commands.json'
    170 
    171   if args.build_path is not None:
    172     build_path = args.build_path
    173   else:
    174     # Find our database
    175     build_path = find_compilation_database(db_path)
    176 
    177   try:
    178     invocation = [args.clang_tidy_binary, '-list-checks']
    179     invocation.append('-p=' + build_path)
    180     if args.checks:
    181       invocation.append('-checks=' + args.checks)
    182     invocation.append('-')
    183     print(subprocess.check_output(invocation))
    184   except:
    185     print("Unable to run clang-tidy.", file=sys.stderr)
    186     sys.exit(1)
    187 
    188   # Load the database and extract all files.
    189   database = json.load(open(os.path.join(build_path, db_path)))
    190   files = [entry['file'] for entry in database]
    191 
    192   max_task = args.j
    193   if max_task == 0:
    194     max_task = multiprocessing.cpu_count()
    195 
    196   tmpdir = None
    197   if args.fix:
    198     check_clang_apply_replacements_binary(args)
    199     tmpdir = tempfile.mkdtemp()
    200 
    201   # Build up a big regexy filter from all command line arguments.
    202   file_name_re = re.compile('|'.join(args.files))
    203 
    204   try:
    205     # Spin up a bunch of tidy-launching threads.
    206     queue = Queue.Queue(max_task)
    207     for _ in range(max_task):
    208       t = threading.Thread(target=run_tidy,
    209                            args=(args, tmpdir, build_path, queue))
    210       t.daemon = True
    211       t.start()
    212 
    213     # Fill the queue with files.
    214     for name in files:
    215       if file_name_re.search(name):
    216         queue.put(name)
    217 
    218     # Wait for all threads to be done.
    219     queue.join()
    220 
    221   except KeyboardInterrupt:
    222     # This is a sad hack. Unfortunately subprocess goes
    223     # bonkers with ctrl-c and we start forking merrily.
    224     print('\nCtrl-C detected, goodbye.')
    225     if args.fix:
    226       shutil.rmtree(tmpdir)
    227     os.kill(0, 9)
    228 
    229   if args.fix:
    230     print('Applying fixes ...')
    231     successfully_applied = False
    232 
    233     try:
    234       apply_fixes(args, tmpdir)
    235       successfully_applied = True
    236     except:
    237       print('Error applying fixes.\n', file=sys.stderr)
    238       traceback.print_exc()
    239 
    240     shutil.rmtree(tmpdir)
    241     if not successfully_applied:
    242       sys.exit(1)
    243 
    244 if __name__ == '__main__':
    245   main()
    246