check_redundant_namespace_qualifiers.py 5.4 KB

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