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 24 25def find_closing_mustache(contents, initial_depth): 26 """Find the closing mustache for a given number of open mustaches.""" 27 depth = initial_depth 28 start_len = len(contents) 29 while contents: 30 # Skip over strings. 31 if contents[0] == '"': 32 contents = contents[1:] 33 while contents[0] != '"': 34 if contents.startswith("\\\\"): 35 contents = contents[2:] 36 elif contents.startswith('\\"'): 37 contents = contents[2:] 38 else: 39 contents = contents[1:] 40 contents = contents[1:] 41 # And characters that might confuse us. 42 elif ( 43 contents.startswith("'{'") 44 or contents.startswith("'\"'") 45 or contents.startswith("'}'") 46 ): 47 contents = contents[3:] 48 # Skip over comments. 49 elif contents.startswith("//"): 50 contents = contents[contents.find("\n") :] 51 elif contents.startswith("/*"): 52 contents = contents[contents.find("*/") + 2 :] 53 # Count up or down if we see a mustache. 54 elif contents[0] == "{": 55 contents = contents[1:] 56 depth += 1 57 elif contents[0] == "}": 58 contents = contents[1:] 59 depth -= 1 60 if depth == 0: 61 return start_len - len(contents) 62 # Skip over everything else. 63 else: 64 contents = contents[1:] 65 return None 66 67 68def is_a_define_statement(match, body): 69 """See if the matching line begins with #define""" 70 # This does not yet help with multi-line defines 71 m = re.search( 72 r"^#define.*{}$".format(match.group(0)), 73 body[: match.end()], 74 re.MULTILINE, 75 ) 76 return m is not None 77 78 79def update_file(contents, namespaces): 80 """Scan the contents of a file, and for top-level namespaces in namespaces remove redundant usages.""" 81 output = "" 82 while contents: 83 m = re.search(r"namespace ([a-zA-Z0-9_]*) {", contents) 84 if not m: 85 output += contents 86 break 87 output += contents[: m.end()] 88 contents = contents[m.end() :] 89 end = find_closing_mustache(contents, 1) 90 if end is None: 91 print( 92 "Failed to find closing mustache for namespace {}".format( 93 m.group(1) 94 ) 95 ) 96 print("Remaining text:") 97 print(contents) 98 sys.exit(1) 99 body = contents[:end] 100 namespace = m.group(1) 101 if namespace in namespaces: 102 while body: 103 # Find instances of 'namespace::' 104 m = re.search(r"\b" + namespace + r"::\b", body) 105 if not m: 106 break 107 # Ignore instances of '::namespace::' -- these are usually meant to be there. 108 if m.start() >= 2 and body[m.start() - 2 :].startswith("::"): 109 output += body[: m.end()] 110 # Ignore #defines, since they may be used anywhere 111 elif is_a_define_statement(m, body): 112 output += body[: m.end()] 113 else: 114 output += body[: m.start()] 115 body = body[m.end() :] 116 output += body 117 contents = contents[end:] 118 return output 119 120 121# self check before doing anything 122_TEST = """ 123namespace bar { 124 namespace baz { 125 } 126} 127namespace foo {} 128namespace foo { 129 foo::a; 130 ::foo::a; 131} 132""" 133_TEST_EXPECTED = """ 134namespace bar { 135 namespace baz { 136 } 137} 138namespace foo {} 139namespace foo { 140 a; 141 ::foo::a; 142} 143""" 144output = update_file(_TEST, ["foo"]) 145if output != _TEST_EXPECTED: 146 import difflib 147 148 print("FAILED: self check") 149 print( 150 "\n".join( 151 difflib.ndiff(_TEST_EXPECTED.splitlines(1), output.splitlines(1)) 152 ) 153 ) 154 sys.exit(1) 155 156# Main loop. 157Config = collections.namedtuple("Config", ["dirs", "namespaces"]) 158 159_CONFIGURATION = (Config(["src/core", "test/core"], ["grpc_core"]),) 160 161changed = [] 162 163for config in _CONFIGURATION: 164 for dir in config.dirs: 165 for root, dirs, files in os.walk(dir): 166 for file in files: 167 if file.endswith(".cc") or file.endswith(".h"): 168 path = os.path.join(root, file) 169 try: 170 with open(path) as f: 171 contents = f.read() 172 except IOError: 173 continue 174 updated = update_file(contents, config.namespaces) 175 if updated != contents: 176 changed.append(path) 177 with open(os.path.join(root, file), "w") as f: 178 f.write(updated) 179 180if changed: 181 print("The following files were changed:") 182 for path in changed: 183 print(" " + path) 184 sys.exit(1) 185