1#!/usr/bin/env python
2#
3# Copyright 2012 The Closure Linter Authors. All Rights Reserved.
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
16"""Pass that scans for goog.scope aliases and lint/usage errors."""
17
18# Allow non-Google copyright
19# pylint: disable=g-bad-file-header
20
21__author__ = ('nnaze@google.com (Nathan Naze)')
22
23import itertools
24
25from closure_linter import ecmametadatapass
26from closure_linter import errors
27from closure_linter import javascripttokens
28from closure_linter import scopeutil
29from closure_linter import tokenutil
30from closure_linter.common import error
31
32
33# TODO(nnaze): Create a Pass interface and move this class, EcmaMetaDataPass,
34# and related classes onto it.
35
36
37def _GetAliasForIdentifier(identifier, alias_map):
38  """Returns the aliased_symbol name for an identifier.
39
40  Example usage:
41    >>> alias_map = {'MyClass': 'goog.foo.MyClass'}
42    >>> _GetAliasForIdentifier('MyClass.prototype.action', alias_map)
43    'goog.foo.MyClass.prototype.action'
44
45    >>> _GetAliasForIdentifier('MyClass.prototype.action', {})
46    None
47
48  Args:
49    identifier: The identifier.
50    alias_map: A dictionary mapping a symbol to an alias.
51
52  Returns:
53    The aliased symbol name or None if not found.
54  """
55  ns = identifier.split('.', 1)[0]
56  aliased_symbol = alias_map.get(ns)
57  if aliased_symbol:
58    return aliased_symbol + identifier[len(ns):]
59
60
61class AliasPass(object):
62  """Pass to identify goog.scope() usages.
63
64  Identifies goog.scope() usages and finds lint/usage errors.  Notes any
65  aliases of symbols in Closurized namespaces (that is, reassignments
66  such as "var MyClass = goog.foo.MyClass;") and annotates identifiers
67  when they're using an alias (so they may be expanded to the full symbol
68  later -- that "MyClass.prototype.action" refers to
69  "goog.foo.MyClass.prototype.action" when expanded.).
70  """
71
72  def __init__(self, closurized_namespaces=None, error_handler=None):
73    """Creates a new pass.
74
75    Args:
76      closurized_namespaces: A set of Closurized namespaces (e.g. 'goog').
77      error_handler: An error handler to report lint errors to.
78    """
79
80    self._error_handler = error_handler
81
82    # If we have namespaces, freeze the set.
83    if closurized_namespaces:
84      closurized_namespaces = frozenset(closurized_namespaces)
85
86    self._closurized_namespaces = closurized_namespaces
87
88  def Process(self, start_token):
89    """Runs the pass on a token stream.
90
91    Args:
92      start_token: The first token in the stream.
93    """
94
95    if start_token is None:
96      return
97
98    # TODO(nnaze): Add more goog.scope usage checks.
99    self._CheckGoogScopeCalls(start_token)
100
101    # If we have closurized namespaces, identify aliased identifiers.
102    if self._closurized_namespaces:
103      context = start_token.metadata.context
104      root_context = context.GetRoot()
105      self._ProcessRootContext(root_context)
106
107  def _CheckGoogScopeCalls(self, start_token):
108    """Check goog.scope calls for lint/usage errors."""
109
110    def IsScopeToken(token):
111      return (token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER and
112              token.string == 'goog.scope')
113
114    # Find all the goog.scope tokens in the file
115    scope_tokens = [t for t in start_token if IsScopeToken(t)]
116
117    for token in scope_tokens:
118      scope_context = token.metadata.context
119
120      if not (scope_context.type == ecmametadatapass.EcmaContext.STATEMENT and
121              scope_context.parent.type == ecmametadatapass.EcmaContext.ROOT):
122        self._MaybeReportError(
123            error.Error(errors.INVALID_USE_OF_GOOG_SCOPE,
124                        'goog.scope call not in global scope', token))
125
126    # There should be only one goog.scope reference.  Register errors for
127    # every instance after the first.
128    for token in scope_tokens[1:]:
129      self._MaybeReportError(
130          error.Error(errors.EXTRA_GOOG_SCOPE_USAGE,
131                      'More than one goog.scope call in file.', token))
132
133  def _MaybeReportError(self, err):
134    """Report an error to the handler (if registered)."""
135    if self._error_handler:
136      self._error_handler.HandleError(err)
137
138  @classmethod
139  def _YieldAllContexts(cls, context):
140    """Yields all contexts that are contained by the given context."""
141    yield context
142    for child_context in context.children:
143      for descendent_child in cls._YieldAllContexts(child_context):
144        yield descendent_child
145
146  @staticmethod
147  def _IsTokenInParentBlock(token, parent_block):
148    """Determines whether the given token is contained by the given block.
149
150    Args:
151      token: A token
152      parent_block: An EcmaContext.
153
154    Returns:
155      Whether the token is in a context that is or is a child of the given
156      parent_block context.
157    """
158    context = token.metadata.context
159
160    while context:
161      if context is parent_block:
162        return True
163      context = context.parent
164
165    return False
166
167  def _ProcessRootContext(self, root_context):
168    """Processes all goog.scope blocks under the root context."""
169
170    assert root_context.type is ecmametadatapass.EcmaContext.ROOT
171
172    # Identify all goog.scope blocks.
173    goog_scope_blocks = itertools.ifilter(
174        scopeutil.IsGoogScopeBlock,
175        self._YieldAllContexts(root_context))
176
177    # Process each block to find aliases.
178    for scope_block in goog_scope_blocks:
179      self._ProcessGoogScopeBlock(scope_block)
180
181  def _ProcessGoogScopeBlock(self, scope_block):
182    """Scans a goog.scope block to find aliases and mark alias tokens."""
183
184    alias_map = dict()
185
186    # Iterate over every token in the scope_block. Each token points to one
187    # context, but multiple tokens may point to the same context. We only want
188    # to check each context once, so keep track of those we've seen.
189    seen_contexts = set()
190    token = scope_block.start_token
191    while token and self._IsTokenInParentBlock(token, scope_block):
192
193      token_context = token.metadata.context
194
195      # Check to see if this token is an alias.
196      if token_context not in seen_contexts:
197        seen_contexts.add(token_context)
198
199        # If this is a alias statement in the goog.scope block.
200        if (token_context.type == ecmametadatapass.EcmaContext.VAR and
201            token_context.parent.parent is scope_block):
202          match = scopeutil.MatchAlias(token_context)
203
204          # If this is an alias, remember it in the map.
205          if match:
206            alias, symbol = match
207            symbol = _GetAliasForIdentifier(symbol, alias_map) or symbol
208            if scopeutil.IsInClosurizedNamespace(symbol,
209                                                 self._closurized_namespaces):
210              alias_map[alias] = symbol
211
212      # If this token is an identifier that matches an alias,
213      # mark the token as an alias to the original symbol.
214      if (token.type is javascripttokens.JavaScriptTokenType.SIMPLE_LVALUE or
215          token.type is javascripttokens.JavaScriptTokenType.IDENTIFIER):
216        identifier = tokenutil.GetIdentifierForToken(token)
217        if identifier:
218          aliased_symbol = _GetAliasForIdentifier(identifier, alias_map)
219          if aliased_symbol:
220            token.metadata.aliased_symbol = aliased_symbol
221
222      token = token.next  # Get next token
223