scopify.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. #!/usr/bin/python
  2. #
  3. # Copyright 2010 The Closure Library Authors. All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS-IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. """Automatically converts codebases over to goog.scope.
  17. Usage:
  18. cd path/to/my/dir;
  19. ../../../../javascript/closure/bin/scopify.py
  20. Scans every file in this directory, recursively. Looks for existing
  21. goog.scope calls, and goog.require'd symbols. If it makes sense to
  22. generate a goog.scope call for the file, then we will do so, and
  23. try to auto-generate some aliases based on the goog.require'd symbols.
  24. Known Issues:
  25. When a file is goog.scope'd, the file contents will be indented +2.
  26. This may put some lines over 80 chars. These will need to be fixed manually.
  27. We will only try to create aliases for capitalized names. We do not check
  28. to see if those names will conflict with any existing locals.
  29. This creates merge conflicts for every line of every outstanding change.
  30. If you intend to run this on your codebase, make sure your team members
  31. know. Better yet, send them this script so that they can scopify their
  32. outstanding changes and "accept theirs".
  33. When an alias is "captured", it can no longer be stubbed out for testing.
  34. Run your tests.
  35. """
  36. __author__ = 'nicksantos@google.com (Nick Santos)'
  37. import os.path
  38. import re
  39. import sys
  40. REQUIRES_RE = re.compile(r"goog.require\('([^']*)'\)")
  41. # Edit this manually if you want something to "always" be aliased.
  42. # TODO(nicksantos): Add a flag for this.
  43. DEFAULT_ALIASES = {}
  44. def Transform(lines):
  45. """Converts the contents of a file into javascript that uses goog.scope.
  46. Arguments:
  47. lines: A list of strings, corresponding to each line of the file.
  48. Returns:
  49. A new list of strings, or None if the file was not modified.
  50. """
  51. requires = []
  52. # Do an initial scan to be sure that this file can be processed.
  53. for line in lines:
  54. # Skip this file if it has already been scopified.
  55. if line.find('goog.scope') != -1:
  56. return None
  57. # If there are any global vars or functions, then we also have
  58. # to skip the whole file. We might be able to deal with this
  59. # more elegantly.
  60. if line.find('var ') == 0 or line.find('function ') == 0:
  61. return None
  62. for match in REQUIRES_RE.finditer(line):
  63. requires.append(match.group(1))
  64. if len(requires) == 0:
  65. return None
  66. # Backwards-sort the requires, so that when one is a substring of another,
  67. # we match the longer one first.
  68. for val in DEFAULT_ALIASES.values():
  69. if requires.count(val) == 0:
  70. requires.append(val)
  71. requires.sort()
  72. requires.reverse()
  73. # Generate a map of requires to their aliases
  74. aliases_to_globals = DEFAULT_ALIASES.copy()
  75. for req in requires:
  76. index = req.rfind('.')
  77. if index == -1:
  78. alias = req
  79. else:
  80. alias = req[(index + 1):]
  81. # Don't scopify lowercase namespaces, because they may conflict with
  82. # local variables.
  83. if alias[0].isupper():
  84. aliases_to_globals[alias] = req
  85. aliases_to_matchers = {}
  86. globals_to_aliases = {}
  87. for alias, symbol in aliases_to_globals.items():
  88. globals_to_aliases[symbol] = alias
  89. aliases_to_matchers[alias] = re.compile('\\b%s\\b' % symbol)
  90. # Insert a goog.scope that aliases all required symbols.
  91. result = []
  92. START = 0
  93. SEEN_REQUIRES = 1
  94. IN_SCOPE = 2
  95. mode = START
  96. aliases_used = set()
  97. insertion_index = None
  98. num_blank_lines = 0
  99. for line in lines:
  100. if mode == START:
  101. result.append(line)
  102. if re.search(REQUIRES_RE, line):
  103. mode = SEEN_REQUIRES
  104. elif mode == SEEN_REQUIRES:
  105. if (line and
  106. not re.search(REQUIRES_RE, line) and
  107. not line.isspace()):
  108. # There should be two blank lines before goog.scope
  109. result += ['\n'] * 2
  110. result.append('goog.scope(function() {\n')
  111. insertion_index = len(result)
  112. result += ['\n'] * num_blank_lines
  113. mode = IN_SCOPE
  114. elif line.isspace():
  115. # Keep track of the number of blank lines before each block of code so
  116. # that we can move them after the goog.scope line if necessary.
  117. num_blank_lines += 1
  118. else:
  119. # Print the blank lines we saw before this code block
  120. result += ['\n'] * num_blank_lines
  121. num_blank_lines = 0
  122. result.append(line)
  123. if mode == IN_SCOPE:
  124. for symbol in requires:
  125. if not symbol in globals_to_aliases:
  126. continue
  127. alias = globals_to_aliases[symbol]
  128. matcher = aliases_to_matchers[alias]
  129. for match in matcher.finditer(line):
  130. # Check to make sure we're not in a string.
  131. # We do this by being as conservative as possible:
  132. # if there are any quote or double quote characters
  133. # before the symbol on this line, then bail out.
  134. before_symbol = line[:match.start(0)]
  135. if before_symbol.count('"') > 0 or before_symbol.count("'") > 0:
  136. continue
  137. line = line.replace(match.group(0), alias)
  138. aliases_used.add(alias)
  139. if line.isspace():
  140. # Truncate all-whitespace lines
  141. result.append('\n')
  142. else:
  143. result.append(line)
  144. if len(aliases_used):
  145. aliases_used = [alias for alias in aliases_used]
  146. aliases_used.sort()
  147. aliases_used.reverse()
  148. for alias in aliases_used:
  149. symbol = aliases_to_globals[alias]
  150. result.insert(insertion_index,
  151. 'var %s = %s;\n' % (alias, symbol))
  152. result.append('}); // goog.scope\n')
  153. return result
  154. else:
  155. return None
  156. def TransformFileAt(path):
  157. """Converts a file into javascript that uses goog.scope.
  158. Arguments:
  159. path: A path to a file.
  160. """
  161. f = open(path)
  162. lines = Transform(f.readlines())
  163. if lines:
  164. f = open(path, 'w')
  165. for l in lines:
  166. f.write(l)
  167. f.close()
  168. if __name__ == '__main__':
  169. args = sys.argv[1:]
  170. if not len(args):
  171. args = '.'
  172. for file_name in args:
  173. if os.path.isdir(file_name):
  174. for root, dirs, files in os.walk(file_name):
  175. for name in files:
  176. if name.endswith('.js') and \
  177. not os.path.islink(os.path.join(root, name)):
  178. TransformFileAt(os.path.join(root, name))
  179. else:
  180. if file_name.endswith('.js') and \
  181. not os.path.islink(file_name):
  182. TransformFileAt(file_name)