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