summaryrefslogtreecommitdiff
path: root/tools/closure_linter/closure_linter/aliaspass.py
blob: bb37bfa07b2d9087ecff365dceb91a6dadfac3c2 (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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
#!/usr/bin/env python
#
# Copyright 2012 The Closure Linter 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.

"""Pass that scans for goog.scope aliases and lint/usage errors."""

# Allow non-Google copyright
# pylint: disable=g-bad-file-header

__author__ = ('nnaze@google.com (Nathan Naze)')

from closure_linter import ecmametadatapass
from closure_linter import errors
from closure_linter import javascripttokens
from closure_linter import scopeutil
from closure_linter import tokenutil
from closure_linter.common import error


# TODO(nnaze): Create a Pass interface and move this class, EcmaMetaDataPass,
# and related classes onto it.


def _GetAliasForIdentifier(identifier, alias_map):
  """Returns the aliased_symbol name for an identifier.

  Example usage:
    >>> alias_map = {'MyClass': 'goog.foo.MyClass'}
    >>> _GetAliasForIdentifier('MyClass.prototype.action', alias_map)
    'goog.foo.MyClass.prototype.action'

    >>> _GetAliasForIdentifier('MyClass.prototype.action', {})
    None

  Args:
    identifier: The identifier.
    alias_map: A dictionary mapping a symbol to an alias.

  Returns:
    The aliased symbol name or None if not found.
  """
  ns = identifier.split('.', 1)[0]
  aliased_symbol = alias_map.get(ns)
  if aliased_symbol:
    return aliased_symbol + identifier[len(ns):]


def _SetTypeAlias(js_type, alias_map):
  """Updates the alias for identifiers in a type.

  Args:
    js_type: A typeannotation.TypeAnnotation instance.
    alias_map: A dictionary mapping a symbol to an alias.
  """
  aliased_symbol = _GetAliasForIdentifier(js_type.identifier, alias_map)
  if aliased_symbol:
    js_type.alias = aliased_symbol
  for sub_type in js_type.IterTypes():
    _SetTypeAlias(sub_type, alias_map)


class AliasPass(object):
  """Pass to identify goog.scope() usages.

  Identifies goog.scope() usages and finds lint/usage errors.  Notes any
  aliases of symbols in Closurized namespaces (that is, reassignments
  such as "var MyClass = goog.foo.MyClass;") and annotates identifiers
  when they're using an alias (so they may be expanded to the full symbol
  later -- that "MyClass.prototype.action" refers to
  "goog.foo.MyClass.prototype.action" when expanded.).
  """

  def __init__(self, closurized_namespaces=None, error_handler=None):
    """Creates a new pass.

    Args:
      closurized_namespaces: A set of Closurized namespaces (e.g. 'goog').
      error_handler: An error handler to report lint errors to.
    """

    self._error_handler = error_handler

    # If we have namespaces, freeze the set.
    if closurized_namespaces:
      closurized_namespaces = frozenset(closurized_namespaces)

    self._closurized_namespaces = closurized_namespaces

  def Process(self, start_token):
    """Runs the pass on a token stream.

    Args:
      start_token: The first token in the stream.
    """

    if start_token is None:
      return

    # TODO(nnaze): Add more goog.scope usage checks.
    self._CheckGoogScopeCalls(start_token)

    # If we have closurized namespaces, identify aliased identifiers.
    if self._closurized_namespaces:
      context = start_token.metadata.context
      root_context = context.GetRoot()
      self._ProcessRootContext(root_context)

  def _CheckGoogScopeCalls(self, start_token):
    """Check goog.scope calls for lint/usage errors."""

    def IsScopeToken(token):
      return (token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER and
              token.string == 'goog.scope')

    # Find all the goog.scope tokens in the file
    scope_tokens = [t for t in start_token if IsScopeToken(t)]

    for token in scope_tokens:
      scope_context = token.metadata.context

      if not (scope_context.type == ecmametadatapass.EcmaContext.STATEMENT and
              scope_context.parent.type == ecmametadatapass.EcmaContext.ROOT):
        self._MaybeReportError(
            error.Error(errors.INVALID_USE_OF_GOOG_SCOPE,
                        'goog.scope call not in global scope', token))

    # There should be only one goog.scope reference.  Register errors for
    # every instance after the first.
    for token in scope_tokens[1:]:
      self._MaybeReportError(
          error.Error(errors.EXTRA_GOOG_SCOPE_USAGE,
                      'More than one goog.scope call in file.', token))

  def _MaybeReportError(self, err):
    """Report an error to the handler (if registered)."""
    if self._error_handler:
      self._error_handler.HandleError(err)

  @classmethod
  def _YieldAllContexts(cls, context):
    """Yields all contexts that are contained by the given context."""
    yield context
    for child_context in context.children:
      for descendent_child in cls._YieldAllContexts(child_context):
        yield descendent_child

  @staticmethod
  def _IsTokenInParentBlock(token, parent_block):
    """Determines whether the given token is contained by the given block.

    Args:
      token: A token
      parent_block: An EcmaContext.

    Returns:
      Whether the token is in a context that is or is a child of the given
      parent_block context.
    """
    context = token.metadata.context

    while context:
      if context is parent_block:
        return True
      context = context.parent

    return False

  def _ProcessRootContext(self, root_context):
    """Processes all goog.scope blocks under the root context."""

    assert root_context.type is ecmametadatapass.EcmaContext.ROOT

    # Process aliases in statements in the root scope for goog.module-style
    # aliases.
    global_alias_map = {}
    for context in root_context.children:
      if context.type == ecmametadatapass.EcmaContext.STATEMENT:
        for statement_child in context.children:
          if statement_child.type == ecmametadatapass.EcmaContext.VAR:
            match = scopeutil.MatchModuleAlias(statement_child)
            if match:
              # goog.require aliases cannot use further aliases, the symbol is
              # the second part of match, directly.
              symbol = match[1]
              if scopeutil.IsInClosurizedNamespace(symbol,
                                                   self._closurized_namespaces):
                global_alias_map[match[0]] = symbol

    # Process each block to find aliases.
    for context in root_context.children:
      self._ProcessBlock(context, global_alias_map)

  def _ProcessBlock(self, context, global_alias_map):
    """Scans a goog.scope block to find aliases and mark alias tokens."""
    alias_map = global_alias_map.copy()

    # Iterate over every token in the context. Each token points to one
    # context, but multiple tokens may point to the same context. We only want
    # to check each context once, so keep track of those we've seen.
    seen_contexts = set()
    token = context.start_token
    while token and self._IsTokenInParentBlock(token, context):
      token_context = token.metadata.context if token.metadata else None

      # Check to see if this token is an alias.
      if token_context and token_context not in seen_contexts:
        seen_contexts.add(token_context)

        # If this is a alias statement in the goog.scope block.
        if (token_context.type == ecmametadatapass.EcmaContext.VAR and
            scopeutil.IsGoogScopeBlock(token_context.parent.parent)):
          match = scopeutil.MatchAlias(token_context)

          # If this is an alias, remember it in the map.
          if match:
            alias, symbol = match
            symbol = _GetAliasForIdentifier(symbol, alias_map) or symbol
            if scopeutil.IsInClosurizedNamespace(symbol,
                                                 self._closurized_namespaces):
              alias_map[alias] = symbol

      # If this token is an identifier that matches an alias,
      # mark the token as an alias to the original symbol.
      if (token.type is javascripttokens.JavaScriptTokenType.SIMPLE_LVALUE or
          token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER):
        identifier = tokenutil.GetIdentifierForToken(token)
        if identifier:
          aliased_symbol = _GetAliasForIdentifier(identifier, alias_map)
          if aliased_symbol:
            token.metadata.aliased_symbol = aliased_symbol

      elif token.type == javascripttokens.JavaScriptTokenType.DOC_FLAG:
        flag = token.attached_object
        if flag and flag.HasType() and flag.jstype:
          _SetTypeAlias(flag.jstype, alias_map)

      token = token.next  # Get next token