1#!/usr/bin/env python 2# 3#===- clang-tidy-diff.py - ClangTidy Diff Checker -----------*- 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 11r""" 12ClangTidy Diff Checker 13====================== 14 15This script reads input from a unified diff, runs clang-tidy on all changed 16files and outputs clang-tidy warnings in changed lines only. This is useful to 17detect clang-tidy regressions in the lines touched by a specific patch. 18Example usage for git/svn users: 19 20 git diff -U0 HEAD^ | clang-tidy-diff.py -p1 21 svn diff --diff-cmd=diff -x-U0 | \ 22 clang-tidy-diff.py -fix -checks=-*,modernize-use-override 23 24""" 25 26import argparse 27import glob 28import json 29import multiprocessing 30import os 31import re 32import shutil 33import subprocess 34import sys 35import tempfile 36import threading 37import traceback 38 39try: 40 import yaml 41except ImportError: 42 yaml = None 43 44is_py2 = sys.version[0] == '2' 45 46if is_py2: 47 import Queue as queue 48else: 49 import queue as queue 50 51 52def run_tidy(task_queue, lock, timeout): 53 watchdog = None 54 while True: 55 command = task_queue.get() 56 try: 57 proc = subprocess.Popen(command, 58 stdout=subprocess.PIPE, 59 stderr=subprocess.PIPE) 60 61 if timeout is not None: 62 watchdog = threading.Timer(timeout, proc.kill) 63 watchdog.start() 64 65 stdout, stderr = proc.communicate() 66 67 with lock: 68 sys.stdout.write(stdout.decode('utf-8') + '\n') 69 sys.stdout.flush() 70 if stderr: 71 sys.stderr.write(stderr.decode('utf-8') + '\n') 72 sys.stderr.flush() 73 except Exception as e: 74 with lock: 75 sys.stderr.write('Failed: ' + str(e) + ': '.join(command) + '\n') 76 finally: 77 with lock: 78 if not (timeout is None or watchdog is None): 79 if not watchdog.is_alive(): 80 sys.stderr.write('Terminated by timeout: ' + 81 ' '.join(command) + '\n') 82 watchdog.cancel() 83 task_queue.task_done() 84 85 86def start_workers(max_tasks, tidy_caller, task_queue, lock, timeout): 87 for _ in range(max_tasks): 88 t = threading.Thread(target=tidy_caller, args=(task_queue, lock, timeout)) 89 t.daemon = True 90 t.start() 91 92 93def merge_replacement_files(tmpdir, mergefile): 94 """Merge all replacement files in a directory into a single file""" 95 # The fixes suggested by clang-tidy >= 4.0.0 are given under 96 # the top level key 'Diagnostics' in the output yaml files 97 mergekey = "Diagnostics" 98 merged = [] 99 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): 100 content = yaml.safe_load(open(replacefile, 'r')) 101 if not content: 102 continue # Skip empty files. 103 merged.extend(content.get(mergekey, [])) 104 105 if merged: 106 # MainSourceFile: The key is required by the definition inside 107 # include/clang/Tooling/ReplacementsYaml.h, but the value 108 # is actually never used inside clang-apply-replacements, 109 # so we set it to '' here. 110 output = {'MainSourceFile': '', mergekey: merged} 111 with open(mergefile, 'w') as out: 112 yaml.safe_dump(output, out) 113 else: 114 # Empty the file: 115 open(mergefile, 'w').close() 116 117 118def main(): 119 parser = argparse.ArgumentParser(description= 120 'Run clang-tidy against changed files, and ' 121 'output diagnostics only for modified ' 122 'lines.') 123 parser.add_argument('-clang-tidy-binary', metavar='PATH', 124 default='clang-tidy', 125 help='path to clang-tidy binary') 126 parser.add_argument('-p', metavar='NUM', default=0, 127 help='strip the smallest prefix containing P slashes') 128 parser.add_argument('-regex', metavar='PATTERN', default=None, 129 help='custom pattern selecting file paths to check ' 130 '(case sensitive, overrides -iregex)') 131 parser.add_argument('-iregex', metavar='PATTERN', default= 132 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', 133 help='custom pattern selecting file paths to check ' 134 '(case insensitive, overridden by -regex)') 135 parser.add_argument('-j', type=int, default=1, 136 help='number of tidy instances to be run in parallel.') 137 parser.add_argument('-timeout', type=int, default=None, 138 help='timeout per each file in seconds.') 139 parser.add_argument('-fix', action='store_true', default=False, 140 help='apply suggested fixes') 141 parser.add_argument('-checks', 142 help='checks filter, when not specified, use clang-tidy ' 143 'default', 144 default='') 145 parser.add_argument('-use-color', action='store_true', 146 help='Use colors in output') 147 parser.add_argument('-path', dest='build_path', 148 help='Path used to read a compile command database.') 149 if yaml: 150 parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes', 151 help='Create a yaml file to store suggested fixes in, ' 152 'which can be applied with clang-apply-replacements.') 153 parser.add_argument('-extra-arg', dest='extra_arg', 154 action='append', default=[], 155 help='Additional argument to append to the compiler ' 156 'command line.') 157 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 158 action='append', default=[], 159 help='Additional argument to prepend to the compiler ' 160 'command line.') 161 parser.add_argument('-quiet', action='store_true', default=False, 162 help='Run clang-tidy in quiet mode') 163 parser.add_argument('-load', dest='plugins', 164 action='append', default=[], 165 help='Load the specified plugin in clang-tidy.') 166 167 clang_tidy_args = [] 168 argv = sys.argv[1:] 169 if '--' in argv: 170 clang_tidy_args.extend(argv[argv.index('--'):]) 171 argv = argv[:argv.index('--')] 172 173 args = parser.parse_args(argv) 174 175 # Extract changed lines for each file. 176 filename = None 177 lines_by_file = {} 178 for line in sys.stdin: 179 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 180 if match: 181 filename = match.group(2) 182 if filename is None: 183 continue 184 185 if args.regex is not None: 186 if not re.match('^%s$' % args.regex, filename): 187 continue 188 else: 189 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 190 continue 191 192 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 193 if match: 194 start_line = int(match.group(1)) 195 line_count = 1 196 if match.group(3): 197 line_count = int(match.group(3)) 198 if line_count == 0: 199 continue 200 end_line = start_line + line_count - 1 201 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 202 203 if not any(lines_by_file): 204 print("No relevant changes found.") 205 sys.exit(0) 206 207 max_task_count = args.j 208 if max_task_count == 0: 209 max_task_count = multiprocessing.cpu_count() 210 max_task_count = min(len(lines_by_file), max_task_count) 211 212 tmpdir = None 213 if yaml and args.export_fixes: 214 tmpdir = tempfile.mkdtemp() 215 216 # Tasks for clang-tidy. 217 task_queue = queue.Queue(max_task_count) 218 # A lock for console output. 219 lock = threading.Lock() 220 221 # Run a pool of clang-tidy workers. 222 start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout) 223 224 # Form the common args list. 225 common_clang_tidy_args = [] 226 if args.fix: 227 common_clang_tidy_args.append('-fix') 228 if args.checks != '': 229 common_clang_tidy_args.append('-checks=' + args.checks) 230 if args.quiet: 231 common_clang_tidy_args.append('-quiet') 232 if args.build_path is not None: 233 common_clang_tidy_args.append('-p=%s' % args.build_path) 234 if args.use_color: 235 common_clang_tidy_args.append('--use-color') 236 for arg in args.extra_arg: 237 common_clang_tidy_args.append('-extra-arg=%s' % arg) 238 for arg in args.extra_arg_before: 239 common_clang_tidy_args.append('-extra-arg-before=%s' % arg) 240 for plugin in args.plugins: 241 common_clang_tidy_args.append('-load=%s' % plugin) 242 243 for name in lines_by_file: 244 line_filter_json = json.dumps( 245 [{"name": name, "lines": lines_by_file[name]}], 246 separators=(',', ':')) 247 248 # Run clang-tidy on files containing changes. 249 command = [args.clang_tidy_binary] 250 command.append('-line-filter=' + line_filter_json) 251 if yaml and args.export_fixes: 252 # Get a temporary file. We immediately close the handle so clang-tidy can 253 # overwrite it. 254 (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) 255 os.close(handle) 256 command.append('-export-fixes=' + tmp_name) 257 command.extend(common_clang_tidy_args) 258 command.append(name) 259 command.extend(clang_tidy_args) 260 261 task_queue.put(command) 262 263 # Wait for all threads to be done. 264 task_queue.join() 265 266 if yaml and args.export_fixes: 267 print('Writing fixes to ' + args.export_fixes + ' ...') 268 try: 269 merge_replacement_files(tmpdir, args.export_fixes) 270 except: 271 sys.stderr.write('Error exporting fixes.\n') 272 traceback.print_exc() 273 274 if tmpdir: 275 shutil.rmtree(tmpdir) 276 277 278if __name__ == '__main__': 279 main() 280