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