#!/usr/bin/env python3 # #===- clang-tidy-diff.py - ClangTidy Diff Checker ------------*- python -*--===# # # The LLVM Compiler Infrastructure # # This file is distributed under the University of Illinois Open Source # License. See LICENSE.TXT for details. # #===------------------------------------------------------------------------===# r""" ClangTidy Diff Checker ====================== This script reads input from a unified diff, runs clang-tidy on all changed files and outputs clang-tidy warnings in changed lines only. This is useful to detect clang-tidy regressions in the lines touched by a specific patch. Example usage for git/svn users: git diff -U0 HEAD^ | clang-tidy-diff.py -p1 svn diff --diff-cmd=diff -x-U0 | \ clang-tidy-diff.py -fix -checks=-*,modernize-use-override """ import argparse import json import re import subprocess import sys def main(): parser = argparse.ArgumentParser(description= 'Run clang-tidy against changed files, and ' 'output diagnostics only for modified ' 'lines.') parser.add_argument('-clang-tidy-binary', metavar='PATH', default='clang-tidy', help='path to clang-tidy binary') parser.add_argument('-p', metavar='NUM', default=0, help='strip the smallest prefix containing P slashes') parser.add_argument('-regex', metavar='PATTERN', default=None, help='custom pattern selecting file paths to check ' '(case sensitive, overrides -iregex)') parser.add_argument('-iregex', metavar='PATTERN', default= r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hpp|m|mm|inc)', help='custom pattern selecting file paths to check ' '(case insensitive, overridden by -regex)') parser.add_argument('-fix', action='store_true', default=False, help='apply suggested fixes') parser.add_argument('-checks', help='checks filter, when not specified, use clang-tidy ' 'default', default='') parser.add_argument('-format-style', help='style format', default='') parser.add_argument('-path', dest='build_path', help='Path used to read a compile command database.') parser.add_argument('-extra-arg', dest='extra_arg', action='append', default=[], help='Additional argument to append to the compiler ' 'command line.') parser.add_argument('-extra-arg-before', dest='extra_arg_before', action='append', default=[], help='Additional argument to prepend to the compiler ' 'command line.') parser.add_argument('-quiet', action='store_true', default=False, help='Run clang-tidy in quiet mode') clang_tidy_args = [] argv = sys.argv[1:] if '--' in argv: clang_tidy_args.extend(argv[argv.index('--'):]) argv = argv[:argv.index('--')] args = parser.parse_args(argv) # Extract changed lines for each file. filename = None lines_by_file = {} for line in sys.stdin: match = re.search('^\+\+\+\ \"?(.*?/){%s}([^ \t\n\"]*)' % args.p, line) if match: filename = match.group(2) if filename == None: continue if args.regex is not None: if not re.match('^%s$' % args.regex, filename): continue else: if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE): continue match = re.search('^@@.*\+(\d+)(,(\d+))?', line) if match: start_line = int(match.group(1)) line_count = 1 if match.group(3): line_count = int(match.group(3)) if line_count == 0: continue end_line = start_line + line_count - 1; lines_by_file.setdefault(filename, []).append([start_line, end_line]) if len(lines_by_file) == 0: print("No relevant changes found.") sys.exit(0) line_filter_json = json.dumps( [{"name" : name, "lines" : lines_by_file[name]} for name in lines_by_file], separators = (',', ':')) quote = ""; if sys.platform == 'win32': line_filter_json=re.sub(r'"', r'"""', line_filter_json) else: quote = "'"; # Run clang-tidy on files containing changes. command = [args.clang_tidy_binary] command.append('-line-filter=' + quote + line_filter_json + quote) if args.fix: command.append('-fix') if args.checks != '': command.append('-checks=' + quote + args.checks + quote) if args.format_style != '': command.append('-format-style=' + quote + args.format_style + quote) if args.quiet: command.append('-quiet') if args.build_path is not None: command.append('-p=%s' % args.build_path) command.extend(lines_by_file.keys()) for arg in args.extra_arg: command.append('-extra-arg=%s' % arg) for arg in args.extra_arg_before: command.append('-extra-arg-before=%s' % arg) command.extend(clang_tidy_args) sys.exit(subprocess.call(' '.join(command), shell=True)) if __name__ == '__main__': main()