• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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