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) and (not 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 92def merge_replacement_files(tmpdir, mergefile): 93 """Merge all replacement files in a directory into a single file""" 94 # The fixes suggested by clang-tidy >= 4.0.0 are given under 95 # the top level key 'Diagnostics' in the output yaml files 96 mergekey = "Diagnostics" 97 merged = [] 98 for replacefile in glob.iglob(os.path.join(tmpdir, '*.yaml')): 99 content = yaml.safe_load(open(replacefile, 'r')) 100 if not content: 101 continue # Skip empty files. 102 merged.extend(content.get(mergekey, [])) 103 104 if merged: 105 # MainSourceFile: The key is required by the definition inside 106 # include/clang/Tooling/ReplacementsYaml.h, but the value 107 # is actually never used inside clang-apply-replacements, 108 # so we set it to '' here. 109 output = { 'MainSourceFile': '', mergekey: merged } 110 with open(mergefile, 'w') as out: 111 yaml.safe_dump(output, out) 112 else: 113 # Empty the file: 114 open(mergefile, 'w').close() 115 116 117def main(): 118 parser = argparse.ArgumentParser(description= 119 'Run clang-tidy against changed files, and ' 120 'output diagnostics only for modified ' 121 'lines.') 122 parser.add_argument('-clang-tidy-binary', metavar='PATH', 123 default='clang-tidy', 124 help='path to clang-tidy binary') 125 parser.add_argument('-p', metavar='NUM', default=0, 126 help='strip the smallest prefix containing P slashes') 127 parser.add_argument('-regex', metavar='PATTERN', default=None, 128 help='custom pattern selecting file paths to check ' 129 '(case sensitive, overrides -iregex)') 130 parser.add_argument('-iregex', metavar='PATTERN', default= 131 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', 132 help='custom pattern selecting file paths to check ' 133 '(case insensitive, overridden by -regex)') 134 parser.add_argument('-j', type=int, default=1, 135 help='number of tidy instances to be run in parallel.') 136 parser.add_argument('-timeout', type=int, default=None, 137 help='timeout per each file in seconds.') 138 parser.add_argument('-fix', action='store_true', default=False, 139 help='apply suggested fixes') 140 parser.add_argument('-checks', 141 help='checks filter, when not specified, use clang-tidy ' 142 'default', 143 default='') 144 parser.add_argument('-path', dest='build_path', 145 help='Path used to read a compile command database.') 146 if yaml: 147 parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes', 148 help='Create a yaml file to store suggested fixes in, ' 149 'which can be applied with clang-apply-replacements.') 150 parser.add_argument('-extra-arg', dest='extra_arg', 151 action='append', default=[], 152 help='Additional argument to append to the compiler ' 153 'command line.') 154 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 155 action='append', default=[], 156 help='Additional argument to prepend to the compiler ' 157 'command line.') 158 parser.add_argument('-quiet', action='store_true', default=False, 159 help='Run clang-tidy in quiet mode') 160 clang_tidy_args = [] 161 argv = sys.argv[1:] 162 if '--' in argv: 163 clang_tidy_args.extend(argv[argv.index('--'):]) 164 argv = argv[:argv.index('--')] 165 166 args = parser.parse_args(argv) 167 168 # Extract changed lines for each file. 169 filename = None 170 lines_by_file = {} 171 for line in sys.stdin: 172 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 173 if match: 174 filename = match.group(2) 175 if filename is None: 176 continue 177 178 if args.regex is not None: 179 if not re.match('^%s$' % args.regex, filename): 180 continue 181 else: 182 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 183 continue 184 185 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 186 if match: 187 start_line = int(match.group(1)) 188 line_count = 1 189 if match.group(3): 190 line_count = int(match.group(3)) 191 if line_count == 0: 192 continue 193 end_line = start_line + line_count - 1 194 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 195 196 if not any(lines_by_file): 197 print("No relevant changes found.") 198 sys.exit(0) 199 200 max_task_count = args.j 201 if max_task_count == 0: 202 max_task_count = multiprocessing.cpu_count() 203 max_task_count = min(len(lines_by_file), max_task_count) 204 205 tmpdir = None 206 if yaml and args.export_fixes: 207 tmpdir = tempfile.mkdtemp() 208 209 # Tasks for clang-tidy. 210 task_queue = queue.Queue(max_task_count) 211 # A lock for console output. 212 lock = threading.Lock() 213 214 # Run a pool of clang-tidy workers. 215 start_workers(max_task_count, run_tidy, task_queue, lock, args.timeout) 216 217 # Form the common args list. 218 common_clang_tidy_args = [] 219 if args.fix: 220 common_clang_tidy_args.append('-fix') 221 if args.checks != '': 222 common_clang_tidy_args.append('-checks=' + args.checks) 223 if args.quiet: 224 common_clang_tidy_args.append('-quiet') 225 if args.build_path is not None: 226 common_clang_tidy_args.append('-p=%s' % args.build_path) 227 for arg in args.extra_arg: 228 common_clang_tidy_args.append('-extra-arg=%s' % arg) 229 for arg in args.extra_arg_before: 230 common_clang_tidy_args.append('-extra-arg-before=%s' % arg) 231 232 for name in lines_by_file: 233 line_filter_json = json.dumps( 234 [{"name": name, "lines": lines_by_file[name]}], 235 separators=(',', ':')) 236 237 # Run clang-tidy on files containing changes. 238 command = [args.clang_tidy_binary] 239 command.append('-line-filter=' + line_filter_json) 240 if yaml and args.export_fixes: 241 # Get a temporary file. We immediately close the handle so clang-tidy can 242 # overwrite it. 243 (handle, tmp_name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir) 244 os.close(handle) 245 command.append('-export-fixes=' + tmp_name) 246 command.extend(common_clang_tidy_args) 247 command.append(name) 248 command.extend(clang_tidy_args) 249 250 task_queue.put(command) 251 252 # Wait for all threads to be done. 253 task_queue.join() 254 255 if yaml and args.export_fixes: 256 print('Writing fixes to ' + args.export_fixes + ' ...') 257 try: 258 merge_replacement_files(tmpdir, args.export_fixes) 259 except: 260 sys.stderr.write('Error exporting fixes.\n') 261 traceback.print_exc() 262 263 if tmpdir: 264 shutil.rmtree(tmpdir) 265 266 267if __name__ == '__main__': 268 main() 269