xref: /aosp_15_r20/external/grpc-grpc/tools/distrib/check_redundant_namespace_qualifiers.py (revision cc02d7e222339f7a4f6ba5f422e6413f4bd931f2)
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