File indexing completed on 2024-04-28 11:26:08
0001 #!/usr/bin/env python 0002 # 0003 #===- run-clang-tidy.py - Parallel clang-tidy runner ---------*- python -*--===# 0004 # 0005 # The LLVM Compiler Infrastructure 0006 # 0007 # This file is distributed under the University of Illinois Open Source 0008 # License. See LICENSE.TXT for details. 0009 # 0010 #===------------------------------------------------------------------------===# 0011 # FIXME: Integrate with clang-tidy-diff.py 0012 0013 """ 0014 Parallel clang-tidy runner 0015 ========================== 0016 0017 Runs clang-tidy over all files in a compilation database. Requires clang-tidy 0018 and clang-apply-replacements in $PATH. 0019 0020 Example invocations. 0021 - Run clang-tidy on all files in the current working directory with a default 0022 set of checks and show warnings in the cpp files and all project headers. 0023 run-clang-tidy.py $PWD 0024 0025 - Fix all header guards. 0026 run-clang-tidy.py -fix -checks=-*,llvm-header-guard 0027 0028 - Fix all header guards included from clang-tidy and header guards 0029 for clang-tidy headers. 0030 run-clang-tidy.py -fix -checks=-*,llvm-header-guard extra/clang-tidy \ 0031 -header-filter=extra/clang-tidy 0032 0033 Compilation database setup: 0034 https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html 0035 """ 0036 0037 from __future__ import print_function 0038 0039 import argparse 0040 import glob 0041 import json 0042 import multiprocessing 0043 import os 0044 import re 0045 import shutil 0046 import subprocess 0047 import sys 0048 import tempfile 0049 import threading 0050 import traceback 0051 import yaml 0052 0053 is_py2 = sys.version[0] == '2' 0054 0055 if is_py2: 0056 import Queue as queue 0057 else: 0058 import queue as queue 0059 0060 def find_compilation_database(path): 0061 """Adjusts the directory until a compilation database is found.""" 0062 result = './' 0063 while not os.path.isfile(os.path.join(result, path)): 0064 if os.path.realpath(result) == '/': 0065 print('Error: could not find compilation database.') 0066 sys.exit(1) 0067 result += '../' 0068 return os.path.realpath(result) 0069 0070 0071 def make_absolute(f, directory): 0072 if os.path.isabs(f): 0073 return f 0074 return os.path.normpath(os.path.join(directory, f)) 0075 0076 0077 def get_tidy_invocation(f, clang_tidy_binary, checks, tmpdir, build_path, 0078 header_filter, extra_arg, extra_arg_before, quiet): 0079 """Gets a command line for clang-tidy.""" 0080 start = [clang_tidy_binary] 0081 if header_filter is not None: 0082 start.append('-header-filter=' + header_filter) 0083 else: 0084 # Show warnings in all in-project headers by default. 0085 start.append('-header-filter=^' + build_path + '/.*') 0086 if checks: 0087 start.append('-checks=' + checks) 0088 if tmpdir is not None: 0089 start.append('-export-fixes') 0090 # Get a temporary file. We immediately close the handle so clang-tidy can 0091 # overwrite it. 0092 (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) 0093 os.close(handle) 0094 start.append(name) 0095 for arg in extra_arg: 0096 start.append('-extra-arg=%s' % arg) 0097 for arg in extra_arg_before: 0098 start.append('-extra-arg-before=%s' % arg) 0099 start.append('-p=' + build_path) 0100 if quiet: 0101 start.append('-quiet') 0102 start.append(f) 0103 return start 0104 0105 0106 def merge_replacement_files(tmpdir, mergefile): 0107 """Merge all replacement files in a directory into a single file""" 0108 # The fixes suggested by clang-tidy >= 4.0.0 are given under 0109 # the top level key 'Diagnostics' in the output yaml files 0110 mergekey="Diagnostics" 0111 merged=[] 0112 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): 0113 content = yaml.safe_load(open(replacefile, 'r')) 0114 if not content: 0115 continue # Skip empty files. 0116 merged.extend(content.get(mergekey, [])) 0117 0118 if merged: 0119 # MainSourceFile: The key is required by the definition inside 0120 # include/clang/Tooling/ReplacementsYaml.h, but the value 0121 # is actually never used inside clang-apply-replacements, 0122 # so we set it to '' here. 0123 output = { 'MainSourceFile': '', mergekey: merged } 0124 with open(mergefile, 'w') as out: 0125 yaml.safe_dump(output, out) 0126 else: 0127 # Empty the file: 0128 open(mergefile, 'w').close() 0129 0130 0131 def check_clang_apply_replacements_binary(args): 0132 """Checks if invoking supplied clang-apply-replacements binary works.""" 0133 try: 0134 subprocess.check_call([args.clang_apply_replacements_binary, '--version']) 0135 except: 0136 print('Unable to run clang-apply-replacements. Is clang-apply-replacements ' 0137 'binary correctly specified?', file=sys.stderr) 0138 traceback.print_exc() 0139 sys.exit(1) 0140 0141 0142 def apply_fixes(args, tmpdir): 0143 """Calls clang-apply-fixes on a given directory.""" 0144 invocation = [args.clang_apply_replacements_binary] 0145 if args.format: 0146 invocation.append('-format') 0147 if args.style: 0148 invocation.append('-style=' + args.style) 0149 invocation.append(tmpdir) 0150 subprocess.call(invocation) 0151 0152 0153 def run_tidy(args, tmpdir, build_path, queue): 0154 """Takes filenames out of queue and runs clang-tidy on them.""" 0155 while True: 0156 name = queue.get() 0157 invocation = get_tidy_invocation(name, args.clang_tidy_binary, args.checks, 0158 tmpdir, build_path, args.header_filter, 0159 args.extra_arg, args.extra_arg_before, 0160 args.quiet) 0161 sys.stdout.write(' '.join(invocation) + '\n') 0162 subprocess.call(invocation) 0163 queue.task_done() 0164 0165 0166 def main(): 0167 parser = argparse.ArgumentParser(description='Runs clang-tidy over all files ' 0168 'in a compilation database. Requires ' 0169 'clang-tidy and clang-apply-replacements in ' 0170 '$PATH.') 0171 parser.add_argument('-clang-tidy-binary', metavar='PATH', 0172 default='clang-tidy', 0173 help='path to clang-tidy binary') 0174 parser.add_argument('-clang-apply-replacements-binary', metavar='PATH', 0175 default='clang-apply-replacements', 0176 help='path to clang-apply-replacements binary') 0177 parser.add_argument('-checks', default=None, 0178 help='checks filter, when not specified, use clang-tidy ' 0179 'default') 0180 parser.add_argument('-header-filter', default=None, 0181 help='regular expression matching the names of the ' 0182 'headers to output diagnostics from. Diagnostics from ' 0183 'the main file of each translation unit are always ' 0184 'displayed.') 0185 parser.add_argument('-export-fixes', metavar='filename', dest='export_fixes', 0186 help='Create a yaml file to store suggested fixes in, ' 0187 'which can be applied with clang-apply-replacements.') 0188 parser.add_argument('-j', type=int, default=0, 0189 help='number of tidy instances to be run in parallel.') 0190 parser.add_argument('files', nargs='*', default=['.*'], 0191 help='files to be processed (regex on path)') 0192 parser.add_argument('-fix', action='store_true', help='apply fix-its') 0193 parser.add_argument('-format', action='store_true', help='Reformat code ' 0194 'after applying fixes') 0195 parser.add_argument('-style', default='file', help='The style of reformat ' 0196 'code after applying fixes') 0197 parser.add_argument('-p', dest='build_path', 0198 help='Path used to read a compile command database.') 0199 parser.add_argument('-extra-arg', dest='extra_arg', 0200 action='append', default=[], 0201 help='Additional argument to append to the compiler ' 0202 'command line.') 0203 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 0204 action='append', default=[], 0205 help='Additional argument to prepend to the compiler ' 0206 'command line.') 0207 parser.add_argument('-quiet', action='store_true', 0208 help='Run clang-tidy in quiet mode') 0209 args = parser.parse_args() 0210 0211 db_path = 'compile_commands.json' 0212 0213 if args.build_path is not None: 0214 build_path = args.build_path 0215 else: 0216 # Find our database 0217 build_path = find_compilation_database(db_path) 0218 0219 try: 0220 invocation = [args.clang_tidy_binary, '-list-checks'] 0221 invocation.append('-p=' + build_path) 0222 if args.checks: 0223 invocation.append('-checks=' + args.checks) 0224 invocation.append('-') 0225 print(subprocess.check_output(invocation)) 0226 except: 0227 print("Unable to run clang-tidy.", file=sys.stderr) 0228 sys.exit(1) 0229 0230 # Load the database and extract all files. 0231 database = json.load(open(os.path.join(build_path, db_path))) 0232 files = [make_absolute(entry['file'], entry['directory']) 0233 for entry in database] 0234 0235 max_task = args.j 0236 if max_task == 0: 0237 max_task = multiprocessing.cpu_count() 0238 0239 tmpdir = None 0240 if args.fix or args.export_fixes: 0241 check_clang_apply_replacements_binary(args) 0242 tmpdir = tempfile.mkdtemp() 0243 0244 # Build up a big regexy filter from all command line arguments. 0245 file_name_re = re.compile('|'.join(args.files)) 0246 0247 try: 0248 # Spin up a bunch of tidy-launching threads. 0249 task_queue = queue.Queue(max_task) 0250 for _ in range(max_task): 0251 t = threading.Thread(target=run_tidy, 0252 args=(args, tmpdir, build_path, task_queue)) 0253 t.daemon = True 0254 t.start() 0255 0256 # Fill the queue with files. 0257 for name in files: 0258 if file_name_re.search(name): 0259 task_queue.put(name) 0260 0261 # Wait for all threads to be done. 0262 task_queue.join() 0263 0264 except KeyboardInterrupt: 0265 # This is a sad hack. Unfortunately subprocess goes 0266 # bonkers with ctrl-c and we start forking merrily. 0267 print('\nCtrl-C detected, goodbye.') 0268 if tmpdir: 0269 shutil.rmtree(tmpdir) 0270 os.kill(0, 9) 0271 0272 return_code = 0 0273 if args.export_fixes: 0274 print('Writing fixes to ' + args.export_fixes + ' ...') 0275 try: 0276 merge_replacement_files(tmpdir, args.export_fixes) 0277 except: 0278 print('Error exporting fixes.\n', file=sys.stderr) 0279 traceback.print_exc() 0280 return_code=1 0281 0282 if args.fix: 0283 print('Applying fixes ...') 0284 try: 0285 apply_fixes(args, tmpdir) 0286 except: 0287 print('Error applying fixes.\n', file=sys.stderr) 0288 traceback.print_exc() 0289 return_code=1 0290 0291 if tmpdir: 0292 shutil.rmtree(tmpdir) 0293 sys.exit(return_code) 0294 0295 if __name__ == '__main__': 0296 main() 0297