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('-path', dest='build_path', 146 help='Path used to read a compile command database.') 147 if yaml: 148 parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes', 149 help='Create a yaml file to store suggested fixes in, ' 150 'which can be applied with clang-apply-replacements.') 151 parser.add_argument('-extra-arg', dest='extra_arg', 152 action='append', default=[], 153 help='Additional argument to append to the compiler ' 154 'command line.') 155 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 156 action='append', default=[], 157 help='Additional argument to prepend to the compiler ' 158 'command line.') 159 parser.add_argument('-quiet', action='store_true', default=False, 160 help='Run clang-tidy in quiet mode') 161 clang_tidy_args = [] 162 argv = sys.argv[1:] 163 if '--' in argv: 164 clang_tidy_args.extend(argv[argv.index('--'):]) 165 argv = argv[:argv.index('--')] 166 167 args = parser.parse_args(argv) 168 169 # Extract changed lines for each file. 170 filename = None 171 lines_by_file = {} 172 for line in sys.stdin: 173 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 174 if match: 175 filename = match.group(2) 176 if filename is None: 177 continue 178 179 if args.regex is not None: 180 if not re.match('^%s$' % args.regex, filename): 181 continue 182 else: 183 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 184 continue 185 186 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 187 if match: 188 start_line = int(match.group(1)) 189 line_count = 1 190 if match.group(3): 191 line_count = int(match.group(3)) 192 if line_count == 0: 193 continue 194 end_line = start_line + line_count - 1 195 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 196 197 if not any(lines_by_file): 198 print("No relevant changes found.") 199 sys.exit(0) 200 201 max_task_count = args.j 202 if max_task_count == 0: 203 max_task_count = multiprocessing.cpu_count() 204 max_task_count = min(len(lines_by_file), max_task_count) 205 206 tmpdir = None 207 if yaml and args.export_fixes: 208 tmpdir = tempfile.mkdtemp() 209 210 # Tasks for clang-tidy. 211 task_queue = queue.Queue(max_task_count) 212 # A lock for console output. 213 lock = threading.Lock() 214 215 # Run a pool of clang-tidy workers. 216 start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout) 217 218 # Form the common args list. 219 common_clang_tidy_args = [] 220 if args.fix: 221 common_clang_tidy_args.append('-fix') 222 if args.checks != '': 223 common_clang_tidy_args.append('-checks=' + args.checks) 224 if args.quiet: 225 common_clang_tidy_args.append('-quiet') 226 if args.build_path is not None: 227 common_clang_tidy_args.append('-p=%s' % args.build_path) 228 for arg in args.extra_arg: 229 common_clang_tidy_args.append('-extra-arg=%s' % arg) 230 for arg in args.extra_arg_before: 231 common_clang_tidy_args.append('-extra-arg-before=%s' % arg) 232 233 for name in lines_by_file: 234 line_filter_json = json.dumps( 235 [{"name": name, "lines": lines_by_file[name]}], 236 separators=(',', ':')) 237 238 # Run clang-tidy on files containing changes. 239 command = [args.clang_tidy_binary] 240 command.append('-line-filter=' + line_filter_json) 241 if yaml and args.export_fixes: 242 # Get a temporary file. We immediately close the handle so clang-tidy can 243 # overwrite it. 244 (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) 245 os.close(handle) 246 command.append('-export-fixes=' + tmp_name) 247 command.extend(common_clang_tidy_args) 248 command.append(name) 249 command.extend(clang_tidy_args) 250 251 task_queue.put(command) 252 253 # Wait for all threads to be done. 254 task_queue.join() 255 256 if yaml and args.export_fixes: 257 print('Writing fixes to ' + args.export_fixes + ' ...') 258 try: 259 merge_replacement_files(tmpdir, args.export_fixes) 260 except: 261 sys.stderr.write('Error exporting fixes.\n') 262 traceback.print_exc() 263 264 if tmpdir: 265 shutil.rmtree(tmpdir) 266 267 268if __name__ == '__main__': 269 main() 270