• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = fpath.replace('++', 'XX').replace('.',
27                                                         '_').upper().split('/')
28    if fpath.startswith('include/'):
29        return '_'.join(guard_components[1:])
30    else:
31        return 'GRPC_' + '_'.join(guard_components)
32
33
34def load(fpath):
35    with open(fpath, 'r') as f:
36        return f.read()
37
38
39def save(fpath, contents):
40    with open(fpath, 'w') as f:
41        f.write(contents)
42
43
44class GuardValidator(object):
45
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        self.endif_re = re.compile(r'#endif  // ([A-Z][A-Z_0-9]*)')
52        self.comments_then_includes_re = re.compile(
53            r'^((//.*?$|/\*.*?\*/|[ \r\n\t])*)(([ \r\n\t]|#include .*)*)(#ifndef [^\n]*\n#define [^\n]*\n)',
54            re.DOTALL | re.MULTILINE)
55        self.failed = False
56
57    def _is_c_core_header(self, fpath):
58        return 'include' in fpath and not (
59            'grpc++' in fpath or 'grpcpp' in fpath or 'event_engine' in fpath or
60            fpath.endswith('/grpc_audit_logging.h') or
61            fpath.endswith('/json.h'))
62
63    def fail(self, fpath, regexp, fcontents, match_txt, correct, fix):
64        c_core_header = self._is_c_core_header(fpath)
65        self.failed = True
66        invalid_guards_msg_template = (
67            '{0}: Missing preprocessor guards (RE {1}). '
68            'Please wrap your code around the following guards:\n'
69            '#ifndef {2}\n'
70            '#define {2}\n'
71            '...\n'
72            '... epic code ...\n'
73            '...\n') + ('#endif /* {2} */'
74                        if c_core_header else '#endif  // {2}')
75        if not match_txt:
76            print(
77                (invalid_guards_msg_template.format(fpath, regexp.pattern,
78                                                    build_valid_guard(fpath))))
79            return fcontents
80
81        print((('{}: Wrong preprocessor guards (RE {}):'
82                '\n\tFound {}, expected {}').format(fpath, regexp.pattern,
83                                                    match_txt, correct)))
84        if fix:
85            print(('Fixing {}...\n'.format(fpath)))
86            fixed_fcontents = re.sub(match_txt, correct, fcontents)
87            if fixed_fcontents:
88                self.failed = False
89            return fixed_fcontents
90        else:
91            print()
92        return fcontents
93
94    def check(self, fpath, fix):
95        c_core_header = self._is_c_core_header(fpath)
96        valid_guard = build_valid_guard(fpath)
97
98        fcontents = load(fpath)
99
100        match = self.ifndef_re.search(fcontents)
101        if not match:
102            print(('something drastically wrong with: %s' % fpath))
103            return False  # failed
104        if match.lastindex is None:
105            # No ifndef. Request manual addition with hints
106            self.fail(fpath, match.re, match.string, '', '', False)
107            return False  # failed
108
109        # Does the guard end with a '_H'?
110        running_guard = match.group(1)
111        if not running_guard.endswith('_H'):
112            fcontents = self.fail(fpath, match.re, match.string, match.group(1),
113                                  valid_guard, fix)
114            if fix:
115                save(fpath, fcontents)
116
117        # Is it the expected one based on the file path?
118        if running_guard != valid_guard:
119            fcontents = self.fail(fpath, match.re, match.string, match.group(1),
120                                  valid_guard, fix)
121            if fix:
122                save(fpath, fcontents)
123
124        # Is there a #define? Is it the same as the #ifndef one?
125        match = self.define_re.search(fcontents)
126        if match.lastindex is None:
127            # No define. Request manual addition with hints
128            self.fail(fpath, match.re, match.string, '', '', False)
129            return False  # failed
130
131        # Is the #define guard the same as the #ifndef guard?
132        if match.group(1) != running_guard:
133            fcontents = self.fail(fpath, match.re, match.string, match.group(1),
134                                  valid_guard, fix)
135            if fix:
136                save(fpath, fcontents)
137
138        # Is there a properly commented #endif?
139        flines = fcontents.rstrip().splitlines()
140        # Use findall and use the last result if there are multiple matches,
141        # i.e. nested include guards.
142        match = self.endif_c_core_re.findall('\n'.join(flines[-3:]))
143        if not match and not c_core_header:
144            match = self.endif_re.findall('\n'.join(flines[-3:]))
145        if not match:
146            # No endif. Check if we have the last line as just '#endif' and if so
147            # replace it with a properly commented one.
148            if flines[-1] == '#endif':
149                flines[-1] = (
150                    '#endif' +
151                    (' /* {} */\n'.format(valid_guard)
152                     if c_core_header else '  // {}\n'.format(valid_guard)))
153                if fix:
154                    fcontents = '\n'.join(flines)
155                    save(fpath, fcontents)
156            else:
157                # something else is wrong, bail out
158                self.fail(
159                    fpath,
160                    self.endif_c_core_re if c_core_header else self.endif_re,
161                    flines[-1], '', '', False)
162        elif match[-1] != running_guard:
163            # Is the #endif guard the same as the #ifndef and #define guards?
164            fcontents = self.fail(fpath, self.endif_re, fcontents, match[-1],
165                                  valid_guard, fix)
166            if fix:
167                save(fpath, fcontents)
168
169        match = self.comments_then_includes_re.search(fcontents)
170        assert (match)
171        bad_includes = match.group(3)
172        if bad_includes:
173            print(
174                "includes after initial comments but before include guards in",
175                fpath)
176            if fix:
177                fcontents = fcontents[:match.start(3)] + match.group(
178                    5) + match.group(3) + fcontents[match.end(5):]
179                save(fpath, fcontents)
180
181        return not self.failed  # Did the check succeed? (ie, not failed)
182
183
184# find our home
185ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
186os.chdir(ROOT)
187
188# parse command line
189argp = argparse.ArgumentParser(description='include guard checker')
190argp.add_argument('-f', '--fix', default=False, action='store_true')
191argp.add_argument('--precommit', default=False, action='store_true')
192args = argp.parse_args()
193
194grep_filter = r"grep -E '^(include|src/core|src/cpp|test/core|test/cpp|fuzztest/)/.*\.h$'"
195if args.precommit:
196    git_command = 'git diff --name-only HEAD'
197else:
198    git_command = 'git ls-tree -r --name-only -r HEAD'
199
200FILE_LIST_COMMAND = ' | '.join((git_command, grep_filter))
201
202# scan files
203ok = True
204filename_list = []
205try:
206    filename_list = subprocess.check_output(FILE_LIST_COMMAND,
207                                            shell=True).decode().splitlines()
208    # Filter out non-existent files (ie, file removed or renamed)
209    filename_list = (f for f in filename_list if os.path.isfile(f))
210except subprocess.CalledProcessError:
211    sys.exit(0)
212
213validator = GuardValidator()
214
215for filename in filename_list:
216    # Skip check for upb generated code.
217    if (filename.endswith('.upb.h') or filename.endswith('.upb.c') or
218            filename.endswith('.upbdefs.h') or filename.endswith('.upbdefs.c')):
219        continue
220    ok = ok and validator.check(filename, args.fix)
221
222sys.exit(0 if ok else 1)
223