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