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