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