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