1#!/usr/bin/env python 2# 3#===- run-clang-tidy.py - Parallel clang-tidy runner --------*- python -*--===# 4# 5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 6# See https://llvm.org/LICENSE.txt for license information. 7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 8# 9#===-----------------------------------------------------------------------===# 10# FIXME: Integrate with clang-tidy-diff.py 11 12 13""" 14Parallel clang-tidy runner 15========================== 16 17Runs clang-tidy over all files in a compilation database. Requires clang-tidy 18and clang-apply-replacements in $PATH. 19 20Example 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 33Compilation database setup: 34http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html 35""" 36 37from __future__ import print_function 38 39import argparse 40import glob 41import json 42import multiprocessing 43import os 44import re 45import shutil 46import subprocess 47import sys 48import tempfile 49import threading 50import traceback 51 52try: 53 import yaml 54except ImportError: 55 yaml = None 56 57is_py2 = sys.version[0] == '2' 58 59if is_py2: 60 import Queue as queue 61else: 62 import queue as queue 63 64 65def find_compilation_database(path): 66 """Adjusts the directory until a compilation database is found.""" 67 result = './' 68 while not os.path.isfile(os.path.join(result, path)): 69 if os.path.realpath(result) == '/': 70 print('Error: could not find compilation database.') 71 sys.exit(1) 72 result += '../' 73 return os.path.realpath(result) 74 75 76def make_absolute(f, directory): 77 if os.path.isabs(f): 78 return f 79 return os.path.normpath(os.path.join(directory, f)) 80 81 82def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, 83 header_filter, allow_enabling_alpha_checkers, 84 extra_arg, extra_arg_before, quiet, config): 85 """Gets a command line for clang-tidy.""" 86 start = [clang_tidy_binary, '--use-color'] 87 if allow_enabling_alpha_checkers: 88 start.append('-allow-enabling-analyzer-alpha-checkers') 89 if header_filter is not None: 90 start.append('-header-filter=' + header_filter) 91 if checks: 92 start.append('-checks=' + checks) 93 if tmpdir is not None: 94 start.append('-export-fixes') 95 # Get a temporary file. We immediately close the handle so clang-tidy can 96 # overwrite it. 97 (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) 98 os.close(handle) 99 start.append(name) 100 for arg in extra_arg: 101 start.append('-extra-arg=%s' % arg) 102 for arg in extra_arg_before: 103 start.append('-extra-arg-before=%s' % arg) 104 start.append('-p=' + build_path) 105 if quiet: 106 start.append('-quiet') 107 if config: 108 start.append('-config=' + config) 109 start.append(f) 110 return start 111 112 113def merge_replacement_files(tmpdir, mergefile): 114 """Merge all replacement files in a directory into a single file""" 115 # The fixes suggested by clang-tidy >= 4.0.0 are given under 116 # the top level key 'Diagnostics' in the output yaml files 117 mergekey = "Diagnostics" 118 merged=[] 119 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): 120 content = yaml.safe_load(open(replacefile, 'r')) 121 if not content: 122 continue # Skip empty files. 123 merged.extend(content.get(mergekey, [])) 124 125 if merged: 126 # MainSourceFile: The key is required by the definition inside 127 # include/clang/Tooling/ReplacementsYaml.h, but the value 128 # is actually never used inside clang-apply-replacements, 129 # so we set it to '' here. 130 output = {'MainSourceFile': '', mergekey: merged} 131 with open(mergefile, 'w') as out: 132 yaml.safe_dump(output, out) 133 else: 134 # Empty the file: 135 open(mergefile, 'w').close() 136 137 138def check_clang_apply_replacements_binary(args): 139 """Checks if invoking supplied clang-apply-replacements binary works.""" 140 try: 141 subprocess.check_call([args.clang_apply_replacements_binary, '--version']) 142 except: 143 print('Unable to run clang-apply-replacements. Is clang-apply-replacements ' 144 'binary correctly specified?', file=sys.stderr) 145 traceback.print_exc() 146 sys.exit(1) 147 148 149def apply_fixes(args, tmpdir): 150 """Calls clang-apply-fixes on a given directory.""" 151 invocation = [args.clang_apply_replacements_binary] 152 if args.format: 153 invocation.append('-format') 154 if args.style: 155 invocation.append('-style=' + args.style) 156 invocation.append(tmpdir) 157 subprocess.call(invocation) 158 159 160def run_tidy(args, tmpdir, build_path, queue, lock, failed_files): 161 """Takes filenames out of queue and runs clang-tidy on them.""" 162 while True: 163 name = queue.get() 164 invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks, 165 tmpdir, build_path, args.header_filter, 166 args.allow_enabling_alpha_checkers, 167 args.extra_arg, args.extra_arg_before, 168 args.quiet, args.config) 169 170 proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 171 output, err = proc.communicate() 172 if proc.returncode != 0: 173 failed_files.append(name) 174 with lock: 175 sys.stdout.write(' '.join(invocation) + '\n' + output.decode('utf-8')) 176 if len(err) > 0: 177 sys.stdout.flush() 178 sys.stderr.write(err.decode('utf-8')) 179 queue.task_done() 180 181 182def main(): 183 parser = argparse.ArgumentParser(description='Runs clang-tidy over all files ' 184 'in a compilation database. Requires ' 185 'clang-tidy and clang-apply-replacements in ' 186 '$PATH.') 187 parser.add_argument('-allow-enabling-alpha-checkers', 188 action='store_true', help='allow alpha checkers from ' 189 'clang-analyzer.') 190 parser.add_argument('-clang-tidy-binary', metavar='PATH', 191 default='clang-tidy', 192 help='path to clang-tidy binary') 193 parser.add_argument('-clang-apply-replacements-binary', metavar='PATH', 194 default='clang-apply-replacements', 195 help='path to clang-apply-replacements binary') 196 parser.add_argument('-checks', default=None, 197 help='checks filter, when not specified, use clang-tidy ' 198 'default') 199 parser.add_argument('-config', default=None, 200 help='Specifies a configuration in YAML/JSON format: ' 201 ' -config="{Checks: \'*\', ' 202 ' CheckOptions: [{key: x, ' 203 ' value: y}]}" ' 204 'When the value is empty, clang-tidy will ' 205 'attempt to find a file named .clang-tidy for ' 206 'each source file in its parent directories.') 207 parser.add_argument('-header-filter', default=None, 208 help='regular expression matching the names of the ' 209 'headers to output diagnostics from. Diagnostics from ' 210 'the main file of each translation unit are always ' 211 'displayed.') 212 if yaml: 213 parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes', 214 help='Create a yaml file to store suggested fixes in, ' 215 'which can be applied with clang-apply-replacements.') 216 parser.add_argument('-j', type=int, default=0, 217 help='number of tidy instances to be run in parallel.') 218 parser.add_argument('files', nargs='*', default=['.*'], 219 help='files to be processed (regex on path)') 220 parser.add_argument('-fix', action='store_true', help='apply fix-its') 221 parser.add_argument('-format', action='store_true', help='Reformat code ' 222 'after applying fixes') 223 parser.add_argument('-style', default='file', help='The style of reformat ' 224 'code after applying fixes') 225 parser.add_argument('-p', dest='build_path', 226 help='Path used to read a compile command database.') 227 parser.add_argument('-extra-arg', dest='extra_arg', 228 action='append', default=[], 229 help='Additional argument to append to the compiler ' 230 'command line.') 231 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 232 action='append', default=[], 233 help='Additional argument to prepend to the compiler ' 234 'command line.') 235 parser.add_argument('-quiet', action='store_true', 236 help='Run clang-tidy in quiet mode') 237 args = parser.parse_args() 238 239 db_path = 'compile_commands.json' 240 241 if args.build_path is not None: 242 build_path = args.build_path 243 else: 244 # Find our database 245 build_path = find_compilation_database(db_path) 246 247 try: 248 invocation = [args.clang_tidy_binary, '-list-checks'] 249 if args.allow_enabling_alpha_checkers: 250 invocation.append('-allow-enabling-analyzer-alpha-checkers') 251 invocation.append('-p=' + build_path) 252 if args.checks: 253 invocation.append('-checks=' + args.checks) 254 invocation.append('-') 255 if args.quiet: 256 # Even with -quiet we still want to check if we can call clang-tidy. 257 with open(os.devnull, 'w') as dev_null: 258 subprocess.check_call(invocation, stdout=dev_null) 259 else: 260 subprocess.check_call(invocation) 261 except: 262 print("Unable to run clang-tidy.", file=sys.stderr) 263 sys.exit(1) 264 265 # Load the database and extract all files. 266 database = json.load(open(os.path.join(build_path, db_path))) 267 files = [make_absolute(entry['file'], entry['directory']) 268 for entry in database] 269 270 max_task = args.j 271 if max_task == 0: 272 max_task = multiprocessing.cpu_count() 273 274 tmpdir = None 275 if args.fix or (yaml and args.export_fixes): 276 check_clang_apply_replacements_binary(args) 277 tmpdir = tempfile.mkdtemp() 278 279 # Build up a big regexy filter from all command line arguments. 280 file_name_re = re.compile('|'.join(args.files)) 281 282 return_code = 0 283 try: 284 # Spin up a bunch of tidy-launching threads. 285 task_queue = queue.Queue(max_task) 286 # List of files with a non-zero return code. 287 failed_files = [] 288 lock = threading.Lock() 289 for _ in range(max_task): 290 t = threading.Thread(target=run_tidy, 291 args=(args, tmpdir, build_path, task_queue, lock, failed_files)) 292 t.daemon = True 293 t.start() 294 295 # Fill the queue with files. 296 for name in files: 297 if file_name_re.search(name): 298 task_queue.put(name) 299 300 # Wait for all threads to be done. 301 task_queue.join() 302 if len(failed_files): 303 return_code = 1 304 305 except KeyboardInterrupt: 306 # This is a sad hack. Unfortunately subprocess goes 307 # bonkers with ctrl-c and we start forking merrily. 308 print('\nCtrl-C detected, goodbye.') 309 if tmpdir: 310 shutil.rmtree(tmpdir) 311 os.kill(0, 9) 312 313 if yaml and args.export_fixes: 314 print('Writing fixes to ' + args.export_fixes + ' ...') 315 try: 316 merge_replacement_files(tmpdir, args.export_fixes) 317 except: 318 print('Error exporting fixes.\n', file=sys.stderr) 319 traceback.print_exc() 320 return_code=1 321 322 if args.fix: 323 print('Applying fixes ...') 324 try: 325 apply_fixes(args, tmpdir) 326 except: 327 print('Error applying fixes.\n', file=sys.stderr) 328 traceback.print_exc() 329 return_code = 1 330 331 if tmpdir: 332 shutil.rmtree(tmpdir) 333 sys.exit(return_code) 334 335 336if __name__ == '__main__': 337 main() 338