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