Index: third_party/closure_linter/closure_linter/closurizednamespacesinfo.py |
diff --git a/third_party/closure_linter/closure_linter/closurizednamespacesinfo.py b/third_party/closure_linter/closure_linter/closurizednamespacesinfo.py |
index f2902dc31c7029f5f476c442f494fb28c93c6071..03bcf1caf34c67babd2dffb8bc52d816410bc8f1 100755 |
--- a/third_party/closure_linter/closure_linter/closurizednamespacesinfo.py |
+++ b/third_party/closure_linter/closure_linter/closurizednamespacesinfo.py |
@@ -24,17 +24,20 @@ processed to determine if they constitute the creation or usage of a dependency. |
+import re |
+ |
from closure_linter import javascripttokens |
from closure_linter import tokenutil |
-# pylint: disable-msg=C6409 |
+# pylint: disable=g-bad-name |
TokenType = javascripttokens.JavaScriptTokenType |
DEFAULT_EXTRA_NAMESPACES = [ |
- 'goog.testing.asserts', |
- 'goog.testing.jsunit', |
+ 'goog.testing.asserts', |
+ 'goog.testing.jsunit', |
] |
+ |
class ClosurizedNamespacesInfo(object): |
"""Dependency information for closurized JavaScript files. |
@@ -79,11 +82,13 @@ class ClosurizedNamespacesInfo(object): |
# two lists would only have to contain namespaces. |
# A list of tuples where the first element is the namespace of an identifier |
- # created in the file and the second is the identifier itself. |
+ # created in the file, the second is the identifier itself and the third is |
+ # the line number where it's created. |
self._created_namespaces = [] |
# A list of tuples where the first element is the namespace of an identifier |
- # used in the file and the second is the identifier itself. |
+ # used in the file, the second is the identifier itself and the third is the |
+ # line number where it's used. |
self._used_namespaces = [] |
# A list of seemingly-unnecessary namespaces that are goog.required() and |
@@ -111,7 +116,7 @@ class ClosurizedNamespacesInfo(object): |
A list of strings where each string is a 'namespace' corresponding to an |
existing goog.provide statement in the file being checked. |
""" |
- return list(self._provided_namespaces) |
+ return set(self._provided_namespaces) |
def GetRequiredNamespaces(self): |
"""Returns the namespaces which are already required by this file. |
@@ -120,7 +125,7 @@ class ClosurizedNamespacesInfo(object): |
A list of strings where each string is a 'namespace' corresponding to an |
existing goog.require statement in the file being checked. |
""" |
- return list(self._required_namespaces) |
+ return set(self._required_namespaces) |
def IsExtraProvide(self, token): |
"""Returns whether the given goog.provide token is unnecessary. |
@@ -132,10 +137,7 @@ class ClosurizedNamespacesInfo(object): |
True if the given token corresponds to an unnecessary goog.provide |
statement, otherwise False. |
""" |
- if self._scopified_file: |
- return False |
- |
- namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string |
+ namespace = tokenutil.GetStringAfterToken(token) |
base_namespace = namespace.split('.', 1)[0] |
if base_namespace not in self._closurized_namespaces: |
@@ -145,7 +147,7 @@ class ClosurizedNamespacesInfo(object): |
return True |
# TODO(user): There's probably a faster way to compute this. |
- for created_namespace, created_identifier in self._created_namespaces: |
+ for created_namespace, created_identifier, _ in self._created_namespaces: |
if namespace == created_namespace or namespace == created_identifier: |
return False |
@@ -161,10 +163,7 @@ class ClosurizedNamespacesInfo(object): |
True if the given token corresponds to an unnecessary goog.require |
statement, otherwise False. |
""" |
- if self._scopified_file: |
- return False |
- |
- namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string |
+ namespace = tokenutil.GetStringAfterToken(token) |
base_namespace = namespace.split('.', 1)[0] |
if base_namespace not in self._closurized_namespaces: |
@@ -186,34 +185,33 @@ class ClosurizedNamespacesInfo(object): |
return True |
# TODO(user): There's probably a faster way to compute this. |
- for used_namespace, used_identifier in self._used_namespaces: |
+ for used_namespace, used_identifier, _ in self._used_namespaces: |
if namespace == used_namespace or namespace == used_identifier: |
return False |
return True |
def GetMissingProvides(self): |
- """Returns the set of missing provided namespaces for the current file. |
+ """Returns the dict of missing provided namespaces for the current file. |
Returns: |
- Returns a set of strings where each string is a namespace that should be |
- provided by this file, but is not. |
+ Returns a dictionary of key as string and value as integer where each |
+ string(key) is a namespace that should be provided by this file, but is |
+ not and integer(value) is first line number where it's defined. |
""" |
- if self._scopified_file: |
- return set() |
- |
- missing_provides = set() |
- for namespace, identifier in self._created_namespaces: |
+ missing_provides = dict() |
+ for namespace, identifier, line_number in self._created_namespaces: |
if (not self._IsPrivateIdentifier(identifier) and |
namespace not in self._provided_namespaces and |
identifier not in self._provided_namespaces and |
- namespace not in self._required_namespaces): |
- missing_provides.add(namespace) |
+ namespace not in self._required_namespaces and |
+ namespace not in missing_provides): |
+ missing_provides[namespace] = line_number |
return missing_provides |
def GetMissingRequires(self): |
- """Returns the set of missing required namespaces for the current file. |
+ """Returns the dict of missing required namespaces for the current file. |
For each non-private identifier used in the file, find either a |
goog.require, goog.provide or a created identifier that satisfies it. |
@@ -227,29 +225,28 @@ class ClosurizedNamespacesInfo(object): |
can't always detect the creation of the namespace. |
Returns: |
- Returns a set of strings where each string is a namespace that should be |
- required by this file, but is not. |
+ Returns a dictionary of key as string and value integer where each |
+ string(key) is a namespace that should be required by this file, but is |
+ not and integer(value) is first line number where it's used. |
""" |
- if self._scopified_file: |
- return set() |
- |
external_dependencies = set(self._required_namespaces) |
# Assume goog namespace is always available. |
external_dependencies.add('goog') |
created_identifiers = set() |
- for namespace, identifier in self._created_namespaces: |
+ for namespace, identifier, line_number in self._created_namespaces: |
created_identifiers.add(identifier) |
- missing_requires = set() |
- for namespace, identifier in self._used_namespaces: |
+ missing_requires = dict() |
+ for namespace, identifier, line_number in self._used_namespaces: |
if (not self._IsPrivateIdentifier(identifier) and |
namespace not in external_dependencies and |
namespace not in self._provided_namespaces and |
identifier not in external_dependencies and |
- identifier not in created_identifiers): |
- missing_requires.add(namespace) |
+ identifier not in created_identifiers and |
+ namespace not in missing_requires): |
+ missing_requires[namespace] = line_number |
return missing_requires |
@@ -295,7 +292,7 @@ class ClosurizedNamespacesInfo(object): |
if token.type == TokenType.IDENTIFIER: |
# TODO(user): Consider saving the whole identifier in metadata. |
- whole_identifier_string = self._GetWholeIdentifierString(token) |
+ whole_identifier_string = tokenutil.GetIdentifierForToken(token) |
if whole_identifier_string is None: |
# We only want to process the identifier one time. If the whole string |
# identifier is None, that means this token was part of a multi-token |
@@ -306,7 +303,7 @@ class ClosurizedNamespacesInfo(object): |
# just ignore it (e.g. dynamic loading in test runners). |
if token.string == 'goog.require' and not state_tracker.InFunction(): |
self._require_tokens.append(token) |
- namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string |
+ namespace = tokenutil.GetStringAfterToken(token) |
if namespace in self._required_namespaces: |
self._duplicate_require_tokens.append(token) |
else: |
@@ -317,11 +314,11 @@ class ClosurizedNamespacesInfo(object): |
jsdoc = state_tracker.GetDocComment() |
if jsdoc and ('extraRequire' in jsdoc.suppressions): |
self._suppressed_requires.append(namespace) |
- self._AddUsedNamespace(state_tracker, namespace) |
+ self._AddUsedNamespace(state_tracker, namespace, token.line_number) |
elif token.string == 'goog.provide': |
self._provide_tokens.append(token) |
- namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string |
+ namespace = tokenutil.GetStringAfterToken(token) |
if namespace in self._provided_namespaces: |
self._duplicate_provide_tokens.append(token) |
else: |
@@ -331,27 +328,63 @@ class ClosurizedNamespacesInfo(object): |
# gets treated as a regular goog.provide (i.e. still gets sorted). |
jsdoc = state_tracker.GetDocComment() |
if jsdoc and ('extraProvide' in jsdoc.suppressions): |
- self._AddCreatedNamespace(state_tracker, namespace) |
+ self._AddCreatedNamespace(state_tracker, namespace, token.line_number) |
elif token.string == 'goog.scope': |
self._scopified_file = True |
+ elif token.string == 'goog.setTestOnly': |
+ |
+ # Since the message is optional, we don't want to scan to later lines. |
+ for t in tokenutil.GetAllTokensInSameLine(token): |
+ if t.type == TokenType.STRING_TEXT: |
+ message = t.string |
+ |
+ if re.match(r'^\w+(\.\w+)+$', message): |
+ # This looks like a namespace. If it's a Closurized namespace, |
+ # consider it created. |
+ base_namespace = message.split('.', 1)[0] |
+ if base_namespace in self._closurized_namespaces: |
+ self._AddCreatedNamespace(state_tracker, message, |
+ token.line_number) |
+ |
+ break |
else: |
jsdoc = state_tracker.GetDocComment() |
+ if token.metadata and token.metadata.aliased_symbol: |
+ whole_identifier_string = token.metadata.aliased_symbol |
if jsdoc and jsdoc.HasFlag('typedef'): |
self._AddCreatedNamespace(state_tracker, whole_identifier_string, |
- self.GetClosurizedNamespace( |
+ token.line_number, |
+ namespace=self.GetClosurizedNamespace( |
whole_identifier_string)) |
else: |
- self._AddUsedNamespace(state_tracker, whole_identifier_string) |
+ if not (token.metadata and token.metadata.is_alias_definition): |
+ self._AddUsedNamespace(state_tracker, whole_identifier_string, |
+ token.line_number) |
elif token.type == TokenType.SIMPLE_LVALUE: |
identifier = token.values['identifier'] |
- namespace = self.GetClosurizedNamespace(identifier) |
- if state_tracker.InFunction(): |
- self._AddUsedNamespace(state_tracker, identifier) |
- elif namespace and namespace != 'goog': |
- self._AddCreatedNamespace(state_tracker, identifier, namespace) |
+ start_token = tokenutil.GetIdentifierStart(token) |
+ if start_token and start_token != token: |
+ # Multi-line identifier being assigned. Get the whole identifier. |
+ identifier = tokenutil.GetIdentifierForToken(start_token) |
+ else: |
+ start_token = token |
+ # If an alias is defined on the start_token, use it instead. |
+ if (start_token and |
+ start_token.metadata and |
+ start_token.metadata.aliased_symbol and |
+ not start_token.metadata.is_alias_definition): |
+ identifier = start_token.metadata.aliased_symbol |
+ |
+ if identifier: |
+ namespace = self.GetClosurizedNamespace(identifier) |
+ if state_tracker.InFunction(): |
+ self._AddUsedNamespace(state_tracker, identifier, token.line_number) |
+ elif namespace and namespace != 'goog': |
+ self._AddCreatedNamespace(state_tracker, identifier, |
+ token.line_number, namespace=namespace) |
elif token.type == TokenType.DOC_FLAG: |
flag_type = token.attached_object.flag_type |
@@ -360,53 +393,11 @@ class ClosurizedNamespacesInfo(object): |
# Interfaces should be goog.require'd. |
doc_start = tokenutil.Search(token, TokenType.DOC_START_BRACE) |
interface = tokenutil.Search(doc_start, TokenType.COMMENT) |
- self._AddUsedNamespace(state_tracker, interface.string) |
- |
- |
- def _GetWholeIdentifierString(self, token): |
- """Returns the whole identifier string for the given token. |
- |
- Checks the tokens after the current one to see if the token is one in a |
- sequence of tokens which are actually just one identifier (i.e. a line was |
- wrapped in the middle of an identifier). |
+ self._AddUsedNamespace(state_tracker, interface.string, |
+ token.line_number) |
- Args: |
- token: The token to check. |
- |
- Returns: |
- The whole identifier string or None if this token is not the first token |
- in a multi-token identifier. |
- """ |
- result = '' |
- |
- # Search backward to determine if this token is the first token of the |
- # identifier. If it is not the first token, return None to signal that this |
- # token should be ignored. |
- prev_token = token.previous |
- while prev_token: |
- if (prev_token.IsType(TokenType.IDENTIFIER) or |
- prev_token.IsType(TokenType.NORMAL) and prev_token.string == '.'): |
- return None |
- elif (not prev_token.IsType(TokenType.WHITESPACE) and |
- not prev_token.IsAnyType(TokenType.COMMENT_TYPES)): |
- break |
- prev_token = prev_token.previous |
- |
- # Search forward to find other parts of this identifier separated by white |
- # space. |
- next_token = token |
- while next_token: |
- if (next_token.IsType(TokenType.IDENTIFIER) or |
- next_token.IsType(TokenType.NORMAL) and next_token.string == '.'): |
- result += next_token.string |
- elif (not next_token.IsType(TokenType.WHITESPACE) and |
- not next_token.IsAnyType(TokenType.COMMENT_TYPES)): |
- break |
- next_token = next_token.next |
- |
- return result |
- |
- def _AddCreatedNamespace(self, state_tracker, identifier, namespace=None): |
+ def _AddCreatedNamespace(self, state_tracker, identifier, line_number, |
+ namespace=None): |
"""Adds the namespace of an identifier to the list of created namespaces. |
If the identifier is annotated with a 'missingProvide' suppression, it is |
@@ -415,6 +406,7 @@ class ClosurizedNamespacesInfo(object): |
Args: |
state_tracker: The JavaScriptStateTracker instance. |
identifier: The identifier to add. |
+ line_number: Line number where namespace is created. |
namespace: The namespace of the identifier or None if the identifier is |
also the namespace. |
""" |
@@ -425,9 +417,9 @@ class ClosurizedNamespacesInfo(object): |
if jsdoc and 'missingProvide' in jsdoc.suppressions: |
return |
- self._created_namespaces.append([namespace, identifier]) |
+ self._created_namespaces.append([namespace, identifier, line_number]) |
- def _AddUsedNamespace(self, state_tracker, identifier): |
+ def _AddUsedNamespace(self, state_tracker, identifier, line_number): |
"""Adds the namespace of an identifier to the list of used namespaces. |
If the identifier is annotated with a 'missingRequire' suppression, it is |
@@ -436,14 +428,16 @@ class ClosurizedNamespacesInfo(object): |
Args: |
state_tracker: The JavaScriptStateTracker instance. |
identifier: An identifier which has been used. |
+ line_number: Line number where namespace is used. |
""" |
jsdoc = state_tracker.GetDocComment() |
if jsdoc and 'missingRequire' in jsdoc.suppressions: |
return |
namespace = self.GetClosurizedNamespace(identifier) |
- if namespace: |
- self._used_namespaces.append([namespace, identifier]) |
+ # b/5362203 If its a variable in scope then its not a required namespace. |
+ if namespace and not state_tracker.IsVariableInScope(namespace): |
+ self._used_namespaces.append([namespace, identifier, line_number]) |
def GetClosurizedNamespace(self, identifier): |
"""Given an identifier, returns the namespace that identifier is from. |