1#!/usr/bin/env python 2# 3#===- clang-tidy-diff.py - ClangTidy Diff Checker ------------*- python -*--===# 4# 5# The LLVM Compiler Infrastructure 6# 7# This file is distributed under the University of Illinois Open Source 8# License. See LICENSE.TXT for details. 9# 10#===------------------------------------------------------------------------===# 11 12r""" 13ClangTidy Diff Checker 14====================== 15 16This script reads input from a unified diff, runs clang-tidy on all changed 17files and outputs clang-tidy warnings in changed lines only. This is useful to 18detect clang-tidy regressions in the lines touched by a specific patch. 19Example usage for git/svn users: 20 21 git diff -U0 HEAD^ | clang-tidy-diff.py -p1 22 svn diff --diff-cmd=diff -x-U0 | \ 23 clang-tidy-diff.py -fix -checks=-*,modernize-use-override 24 25""" 26 27import argparse 28import json 29import re 30import subprocess 31import sys 32 33 34def main(): 35 parser = argparse.ArgumentParser(description= 36 'Run clang-tidy against changed files, and ' 37 'output diagnostics only for modified ' 38 'lines.') 39 parser.add_argument('-clang-tidy-binary', metavar='PATH', 40 default='clang-tidy', 41 help='path to clang-tidy binary') 42 parser.add_argument('-p', metavar='NUM', default=0, 43 help='strip the smallest prefix containing P slashes') 44 parser.add_argument('-regex', metavar='PATTERN', default=None, 45 help='custom pattern selecting file paths to check ' 46 '(case sensitive, overrides -iregex)') 47 parser.add_argument('-iregex', metavar='PATTERN', default= 48 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', 49 help='custom pattern selecting file paths to check ' 50 '(case insensitive, overridden by -regex)') 51 52 parser.add_argument('-fix', action='store_true', default=False, 53 help='apply suggested fixes') 54 parser.add_argument('-checks', 55 help='checks filter, when not specified, use clang-tidy ' 56 'default', 57 default='') 58 clang_tidy_args = [] 59 argv = sys.argv[1:] 60 if '--' in argv: 61 clang_tidy_args.extend(argv[argv.index('--'):]) 62 argv = argv[:argv.index('--')] 63 64 args = parser.parse_args(argv) 65 66 # Extract changed lines for each file. 67 filename = None 68 lines_by_file = {} 69 for line in sys.stdin: 70 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 71 if match: 72 filename = match.group(2) 73 if filename == None: 74 continue 75 76 if args.regex is not None: 77 if not re.match('^%s$' % args.regex, filename): 78 continue 79 else: 80 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 81 continue 82 83 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 84 if match: 85 start_line = int(match.group(1)) 86 line_count = 1 87 if match.group(3): 88 line_count = int(match.group(3)) 89 if line_count == 0: 90 continue 91 end_line = start_line + line_count - 1; 92 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 93 94 if len(lines_by_file) == 0: 95 print("No relevant changes found.") 96 sys.exit(0) 97 98 line_filter_json = json.dumps( 99 [{"name" : name, "lines" : lines_by_file[name]} for name in lines_by_file], 100 separators = (',', ':')) 101 102 quote = ""; 103 if sys.platform == 'win32': 104 line_filter_json=re.sub(r'"', r'"""', line_filter_json) 105 else: 106 quote = "'"; 107 108 # Run clang-tidy on files containing changes. 109 command = [args.clang_tidy_binary] 110 command.append('-line-filter=' + quote + line_filter_json + quote) 111 if args.fix: 112 command.append('-fix') 113 if args.checks != '': 114 command.append('-checks=' + quote + args.checks + quote) 115 command.extend(lines_by_file.keys()) 116 command.extend(clang_tidy_args) 117 118 sys.exit(subprocess.call(' '.join(command), shell=True)) 119 120if __name__ == '__main__': 121 main() 122