xref: /llvm-project/clang/tools/clang-format/clang-format-diff.py (revision 900bb318b5b8c485e57cf810253a656b0fb683bc)
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    for filename, lines in lines_by_file.items():
142        if args.i and args.verbose:
143            print("Formatting {}".format(filename))
144        command = [args.binary, filename]
145        if args.i:
146            command.append("-i")
147        if args.sort_includes:
148            command.append("-sort-includes")
149        command.extend(lines)
150        if args.style:
151            command.extend(["-style", args.style])
152        if args.fallback_style:
153            command.extend(["-fallback-style", args.fallback_style])
154
155        try:
156            p = subprocess.Popen(
157                command,
158                stdout=subprocess.PIPE,
159                stderr=None,
160                stdin=subprocess.PIPE,
161                universal_newlines=True,
162            )
163        except OSError as e:
164            # Give the user more context when clang-format isn't
165            # found/isn't executable, etc.
166            raise RuntimeError(
167                'Failed to run "%s" - %s"' % (" ".join(command), e.strerror)
168            )
169
170        stdout, stderr = p.communicate()
171        if p.returncode != 0:
172            sys.exit(p.returncode)
173
174        if not args.i:
175            with open(filename) as f:
176                code = f.readlines()
177            formatted_code = StringIO(stdout).readlines()
178            diff = difflib.unified_diff(
179                code,
180                formatted_code,
181                filename,
182                filename,
183                "(before formatting)",
184                "(after formatting)",
185            )
186            diff_string = "".join(diff)
187            if len(diff_string) > 0:
188                sys.stdout.write(diff_string)
189                sys.exit(1)
190
191
192if __name__ == "__main__":
193    main()
194