1#!/usr/bin/env python3 2# 3# ===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- 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 11""" 12This script reads input from a unified diff and reformats all the changed 13lines. This is useful to reformat all the lines touched by a specific patch. 14Example usage for git/svn users: 15 16 git diff -U0 --no-color --relative HEAD^ | {clang_format_diff} -p1 -i 17 svn diff --diff-cmd=diff -x-U0 | {clang_format_diff} -i 18 19It should be noted that the filename contained in the diff is used unmodified 20to determine the source file to update. Users calling this script directly 21should be careful to ensure that the path in the diff is correct relative to the 22current working directory. 23""" 24from __future__ import absolute_import, division, print_function 25 26import argparse 27import difflib 28import os 29import re 30import subprocess 31import sys 32 33if sys.version_info.major >= 3: 34 from io import StringIO 35else: 36 from io import BytesIO as StringIO 37 38 39def main(): 40 basename = os.path.basename(sys.argv[0]) 41 parser = argparse.ArgumentParser( 42 description=__doc__.format(clang_format_diff=basename), 43 formatter_class=argparse.RawDescriptionHelpFormatter, 44 ) 45 parser.add_argument( 46 "-i", 47 action="store_true", 48 default=False, 49 help="apply edits to files instead of displaying a diff", 50 ) 51 parser.add_argument( 52 "-p", 53 metavar="NUM", 54 default=0, 55 help="strip the smallest prefix containing P slashes", 56 ) 57 parser.add_argument( 58 "-regex", 59 metavar="PATTERN", 60 default=None, 61 help="custom pattern selecting file paths to reformat " 62 "(case sensitive, overrides -iregex)", 63 ) 64 parser.add_argument( 65 "-iregex", 66 metavar="PATTERN", 67 default=r".*\.(?:cpp|cc|c\+\+|cxx|cppm|ccm|cxxm|c\+\+m|c|cl|h|hh|hpp" 68 r"|hxx|m|mm|inc|js|ts|proto|protodevel|java|cs|json|s?vh?)", 69 help="custom pattern selecting file paths to reformat " 70 "(case insensitive, overridden by -regex)", 71 ) 72 parser.add_argument( 73 "-sort-includes", 74 action="store_true", 75 default=False, 76 help="let clang-format sort include blocks", 77 ) 78 parser.add_argument( 79 "-v", 80 "--verbose", 81 action="store_true", 82 help="be more verbose, ineffective without -i", 83 ) 84 parser.add_argument( 85 "-style", 86 help="formatting style to apply (LLVM, GNU, Google, Chromium, " 87 "Microsoft, Mozilla, WebKit)", 88 ) 89 parser.add_argument( 90 "-fallback-style", 91 help="The name of the predefined style used as a" 92 "fallback in case clang-format is invoked with" 93 "-style=file, but can not find the .clang-format" 94 "file to use.", 95 ) 96 parser.add_argument( 97 "-binary", 98 default="clang-format", 99 help="location of binary to use for clang-format", 100 ) 101 args = parser.parse_args() 102 103 # Extract changed lines for each file. 104 filename = None 105 lines_by_file = {} 106 for line in sys.stdin: 107 match = re.search(r"^\+\+\+\ (.*?/){%s}(\S*)" % args.p, line) 108 if match: 109 filename = match.group(2) 110 if filename is None: 111 continue 112 113 if args.regex is not None: 114 if not re.match("^%s$" % args.regex, filename): 115 continue 116 else: 117 if not re.match("^%s$" % args.iregex, filename, re.IGNORECASE): 118 continue 119 120 match = re.search(r"^@@.*\+(\d+)(?:,(\d+))?", line) 121 if match: 122 start_line = int(match.group(1)) 123 line_count = 1 124 if match.group(2): 125 line_count = int(match.group(2)) 126 # The input is something like 127 # 128 # @@ -1, +0,0 @@ 129 # 130 # which means no lines were added. 131 if line_count == 0: 132 continue 133 # Also format lines range if line_count is 0 in case of deleting 134 # surrounding statements. 135 end_line = start_line 136 if line_count != 0: 137 end_line += line_count - 1 138 lines_by_file.setdefault(filename, []).extend( 139 ["-lines", str(start_line) + ":" + str(end_line)] 140 ) 141 142 # Reformat files containing changes in place. 143 for filename, lines in lines_by_file.items(): 144 if args.i and args.verbose: 145 print("Formatting {}".format(filename)) 146 command = [args.binary, filename] 147 if args.i: 148 command.append("-i") 149 if args.sort_includes: 150 command.append("-sort-includes") 151 command.extend(lines) 152 if args.style: 153 command.extend(["-style", args.style]) 154 if args.fallback_style: 155 command.extend(["-fallback-style", args.fallback_style]) 156 157 try: 158 p = subprocess.Popen( 159 command, 160 stdout=subprocess.PIPE, 161 stderr=None, 162 stdin=subprocess.PIPE, 163 universal_newlines=True, 164 ) 165 except OSError as e: 166 # Give the user more context when clang-format isn't 167 # found/isn't executable, etc. 168 raise RuntimeError( 169 'Failed to run "%s" - %s"' % (" ".join(command), e.strerror) 170 ) 171 172 stdout, stderr = p.communicate() 173 if p.returncode != 0: 174 sys.exit(p.returncode) 175 176 if not args.i: 177 with open(filename) as f: 178 code = f.readlines() 179 formatted_code = StringIO(stdout).readlines() 180 diff = difflib.unified_diff( 181 code, 182 formatted_code, 183 filename, 184 filename, 185 "(before formatting)", 186 "(after formatting)", 187 ) 188 diff_string = "".join(diff) 189 if len(diff_string) > 0: 190 sys.stdout.write(diff_string) 191 sys.exit(1) 192 193 194if __name__ == "__main__": 195 main() 196