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