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