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 json 28import re 29import subprocess 30import sys 31 32 33def main(): 34 parser = argparse.ArgumentParser(description= 35 'Run clang-tidy against changed files, and ' 36 'output diagnostics only for modified ' 37 'lines.') 38 parser.add_argument('-clang-tidy-binary', metavar='PATH', 39 default='clang-tidy', 40 help='path to clang-tidy binary') 41 parser.add_argument('-p', metavar='NUM', default=0, 42 help='strip the smallest prefix containing P slashes') 43 parser.add_argument('-regex', metavar='PATTERN', default=None, 44 help='custom pattern selecting file paths to check ' 45 '(case sensitive, overrides -iregex)') 46 parser.add_argument('-iregex', metavar='PATTERN', default= 47 r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', 48 help='custom pattern selecting file paths to check ' 49 '(case insensitive, overridden by -regex)') 50 51 parser.add_argument('-fix', action='store_true', default=False, 52 help='apply suggested fixes') 53 parser.add_argument('-checks', 54 help='checks filter, when not specified, use clang-tidy ' 55 'default', 56 default='') 57 parser.add_argument('-path', dest='build_path', 58 help='Path used to read a compile command database.') 59 parser.add_argument('-export-fixes', metavar='FILE', dest='export_fixes', 60 help='Create a yaml file to store suggested fixes in, ' 61 'which can be applied with clang-apply-replacements.') 62 parser.add_argument('-extra-arg', dest='extra_arg', 63 action='append', default=[], 64 help='Additional argument to append to the compiler ' 65 'command line.') 66 parser.add_argument('-extra-arg-before', dest='extra_arg_before', 67 action='append', default=[], 68 help='Additional argument to prepend to the compiler ' 69 'command line.') 70 parser.add_argument('-quiet', action='store_true', default=False, 71 help='Run clang-tidy in quiet mode') 72 clang_tidy_args = [] 73 argv = sys.argv[1:] 74 if '--' in argv: 75 clang_tidy_args.extend(argv[argv.index('--'):]) 76 argv = argv[:argv.index('--')] 77 78 args = parser.parse_args(argv) 79 80 # Extract changed lines for each file. 81 filename = None 82 lines_by_file = {} 83 for line in sys.stdin: 84 match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) 85 if match: 86 filename = match.group(2) 87 if filename == None: 88 continue 89 90 if args.regex is not None: 91 if not re.match('^%s$' % args.regex, filename): 92 continue 93 else: 94 if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): 95 continue 96 97 match = re.search('^@@.*\+(\d+)(,(\d+))?', line) 98 if match: 99 start_line = int(match.group(1)) 100 line_count = 1 101 if match.group(3): 102 line_count = int(match.group(3)) 103 if line_count == 0: 104 continue 105 end_line = start_line + line_count - 1; 106 lines_by_file.setdefault(filename, []).append([start_line, end_line]) 107 108 if len(lines_by_file) == 0: 109 print("No relevant changes found.") 110 sys.exit(0) 111 112 line_filter_json = json.dumps( 113 [{"name" : name, "lines" : lines_by_file[name]} for name in lines_by_file], 114 separators = (',', ':')) 115 116 quote = ""; 117 if sys.platform == 'win32': 118 line_filter_json=re.sub(r'"', r'"""', line_filter_json) 119 else: 120 quote = "'"; 121 122 # Run clang-tidy on files containing changes. 123 command = [args.clang_tidy_binary] 124 command.append('-line-filter=' + quote + line_filter_json + quote) 125 if args.fix: 126 command.append('-fix') 127 if args.export_fixes: 128 command.append('-export-fixes=' + args.export_fixes) 129 if args.checks != '': 130 command.append('-checks=' + quote + args.checks + quote) 131 if args.quiet: 132 command.append('-quiet') 133 if args.build_path is not None: 134 command.append('-p=%s' % args.build_path) 135 command.extend(lines_by_file.keys()) 136 for arg in args.extra_arg: 137 command.append('-extra-arg=%s' % arg) 138 for arg in args.extra_arg_before: 139 command.append('-extra-arg-before=%s' % arg) 140 command.extend(clang_tidy_args) 141 142 sys.exit(subprocess.call(' '.join(command), shell=True)) 143 144if __name__ == '__main__': 145 main() 146