Index: third_party/closure_linter/closure_linter/aliaspass.py |
diff --git a/third_party/closure_linter/closure_linter/aliaspass.py b/third_party/closure_linter/closure_linter/aliaspass.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8854380321ef080c459abb70a3ed37d8b509594f |
--- /dev/null |
+++ b/third_party/closure_linter/closure_linter/aliaspass.py |
@@ -0,0 +1,222 @@ |
+#!/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)') |
+ |
+import itertools |
+ |
+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):] |
+ |
+ |
+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 |
+ |
+ # Identify all goog.scope blocks. |
+ goog_scope_blocks = itertools.ifilter( |
+ scopeutil.IsGoogScopeBlock, |
+ self._YieldAllContexts(root_context)) |
+ |
+ # Process each block to find aliases. |
+ for scope_block in goog_scope_blocks: |
+ self._ProcessGoogScopeBlock(scope_block) |
+ |
+ def _ProcessGoogScopeBlock(self, scope_block): |
+ """Scans a goog.scope block to find aliases and mark alias tokens.""" |
+ |
+ alias_map = dict() |
+ |
+ # Iterate over every token in the scope_block. 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 = scope_block.start_token |
+ while token and self._IsTokenInParentBlock(token, scope_block): |
+ |
+ token_context = token.metadata.context |
+ |
+ # Check to see if this token is an alias. |
+ if 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 |
+ token_context.parent.parent is scope_block): |
+ 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 |
+ |
+ token = token.next # Get next token |