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