#!/usr/bin/env python3 # Copyright 2021 gRPC authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Eliminate the kind of redundant namespace qualifiers that tend to # creep in when converting C to C++. import collections import os import re import sys IGNORED_FILES = [ # note: the grpc_core::Server redundant namespace qualification is required # for older gcc versions. "src/core/ext/transport/chttp2/server/chttp2_server.h", "src/core/server/server.h", ] def find_closing_mustache(contents, initial_depth): """Find the closing mustache for a given number of open mustaches.""" depth = initial_depth start_len = len(contents) while contents: # Skip over strings. if contents[0] == '"': contents = contents[1:] while contents[0] != '"': if contents.startswith("\\\\"): contents = contents[2:] elif contents.startswith('\\"'): contents = contents[2:] else: contents = contents[1:] contents = contents[1:] # And characters that might confuse us. elif ( contents.startswith("'{'") or contents.startswith("'\"'") or contents.startswith("'}'") ): contents = contents[3:] # Skip over comments. elif contents.startswith("//"): contents = contents[contents.find("\n") :] elif contents.startswith("/*"): contents = contents[contents.find("*/") + 2 :] # Count up or down if we see a mustache. elif contents[0] == "{": contents = contents[1:] depth += 1 elif contents[0] == "}": contents = contents[1:] depth -= 1 if depth == 0: return start_len - len(contents) # Skip over everything else. else: contents = contents[1:] return None def is_a_define_statement(match, body): """See if the matching line begins with #define""" # This does not yet help with multi-line defines m = re.search( r"^#define.*{}$".format(match.group(0)), body[: match.end()], re.MULTILINE, ) return m is not None def update_file(contents, namespaces): """Scan the contents of a file, and for top-level namespaces in namespaces remove redundant usages.""" output = "" while contents: m = re.search(r"namespace ([a-zA-Z0-9_]*) {", contents) if not m: output += contents break output += contents[: m.end()] contents = contents[m.end() :] end = find_closing_mustache(contents, 1) if end is None: print( "Failed to find closing mustache for namespace {}".format( m.group(1) ) ) print("Remaining text:") print(contents) sys.exit(1) body = contents[:end] namespace = m.group(1) if namespace in namespaces: while body: # Find instances of 'namespace::' m = re.search(r"\b" + namespace + r"::\b", body) if not m: break # Ignore instances of '::namespace::' -- these are usually meant to be there. if m.start() >= 2 and body[m.start() - 2 :].startswith("::"): output += body[: m.end()] # Ignore #defines, since they may be used anywhere elif is_a_define_statement(m, body): output += body[: m.end()] else: output += body[: m.start()] body = body[m.end() :] output += body contents = contents[end:] return output # self check before doing anything _TEST = """ namespace bar { namespace baz { } } namespace foo {} namespace foo { foo::a; ::foo::a; } """ _TEST_EXPECTED = """ namespace bar { namespace baz { } } namespace foo {} namespace foo { a; ::foo::a; } """ output = update_file(_TEST, ["foo"]) if output != _TEST_EXPECTED: import difflib print("FAILED: self check") print( "\n".join( difflib.ndiff(_TEST_EXPECTED.splitlines(1), output.splitlines(1)) ) ) sys.exit(1) # Main loop. Config = collections.namedtuple("Config", ["dirs", "namespaces"]) _CONFIGURATION = (Config(["src/core", "test/core"], ["grpc_core"]),) changed = [] for config in _CONFIGURATION: for dir in config.dirs: for root, dirs, files in os.walk(dir): for file in files: if file.endswith(".cc") or file.endswith(".h"): path = os.path.join(root, file) if path in IGNORED_FILES: continue try: with open(path) as f: contents = f.read() except IOError: continue updated = update_file(contents, config.namespaces) if updated != contents: changed.append(path) with open(os.path.join(root, file), "w") as f: f.write(updated) if changed: print("The following files were changed:") for path in changed: print(" " + path) sys.exit(1)