xref: /llvm-project/clang/tools/clang-format/clang-format-diff.py (revision 68d618f908458e7513e2a56be292216ea0e4ef3f)
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