1#!/usr/bin/env python3 2# Copyright 2021 gRPC authors. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16# Eliminate the kind of redundant namespace qualifiers that tend to 17# creep in when converting C to C++. 18 19import collections 20import os 21import re 22import sys 23 24IGNORED_FILES = [ 25 # note: the grpc_core::Server redundant namespace qualification is required 26 # for older gcc versions. 27 "src/core/ext/transport/chttp2/server/chttp2_server.h", 28 "src/core/server/server.h", 29 # generated code adds a necessary grpc_core:: for a logging macro which can 30 # be used anywhere. 31 "src/core/lib/debug/trace_impl.h", 32] 33 34 35def find_closing_mustache(contents, initial_depth): 36 """Find the closing mustache for a given number of open mustaches.""" 37 depth = initial_depth 38 start_len = len(contents) 39 while contents: 40 # Skip over strings. 41 if contents[0] == '"': 42 contents = contents[1:] 43 while contents[0] != '"': 44 if contents.startswith("\\\\"): 45 contents = contents[2:] 46 elif contents.startswith('\\"'): 47 contents = contents[2:] 48 else: 49 contents = contents[1:] 50 contents = contents[1:] 51 # And characters that might confuse us. 52 elif ( 53 contents.startswith("'{'") 54 or contents.startswith("'\"'") 55 or contents.startswith("'}'") 56 ): 57 contents = contents[3:] 58 # Skip over comments. 59 elif contents.startswith("//"): 60 contents = contents[contents.find("\n") :] 61 elif contents.startswith("/*"): 62 contents = contents[contents.find("*/") + 2 :] 63 # Count up or down if we see a mustache. 64 elif contents[0] == "{": 65 contents = contents[1:] 66 depth += 1 67 elif contents[0] == "}": 68 contents = contents[1:] 69 depth -= 1 70 if depth == 0: 71 return start_len - len(contents) 72 # Skip over everything else. 73 else: 74 contents = contents[1:] 75 return None 76 77 78def is_a_define_statement(match, body): 79 """See if the matching line begins with #define""" 80 # This does not yet help with multi-line defines 81 m = re.search( 82 r"^#define.*{}$".format(match.group(0)), 83 body[: match.end()], 84 re.MULTILINE, 85 ) 86 return m is not None 87 88 89def update_file(contents, namespaces): 90 """Scan the contents of a file, and for top-level namespaces in namespaces remove redundant usages.""" 91 output = "" 92 while contents: 93 m = re.search(r"namespace ([a-zA-Z0-9_]*) {", contents) 94 if not m: 95 output += contents 96 break 97 output += contents[: m.end()] 98 contents = contents[m.end() :] 99 end = find_closing_mustache(contents, 1) 100 if end is None: 101 print( 102 "Failed to find closing mustache for namespace {}".format( 103 m.group(1) 104 ) 105 ) 106 print("Remaining text:") 107 print(contents) 108 sys.exit(1) 109 body = contents[:end] 110 namespace = m.group(1) 111 if namespace in namespaces: 112 while body: 113 # Find instances of 'namespace::' 114 m = re.search(r"\b" + namespace + r"::\b", body) 115 if not m: 116 break 117 # Ignore instances of '::namespace::' -- these are usually meant to be there. 118 if m.start() >= 2 and body[m.start() - 2 :].startswith("::"): 119 output += body[: m.end()] 120 # Ignore #defines, since they may be used anywhere 121 elif is_a_define_statement(m, body): 122 output += body[: m.end()] 123 else: 124 output += body[: m.start()] 125 body = body[m.end() :] 126 output += body 127 contents = contents[end:] 128 return output 129 130 131# self check before doing anything 132_TEST = """ 133namespace bar { 134 namespace baz { 135 } 136} 137namespace foo {} 138namespace foo { 139 foo::a; 140 ::foo::a; 141} 142""" 143_TEST_EXPECTED = """ 144namespace bar { 145 namespace baz { 146 } 147} 148namespace foo {} 149namespace foo { 150 a; 151 ::foo::a; 152} 153""" 154output = update_file(_TEST, ["foo"]) 155if output != _TEST_EXPECTED: 156 import difflib 157 158 print("FAILED: self check") 159 print( 160 "\n".join( 161 difflib.ndiff(_TEST_EXPECTED.splitlines(1), output.splitlines(1)) 162 ) 163 ) 164 sys.exit(1) 165 166# Main loop. 167Config = collections.namedtuple("Config", ["dirs", "namespaces"]) 168 169_CONFIGURATION = (Config(["src/core", "test/core"], ["grpc_core"]),) 170 171changed = [] 172 173for config in _CONFIGURATION: 174 for dir in config.dirs: 175 for root, dirs, files in os.walk(dir): 176 for file in files: 177 if file.endswith(".cc") or file.endswith(".h"): 178 path = os.path.join(root, file) 179 if path in IGNORED_FILES: 180 continue 181 try: 182 with open(path) as f: 183 contents = f.read() 184 except IOError: 185 continue 186 updated = update_file(contents, config.namespaces) 187 if updated != contents: 188 changed.append(path) 189 with open(os.path.join(root, file), "w") as f: 190 f.write(updated) 191 192if changed: 193 print("The following files were changed:") 194 for path in changed: 195 print(" " + path) 196 sys.exit(1) 197