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