aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/bin/scopify.py
blob: e30220b35ed70b4f67725317449ba6fddcbc99b9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
#!/usr/bin/python2.4
#
# Copyright 2010 The Closure Library Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS-IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""Automatically converts codebases over to goog.scope.

Usage:
cd path/to/my/dir;
../../../../javascript/closure/bin/scopify.py

Scans every file in this directory, recursively. Looks for existing
goog.scope calls, and goog.require'd symbols. If it makes sense to
generate a goog.scope call for the file, then we will do so, and
try to auto-generate some aliases based on the goog.require'd symbols.

Known Issues:

  When a file is goog.scope'd, the file contents will be indented +2.
  This may put some lines over 80 chars. These will need to be fixed manually.

  We will only try to create aliases for capitalized names. We do not check
  to see if those names will conflict with any existing locals.

  This creates merge conflicts for every line of every outstanding change.
  If you intend to run this on your codebase, make sure your team members
  know. Better yet, send them this script so that they can scopify their
  outstanding changes and "accept theirs".

  When an alias is "captured", it can no longer be stubbed out for testing.
  Run your tests.

"""

__author__ = 'nicksantos@google.com (Nick Santos)'

import os.path
import re
import sys

REQUIRES_RE = re.compile(r"goog.require\('([^']*)'\)")

# Edit this manually if you want something to "always" be aliased.
# TODO(nicksantos): Add a flag for this.
DEFAULT_ALIASES = {}

def Transform(lines):
  """Converts the contents of a file into javascript that uses goog.scope.

  Arguments:
    lines: A list of strings, corresponding to each line of the file.
  Returns:
    A new list of strings, or None if the file was not modified.
  """
  requires = []

  # Do an initial scan to be sure that this file can be processed.
  for line in lines:
    # Skip this file if it has already been scopified.
    if line.find('goog.scope') != -1:
      return None

    # If there are any global vars or functions, then we also have
    # to skip the whole file. We might be able to deal with this
    # more elegantly.
    if line.find('var ') == 0 or line.find('function ') == 0:
      return None

    for match in REQUIRES_RE.finditer(line):
      requires.append(match.group(1))

  if len(requires) == 0:
    return None

  # Backwards-sort the requires, so that when one is a substring of another,
  # we match the longer one first.
  for val in DEFAULT_ALIASES.values():
    if requires.count(val) == 0:
      requires.append(val)

  requires.sort()
  requires.reverse()

  # Generate a map of requires to their aliases
  aliases_to_globals = DEFAULT_ALIASES.copy()
  for req in requires:
    index = req.rfind('.')
    if index == -1:
      alias = req
    else:
      alias = req[(index + 1):]

    # Don't scopify lowercase namespaces, because they may conflict with
    # local variables.
    if alias[0].isupper():
      aliases_to_globals[alias] = req

  aliases_to_matchers = {}
  globals_to_aliases = {}
  for alias, symbol in aliases_to_globals.items():
    globals_to_aliases[symbol] = alias
    aliases_to_matchers[alias] = re.compile('\\b%s\\b' % symbol)

  # Insert a goog.scope that aliases all required symbols.
  result = []

  START = 0
  SEEN_REQUIRES = 1
  IN_SCOPE = 2

  mode = START
  aliases_used = set()
  insertion_index = None
  for line in lines:
    if mode == START:
      result.append(line)

      if re.search(REQUIRES_RE, line):
        mode = SEEN_REQUIRES

    elif mode == SEEN_REQUIRES:
      if (line and
          not re.search(REQUIRES_RE, line) and
          not line.isspace()):
        result.append('goog.scope(function() {\n')
        insertion_index = len(result)
        result.append('\n')
        mode = IN_SCOPE
      else:
        result.append(line)

    if mode == IN_SCOPE:
      for symbol in requires:
        if not symbol in globals_to_aliases:
          continue

        alias = globals_to_aliases[symbol]
        matcher = aliases_to_matchers[alias]
        for match in matcher.finditer(line):
          # Check to make sure we're not in a string.
          # We do this by being as conservative as possible:
          # if there are any quote or double quote characters
          # before the symbol on this line, then bail out.
          before_symbol = line[:match.start(0)]
          if before_symbol.count('"') > 0 or before_symbol.count("'") > 0:
            continue

          line = line.replace(match.group(0), alias)
          aliases_used.add(alias)

      if line.isspace():
        # Truncate all-whitespace lines
        result.append('\n')
      else:
        result.append('  ' + line)

  if len(aliases_used):
    aliases_used = [alias for alias in aliases_used]
    aliases_used.sort()
    aliases_used.reverse()
    for alias in aliases_used:
      symbol = aliases_to_globals[alias]
      result.insert(insertion_index,
                    '  var %s = %s;\n' % (alias, symbol))
    result.append('});\n')
    return result
  else:
    return None

def TransformFileAt(path):
  """Converts a file into javascript that uses goog.scope.

  Arguments:
    path: A path to a file.
  """
  f = open(path)
  lines = Transform(f.readlines())
  if lines:
    f = open(path, 'w')
    for l in lines:
      f.write(l)
    f.close()

if __name__ == '__main__':
  args = sys.argv[1:]
  if not len(args):
    args = '.'

  for file_name in args:
    if os.path.isdir(file_name):
      for root, dirs, files in os.walk(file_name):
        for name in files:
          if name.endswith('.js') and \
              not os.path.islink(os.path.join(root, name)):
            TransformFileAt(os.path.join(root, name))
    else:
      if file_name.endswith('.js') and \
          not os.path.islink(file_name):
        TransformFileAt(file_name)