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 parser.add_argument('-path', dest='build_path', 59 help='Path used to read a compile command database.') 60 parser.add_argument('-extra-arg', dest='extra_arg', 61 action='append', default=[], 62 help='Additional argument to append to the compiler ' 63 'command line.') 64 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 65 action='append', default=[], 66 help='Additional argument to prepend to the compiler ' 67 'command line.') 68 parser.add_argument('-quiet', action='store_true', default=False, 69 help='Run clang-tidy in quiet mode') 70 clang_tidy_args = [] 71 argv = sys.argv[1:] 72 if '--' in argv: 73 clang_tidy_args.extend(argv[argv.index('--'):]) 74 argv = argv[:argv.index('--')] 75 76 args = parser.parse_args(argv) 77 78 # Extract changed lines for each file. 79 filename = None 80 lines_by_file = {} 81 for line in sys.stdin: 82 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 83 if match: 84 filename = match.group(2) 85 if filename == None: 86 continue 87 88 if args.regex is not None: 89 if not re.match('^%s$' % args.regex, filename): 90 continue 91 else: 92 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 93 continue 94 95 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 96 if match: 97 start_line = int(match.group(1)) 98 line_count = 1 99 if match.group(3): 100 line_count = int(match.group(3)) 101 if line_count == 0: 102 continue 103 end_line = start_line + line_count - 1; 104 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 105 106 if len(lines_by_file) == 0: 107 print("No relevant changes found.") 108 sys.exit(0) 109 110 line_filter_json = json.dumps( 111 [{"name" : name, "lines" : lines_by_file[name]} for name in lines_by_file], 112 separators = (',', ':')) 113 114 quote = ""; 115 if sys.platform == 'win32': 116 line_filter_json=re.sub(r'"', r'"""', line_filter_json) 117 else: 118 quote = "'"; 119 120 # Run clang-tidy on files containing changes. 121 command = [args.clang_tidy_binary] 122 command.append('-line-filter=' + quote + line_filter_json + quote) 123 if args.fix: 124 command.append('-fix') 125 if args.checks != '': 126 command.append('-checks=' + quote + args.checks + quote) 127 if args.quiet: 128 command.append('-quiet') 129 if args.build_path is not None: 130 command.append('-p=%s' % args.build_path) 131 command.extend(lines_by_file.keys()) 132 for arg in args.extra_arg: 133 command.append('-extra-arg=%s' % arg) 134 for arg in args.extra_arg_before: 135 command.append('-extra-arg-before=%s' % arg) 136 command.extend(clang_tidy_args) 137 138 sys.exit(subprocess.call(' '.join(command), shell=True)) 139 140if __name__ == '__main__': 141 main() 142