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