xref: /aosp_15_r20/external/grpc-grpc/tools/distrib/check_include_guards.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
1#!/usr/bin/env python3
2
3# Copyright 2016 gRPC authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17import argparse
18import os
19import os.path
20import re
21import subprocess
22import sys
23
24
25def build_valid_guard(fpath):
26    guard_components = (
27        fpath.replace("++", "XX").replace(".", "_").upper().split("/")
28    )
29    if fpath.startswith("include/"):
30        return "_".join(guard_components[1:])
31    else:
32        return "GRPC_" + "_".join(guard_components)
33
34
35def load(fpath):
36    with open(fpath, "r") as f:
37        return f.read()
38
39
40def save(fpath, contents):
41    with open(fpath, "w") as f:
42        f.write(contents)
43
44
45class GuardValidator(object):
46    def __init__(self):
47        self.ifndef_re = re.compile(r"#ifndef ([A-Z][A-Z_0-9]*)")
48        self.define_re = re.compile(r"#define ([A-Z][A-Z_0-9]*)")
49        self.endif_c_core_re = re.compile(
50            r"#endif /\* (?: *\\\n *)?([A-Z][A-Z_0-9]*) (?:\\\n *)?\*/$"
51        )
52        self.endif_re = re.compile(r"#endif  // ([A-Z][A-Z_0-9]*)")
53        self.comments_then_includes_re = re.compile(
54            (
55                r"^((//.*?$|/\*.*?\*/|[ \r\n\t])*)(([ \r\n\t]|#include"
56                r" .*)*)(#ifndef [^\n]*\n#define [^\n]*\n)"
57            ),
58            re.DOTALL | re.MULTILINE,
59        )
60        self.failed = False
61
62    def _is_c_core_header(self, fpath):
63        return "include" in fpath and not (
64            "grpc++" in fpath
65            or "grpcpp" in fpath
66            or "event_engine" in fpath
67            or fpath.endswith("/grpc_audit_logging.h")
68            or fpath.endswith("/json.h")
69        )
70
71    def fail(self, fpath, regexp, fcontents, match_txt, correct, fix):
72        c_core_header = self._is_c_core_header(fpath)
73        self.failed = True
74        invalid_guards_msg_template = (
75            "{0}: Missing preprocessor guards (RE {1}). "
76            "Please wrap your code around the following guards:\n"
77            "#ifndef {2}\n"
78            "#define {2}\n"
79            "...\n"
80            "... epic code ...\n"
81            "...\n"
82            + ("#endif /* {2} */" if c_core_header else "#endif  // {2}")
83        )
84        if not match_txt:
85            print(
86                (
87                    invalid_guards_msg_template.format(
88                        fpath, regexp.pattern, build_valid_guard(fpath)
89                    )
90                )
91            )
92            return fcontents
93
94        print(
95            (
96                (
97                    "{}: Wrong preprocessor guards (RE {}):"
98                    "\n\tFound {}, expected {}"
99                ).format(fpath, regexp.pattern, match_txt, correct)
100            )
101        )
102        if fix:
103            print("Fixing {}...\n".format(fpath))
104            fixed_fcontents = re.sub(match_txt, correct, fcontents)
105            if fixed_fcontents:
106                self.failed = False
107            return fixed_fcontents
108        else:
109            print()
110        return fcontents
111
112    def check(self, fpath, fix):
113        c_core_header = self._is_c_core_header(fpath)
114        valid_guard = build_valid_guard(fpath)
115
116        fcontents = load(fpath)
117
118        match = self.ifndef_re.search(fcontents)
119        if not match:
120            print(("something drastically wrong with: %s" % fpath))
121            return False  # failed
122        if match.lastindex is None:
123            # No ifndef. Request manual addition with hints
124            self.fail(fpath, match.re, match.string, "", "", False)
125            return False  # failed
126
127        # Does the guard end with a '_H'?
128        running_guard = match.group(1)
129        if not running_guard.endswith("_H"):
130            fcontents = self.fail(
131                fpath, match.re, match.string, match.group(1), valid_guard, fix
132            )
133            if fix:
134                save(fpath, fcontents)
135
136        # Is it the expected one based on the file path?
137        if running_guard != valid_guard:
138            fcontents = self.fail(
139                fpath, match.re, match.string, match.group(1), valid_guard, fix
140            )
141            if fix:
142                save(fpath, fcontents)
143
144        # Is there a #define? Is it the same as the #ifndef one?
145        match = self.define_re.search(fcontents)
146        if match.lastindex is None:
147            # No define. Request manual addition with hints
148            self.fail(fpath, match.re, match.string, "", "", False)
149            return False  # failed
150
151        # Is the #define guard the same as the #ifndef guard?
152        if match.group(1) != running_guard:
153            fcontents = self.fail(
154                fpath, match.re, match.string, match.group(1), valid_guard, fix
155            )
156            if fix:
157                save(fpath, fcontents)
158
159        # Is there a properly commented #endif?
160        flines = fcontents.rstrip().splitlines()
161        # Use findall and use the last result if there are multiple matches,
162        # i.e. nested include guards.
163        match = self.endif_c_core_re.findall("\n".join(flines[-3:]))
164        if not match and not c_core_header:
165            match = self.endif_re.findall("\n".join(flines[-3:]))
166        if not match:
167            # No endif. Check if we have the last line as just '#endif' and if so
168            # replace it with a properly commented one.
169            if flines[-1] == "#endif":
170                flines[-1] = "#endif" + (
171                    " /* {} */\n".format(valid_guard)
172                    if c_core_header
173                    else "  // {}\n".format(valid_guard)
174                )
175                if fix:
176                    fcontents = "\n".join(flines)
177                    save(fpath, fcontents)
178            else:
179                # something else is wrong, bail out
180                self.fail(
181                    fpath,
182                    self.endif_c_core_re if c_core_header else self.endif_re,
183                    flines[-1],
184                    "",
185                    "",
186                    False,
187                )
188        elif match[-1] != running_guard:
189            # Is the #endif guard the same as the #ifndef and #define guards?
190            fcontents = self.fail(
191                fpath, self.endif_re, fcontents, match[-1], valid_guard, fix
192            )
193            if fix:
194                save(fpath, fcontents)
195
196        match = self.comments_then_includes_re.search(fcontents)
197        assert match
198        bad_includes = match.group(3)
199        if bad_includes:
200            print(
201                "includes after initial comments but before include guards in",
202                fpath,
203            )
204            if fix:
205                fcontents = (
206                    fcontents[: match.start(3)]
207                    + match.group(5)
208                    + match.group(3)
209                    + fcontents[match.end(5) :]
210                )
211                save(fpath, fcontents)
212
213        return not self.failed  # Did the check succeed? (ie, not failed)
214
215
216# find our home
217ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "../.."))
218os.chdir(ROOT)
219
220# parse command line
221argp = argparse.ArgumentParser(description="include guard checker")
222argp.add_argument("-f", "--fix", default=False, action="store_true")
223argp.add_argument("--precommit", default=False, action="store_true")
224args = argp.parse_args()
225
226grep_filter = (
227    r"grep -E '^(include|src/core|src/cpp|test/core|test/cpp|fuzztest/)/.*\.h$'"
228)
229if args.precommit:
230    git_command = "git diff --name-only HEAD"
231else:
232    git_command = "git ls-tree -r --name-only -r HEAD"
233
234FILE_LIST_COMMAND = " | ".join((git_command, grep_filter))
235
236# scan files
237ok = True
238filename_list = []
239try:
240    filename_list = (
241        subprocess.check_output(FILE_LIST_COMMAND, shell=True)
242        .decode()
243        .splitlines()
244    )
245    # Filter out non-existent files (ie, file removed or renamed)
246    filename_list = (f for f in filename_list if os.path.isfile(f))
247except subprocess.CalledProcessError:
248    sys.exit(0)
249
250validator = GuardValidator()
251
252for filename in filename_list:
253    # Skip check for upb generated code.
254    if (
255        filename.endswith(".upb.h")
256        or filename.endswith(".upbdefs.h")
257        or filename.endswith(".upbdefs.c")
258        or filename.endswith(".upb_minitable.h")
259        or filename.endswith(".upb_minitable.c")
260    ):
261        continue
262    ok = ok and validator.check(filename, args.fix)
263
264sys.exit(0 if ok else 1)
265