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