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