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