3 # Copyright 2008 The Closure Linter Authors. All Rights Reserved.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS-IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Logic for computing dependency information for closurized JavaScript files.
19 Closurized JavaScript files express dependencies using goog.require and
20 goog.provide statements. In order for the linter to detect when a statement is
21 missing or unnecessary, all identifiers in the JavaScript file must first be
22 processed to determine if they constitute the creation or usage of a dependency.
27 from closure_linter import javascripttokens
28 from closure_linter import tokenutil
30 # pylint: disable-msg=C6409
31 TokenType = javascripttokens.JavaScriptTokenType
33 DEFAULT_EXTRA_NAMESPACES = [
34 'goog.testing.asserts',
35 'goog.testing.jsunit',
38 class ClosurizedNamespacesInfo(object):
39 """Dependency information for closurized JavaScript files.
41 Processes token streams for dependency creation or usage and provides logic
42 for determining if a given require or provide statement is unnecessary or if
43 there are missing require or provide statements.
46 def __init__(self, closurized_namespaces, ignored_extra_namespaces):
47 """Initializes an instance the ClosurizedNamespacesInfo class.
50 closurized_namespaces: A list of namespace prefixes that should be
51 processed for dependency information. Non-matching namespaces are
53 ignored_extra_namespaces: A list of namespaces that should not be reported
54 as extra regardless of whether they are actually used.
56 self._closurized_namespaces = closurized_namespaces
57 self._ignored_extra_namespaces = (ignored_extra_namespaces +
58 DEFAULT_EXTRA_NAMESPACES)
62 """Resets the internal state to prepare for processing a new file."""
64 # A list of goog.provide tokens in the order they appeared in the file.
65 self._provide_tokens = []
67 # A list of goog.require tokens in the order they appeared in the file.
68 self._require_tokens = []
70 # Namespaces that are already goog.provided.
71 self._provided_namespaces = []
73 # Namespaces that are already goog.required.
74 self._required_namespaces = []
76 # Note that created_namespaces and used_namespaces contain both namespaces
77 # and identifiers because there are many existing cases where a method or
78 # constant is provided directly instead of its namespace. Ideally, these
79 # two lists would only have to contain namespaces.
81 # A list of tuples where the first element is the namespace of an identifier
82 # created in the file and the second is the identifier itself.
83 self._created_namespaces = []
85 # A list of tuples where the first element is the namespace of an identifier
86 # used in the file and the second is the identifier itself.
87 self._used_namespaces = []
89 # A list of seemingly-unnecessary namespaces that are goog.required() and
90 # annotated with @suppress {extraRequire}.
91 self._suppressed_requires = []
93 # A list of goog.provide tokens which are duplicates.
94 self._duplicate_provide_tokens = []
96 # A list of goog.require tokens which are duplicates.
97 self._duplicate_require_tokens = []
99 # Whether this file is in a goog.scope. Someday, we may add support
100 # for checking scopified namespaces, but for now let's just fail
101 # in a more reasonable way.
102 self._scopified_file = False
104 # TODO(user): Handle the case where there are 2 different requires
105 # that can satisfy the same dependency, but only one is necessary.
107 def GetProvidedNamespaces(self):
108 """Returns the namespaces which are already provided by this file.
111 A list of strings where each string is a 'namespace' corresponding to an
112 existing goog.provide statement in the file being checked.
114 return list(self._provided_namespaces)
116 def GetRequiredNamespaces(self):
117 """Returns the namespaces which are already required by this file.
120 A list of strings where each string is a 'namespace' corresponding to an
121 existing goog.require statement in the file being checked.
123 return list(self._required_namespaces)
125 def IsExtraProvide(self, token):
126 """Returns whether the given goog.provide token is unnecessary.
129 token: A goog.provide token.
132 True if the given token corresponds to an unnecessary goog.provide
133 statement, otherwise False.
135 if self._scopified_file:
138 namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string
140 base_namespace = namespace.split('.', 1)[0]
141 if base_namespace not in self._closurized_namespaces:
144 if token in self._duplicate_provide_tokens:
147 # TODO(user): There's probably a faster way to compute this.
148 for created_namespace, created_identifier in self._created_namespaces:
149 if namespace == created_namespace or namespace == created_identifier:
154 def IsExtraRequire(self, token):
155 """Returns whether the given goog.require token is unnecessary.
158 token: A goog.require token.
161 True if the given token corresponds to an unnecessary goog.require
162 statement, otherwise False.
164 if self._scopified_file:
167 namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string
169 base_namespace = namespace.split('.', 1)[0]
170 if base_namespace not in self._closurized_namespaces:
173 if namespace in self._ignored_extra_namespaces:
176 if token in self._duplicate_require_tokens:
179 if namespace in self._suppressed_requires:
182 # If the namespace contains a component that is initial caps, then that
183 # must be the last component of the namespace.
184 parts = namespace.split('.')
185 if len(parts) > 1 and parts[-2][0].isupper():
188 # TODO(user): There's probably a faster way to compute this.
189 for used_namespace, used_identifier in self._used_namespaces:
190 if namespace == used_namespace or namespace == used_identifier:
195 def GetMissingProvides(self):
196 """Returns the set of missing provided namespaces for the current file.
199 Returns a set of strings where each string is a namespace that should be
200 provided by this file, but is not.
202 if self._scopified_file:
205 missing_provides = set()
206 for namespace, identifier in self._created_namespaces:
207 if (not self._IsPrivateIdentifier(identifier) and
208 namespace not in self._provided_namespaces and
209 identifier not in self._provided_namespaces and
210 namespace not in self._required_namespaces):
211 missing_provides.add(namespace)
213 return missing_provides
215 def GetMissingRequires(self):
216 """Returns the set of missing required namespaces for the current file.
218 For each non-private identifier used in the file, find either a
219 goog.require, goog.provide or a created identifier that satisfies it.
220 goog.require statements can satisfy the identifier by requiring either the
221 namespace of the identifier or the identifier itself. goog.provide
222 statements can satisfy the identifier by providing the namespace of the
223 identifier. A created identifier can only satisfy the used identifier if
224 it matches it exactly (necessary since things can be defined on a
225 namespace in more than one file). Note that provided namespaces should be
226 a subset of created namespaces, but we check both because in some cases we
227 can't always detect the creation of the namespace.
230 Returns a set of strings where each string is a namespace that should be
231 required by this file, but is not.
233 if self._scopified_file:
236 external_dependencies = set(self._required_namespaces)
238 # Assume goog namespace is always available.
239 external_dependencies.add('goog')
241 created_identifiers = set()
242 for namespace, identifier in self._created_namespaces:
243 created_identifiers.add(identifier)
245 missing_requires = set()
246 for namespace, identifier in self._used_namespaces:
247 if (not self._IsPrivateIdentifier(identifier) and
248 namespace not in external_dependencies and
249 namespace not in self._provided_namespaces and
250 identifier not in external_dependencies and
251 identifier not in created_identifiers):
252 missing_requires.add(namespace)
254 return missing_requires
256 def _IsPrivateIdentifier(self, identifier):
257 """Returns whether the given identifer is private."""
258 pieces = identifier.split('.')
260 if piece.endswith('_'):
264 def IsFirstProvide(self, token):
265 """Returns whether token is the first provide token."""
266 return self._provide_tokens and token == self._provide_tokens[0]
268 def IsFirstRequire(self, token):
269 """Returns whether token is the first require token."""
270 return self._require_tokens and token == self._require_tokens[0]
272 def IsLastProvide(self, token):
273 """Returns whether token is the last provide token."""
274 return self._provide_tokens and token == self._provide_tokens[-1]
276 def IsLastRequire(self, token):
277 """Returns whether token is the last require token."""
278 return self._require_tokens and token == self._require_tokens[-1]
280 def ProcessToken(self, token, state_tracker):
281 """Processes the given token for dependency information.
284 token: The token to process.
285 state_tracker: The JavaScript state tracker.
288 # Note that this method is in the critical path for the linter and has been
289 # optimized for performance in the following ways:
290 # - Tokens are checked by type first to minimize the number of function
291 # calls necessary to determine if action needs to be taken for the token.
292 # - The most common tokens types are checked for first.
293 # - The number of function calls has been minimized (thus the length of this
296 if token.type == TokenType.IDENTIFIER:
297 # TODO(user): Consider saving the whole identifier in metadata.
298 whole_identifier_string = self._GetWholeIdentifierString(token)
299 if whole_identifier_string is None:
300 # We only want to process the identifier one time. If the whole string
301 # identifier is None, that means this token was part of a multi-token
302 # identifier, but it was not the first token of the identifier.
305 # In the odd case that a goog.require is encountered inside a function,
306 # just ignore it (e.g. dynamic loading in test runners).
307 if token.string == 'goog.require' and not state_tracker.InFunction():
308 self._require_tokens.append(token)
309 namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string
310 if namespace in self._required_namespaces:
311 self._duplicate_require_tokens.append(token)
313 self._required_namespaces.append(namespace)
315 # If there is a suppression for the require, add a usage for it so it
316 # gets treated as a regular goog.require (i.e. still gets sorted).
317 jsdoc = state_tracker.GetDocComment()
318 if jsdoc and ('extraRequire' in jsdoc.suppressions):
319 self._suppressed_requires.append(namespace)
320 self._AddUsedNamespace(state_tracker, namespace)
322 elif token.string == 'goog.provide':
323 self._provide_tokens.append(token)
324 namespace = tokenutil.Search(token, TokenType.STRING_TEXT).string
325 if namespace in self._provided_namespaces:
326 self._duplicate_provide_tokens.append(token)
328 self._provided_namespaces.append(namespace)
330 # If there is a suppression for the provide, add a creation for it so it
331 # gets treated as a regular goog.provide (i.e. still gets sorted).
332 jsdoc = state_tracker.GetDocComment()
333 if jsdoc and ('extraProvide' in jsdoc.suppressions):
334 self._AddCreatedNamespace(state_tracker, namespace)
336 elif token.string == 'goog.scope':
337 self._scopified_file = True
340 jsdoc = state_tracker.GetDocComment()
341 if jsdoc and jsdoc.HasFlag('typedef'):
342 self._AddCreatedNamespace(state_tracker, whole_identifier_string,
343 self.GetClosurizedNamespace(
344 whole_identifier_string))
346 self._AddUsedNamespace(state_tracker, whole_identifier_string)
348 elif token.type == TokenType.SIMPLE_LVALUE:
349 identifier = token.values['identifier']
350 namespace = self.GetClosurizedNamespace(identifier)
351 if state_tracker.InFunction():
352 self._AddUsedNamespace(state_tracker, identifier)
353 elif namespace and namespace != 'goog':
354 self._AddCreatedNamespace(state_tracker, identifier, namespace)
356 elif token.type == TokenType.DOC_FLAG:
357 flag_type = token.attached_object.flag_type
358 is_interface = state_tracker.GetDocComment().HasFlag('interface')
359 if flag_type == 'implements' or (flag_type == 'extends' and is_interface):
360 # Interfaces should be goog.require'd.
361 doc_start = tokenutil.Search(token, TokenType.DOC_START_BRACE)
362 interface = tokenutil.Search(doc_start, TokenType.COMMENT)
363 self._AddUsedNamespace(state_tracker, interface.string)
366 def _GetWholeIdentifierString(self, token):
367 """Returns the whole identifier string for the given token.
369 Checks the tokens after the current one to see if the token is one in a
370 sequence of tokens which are actually just one identifier (i.e. a line was
371 wrapped in the middle of an identifier).
374 token: The token to check.
377 The whole identifier string or None if this token is not the first token
378 in a multi-token identifier.
382 # Search backward to determine if this token is the first token of the
383 # identifier. If it is not the first token, return None to signal that this
384 # token should be ignored.
385 prev_token = token.previous
387 if (prev_token.IsType(TokenType.IDENTIFIER) or
388 prev_token.IsType(TokenType.NORMAL) and prev_token.string == '.'):
390 elif (not prev_token.IsType(TokenType.WHITESPACE) and
391 not prev_token.IsAnyType(TokenType.COMMENT_TYPES)):
393 prev_token = prev_token.previous
395 # Search forward to find other parts of this identifier separated by white
399 if (next_token.IsType(TokenType.IDENTIFIER) or
400 next_token.IsType(TokenType.NORMAL) and next_token.string == '.'):
401 result += next_token.string
402 elif (not next_token.IsType(TokenType.WHITESPACE) and
403 not next_token.IsAnyType(TokenType.COMMENT_TYPES)):
405 next_token = next_token.next
409 def _AddCreatedNamespace(self, state_tracker, identifier, namespace=None):
410 """Adds the namespace of an identifier to the list of created namespaces.
412 If the identifier is annotated with a 'missingProvide' suppression, it is
416 state_tracker: The JavaScriptStateTracker instance.
417 identifier: The identifier to add.
418 namespace: The namespace of the identifier or None if the identifier is
422 namespace = identifier
424 jsdoc = state_tracker.GetDocComment()
425 if jsdoc and 'missingProvide' in jsdoc.suppressions:
428 self._created_namespaces.append([namespace, identifier])
430 def _AddUsedNamespace(self, state_tracker, identifier):
431 """Adds the namespace of an identifier to the list of used namespaces.
433 If the identifier is annotated with a 'missingRequire' suppression, it is
437 state_tracker: The JavaScriptStateTracker instance.
438 identifier: An identifier which has been used.
440 jsdoc = state_tracker.GetDocComment()
441 if jsdoc and 'missingRequire' in jsdoc.suppressions:
444 namespace = self.GetClosurizedNamespace(identifier)
446 self._used_namespaces.append([namespace, identifier])
448 def GetClosurizedNamespace(self, identifier):
449 """Given an identifier, returns the namespace that identifier is from.
452 identifier: The identifier to extract a namespace from.
455 The namespace the given identifier resides in, or None if one could not
458 if identifier.startswith('goog.global'):
459 # Ignore goog.global, since it is, by definition, global.
462 parts = identifier.split('.')
463 for namespace in self._closurized_namespaces:
464 if not identifier.startswith(namespace + '.'):
467 last_part = parts[-1]
469 # TODO(robbyw): Handle this: it's a multi-line identifier.
472 # The namespace for a class is the shortest prefix ending in a class
473 # name, which starts with a capital letter but is not a capitalized word.
475 # We ultimately do not want to allow requiring or providing of inner
476 # classes/enums. Instead, a file should provide only the top-level class
477 # and users should require only that.
480 if part == 'prototype' or part.isupper():
481 return '.'.join(namespace)
482 namespace.append(part)
483 if part[0].isupper():
484 return '.'.join(namespace)
486 # At this point, we know there's no class or enum, so the namespace is
487 # just the identifier with the last part removed. With the exception of
488 # apply, inherits, and call, which should also be stripped.
489 if parts[-1] in ('apply', 'inherits', 'call'):
493 # If the last part ends with an underscore, it is a private variable,
494 # method, or enum. The namespace is whatever is before it.
495 if parts and parts[-1].endswith('_'):
498 return '.'.join(parts)