mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
tools: replace closure-linter with eslint
PR-URL: https://github.com/iojs/io.js/pull/1539 Fixes: https://github.com/iojs/io.js/issues/1253 Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com> Reviewed-By: Trevor Norris <trev.norris@gmail.com> Reviewed-By: Roman Reiss <me@silverwind.io> Reviewed-By: Chris Dickinson <christopher.s.dickinson@gmail.com> Reviewed-By: Johan Bergström <bugs@bergstroem.nu> Reviewed-By: Fedor Indutny <fedor.indutny@gmail.com>
This commit is contained in:
1 parent
5883a59b21
commit
f9dd34d301
3016 files changed
+138963
-34334
No files matched your search
@@ -0,0 +1 @@
|
|||||||
|
lib/punycode.js
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
env:
|
||||||
|
node: true
|
||||||
|
|
||||||
|
# enable ECMAScript features
|
||||||
|
ecmaFeatures:
|
||||||
|
blockBindings: true
|
||||||
|
templateStrings: true
|
||||||
|
octalLiterals: true
|
||||||
|
binaryLiterals: true
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# Possible Errors
|
||||||
|
# list: https://github.com/eslint/eslint/tree/master/docs/rules#possible-errors
|
||||||
|
## check debugger sentence
|
||||||
|
no-debugger: 2
|
||||||
|
## check duplicate arguments
|
||||||
|
no-dupe-args: 2
|
||||||
|
## check duplicate object keys
|
||||||
|
no-dupe-keys: 2
|
||||||
|
## check duplicate switch-case
|
||||||
|
no-duplicate-case: 2
|
||||||
|
## disallow assignment of exceptional params
|
||||||
|
no-ex-assign: 2
|
||||||
|
## disallow use of reserved words as keys like enum, class
|
||||||
|
no-reserved-keys: 2
|
||||||
|
## disallow unreachable code
|
||||||
|
no-unreachable: 2
|
||||||
|
## require valid typeof compared string like typeof foo === 'strnig'
|
||||||
|
valid-typeof: 2
|
||||||
|
|
||||||
|
# Best Practices
|
||||||
|
# list: https://github.com/eslint/eslint/tree/master/docs/rules#best-practices
|
||||||
|
## require falls through comment on switch-case
|
||||||
|
no-fallthrough: 2
|
||||||
|
|
||||||
|
# Stylistic Issues
|
||||||
|
# list: https://github.com/eslint/eslint/tree/master/docs/rules#stylistic-issues
|
||||||
|
## use single quote, we can use double quote when escape chars
|
||||||
|
quotes:
|
||||||
|
- 2
|
||||||
|
- "single"
|
||||||
|
- "avoid-escape"
|
||||||
|
## 2 space indentation
|
||||||
|
indent:
|
||||||
|
- 2
|
||||||
|
- 2
|
||||||
|
## add space after comma
|
||||||
|
comma-spacing: 2
|
||||||
|
## put semi-colon
|
||||||
|
semi: 2
|
||||||
|
## require spaces operator like var sum = 1 + 1;
|
||||||
|
space-infix-ops: 2
|
||||||
|
## require spaces return, throw, case
|
||||||
|
space-return-throw-case: 2
|
||||||
|
## require parens for Constructor
|
||||||
|
new-parens: 2
|
||||||
|
## max 80 length
|
||||||
|
max-len:
|
||||||
|
- 2
|
||||||
|
- 80
|
||||||
|
- 2
|
||||||
|
|
||||||
|
|
||||||
|
# Strict Mode
|
||||||
|
# list: https://github.com/eslint/eslint/tree/master/docs/rules#strict-mode
|
||||||
|
## 'use strict' on top
|
||||||
|
strict:
|
||||||
|
- 2
|
||||||
|
- "global"
|
||||||
|
|
||||||
|
# Global scoped method and vars
|
||||||
|
globals:
|
||||||
|
DTRACE_HTTP_CLIENT_REQUEST: true
|
||||||
|
LTTNG_HTTP_CLIENT_REQUEST: true
|
||||||
|
COUNTER_HTTP_CLIENT_REQUEST: true
|
||||||
|
DTRACE_HTTP_CLIENT_RESPONSE: true
|
||||||
|
LTTNG_HTTP_CLIENT_RESPONSE: true
|
||||||
|
COUNTER_HTTP_CLIENT_RESPONSE: true
|
||||||
|
DTRACE_HTTP_SERVER_REQUEST: true
|
||||||
|
LTTNG_HTTP_SERVER_REQUEST: true
|
||||||
|
COUNTER_HTTP_SERVER_REQUEST: true
|
||||||
|
DTRACE_HTTP_SERVER_RESPONSE: true
|
||||||
|
LTTNG_HTTP_SERVER_RESPONSE: true
|
||||||
|
COUNTER_HTTP_SERVER_RESPONSE: true
|
||||||
|
DTRACE_NET_STREAM_END: true
|
||||||
|
LTTNG_NET_STREAM_END: true
|
||||||
|
COUNTER_NET_SERVER_CONNECTION_CLOSE: true
|
||||||
|
DTRACE_NET_SERVER_CONNECTION: true
|
||||||
|
LTTNG_NET_SERVER_CONNECTION: true
|
||||||
|
COUNTER_NET_SERVER_CONNECTION: true
|
||||||
|
|
||||||
@@ -386,11 +386,8 @@ bench-idle:
|
|||||||
sleep 1
|
sleep 1
|
||||||
./$(NODE_EXE) benchmark/idle_clients.js &
|
./$(NODE_EXE) benchmark/idle_clients.js &
|
||||||
|
|
||||||
jslintfix:
|
|
||||||
PYTHONPATH=tools/closure_linter/:tools/gflags/ $(PYTHON) tools/closure_linter/closure_linter/fixjsstyle.py --strict --nojsdoc -r lib/ -r src/ --exclude_files lib/punycode.js
|
|
||||||
|
|
||||||
jslint:
|
jslint:
|
||||||
PYTHONPATH=tools/closure_linter/:tools/gflags/ $(PYTHON) tools/closure_linter/closure_linter/gjslint.py --unix_mode --strict --nojsdoc -r lib/ -r src/ --exclude_files lib/punycode.js
|
./$(NODE_EXE) tools/eslint/bin/eslint.js src/*.js lib/*.js --reset --quiet
|
||||||
|
|
||||||
CPPLINT_EXCLUDE ?=
|
CPPLINT_EXCLUDE ?=
|
||||||
CPPLINT_EXCLUDE += src/node_lttng.cc
|
CPPLINT_EXCLUDE += src/node_lttng.cc
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# This is a list of contributors to the Closure Linter.
|
|
||||||
|
|
||||||
# Names should be added to this file like so:
|
|
||||||
# Name or Organization <email address>
|
|
||||||
|
|
||||||
Google Inc.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
This repository contains the Closure Linter - a style checker for JavaScript.
|
|
||||||
|
|
||||||
To install the application, run
|
|
||||||
python ./setup.py install
|
|
||||||
|
|
||||||
After installing, you get two helper applications installed into /usr/local/bin:
|
|
||||||
|
|
||||||
gjslint.py - runs the linter and checks for errors
|
|
||||||
fixjsstyle.py - tries to fix errors automatically
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Package indicator for gjslint."""
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#!/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)')
|
|
||||||
|
|
||||||
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):]
|
|
||||||
|
|
||||||
|
|
||||||
def _SetTypeAlias(js_type, alias_map):
|
|
||||||
"""Updates the alias for identifiers in a type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
js_type: A typeannotation.TypeAnnotation instance.
|
|
||||||
alias_map: A dictionary mapping a symbol to an alias.
|
|
||||||
"""
|
|
||||||
aliased_symbol = _GetAliasForIdentifier(js_type.identifier, alias_map)
|
|
||||||
if aliased_symbol:
|
|
||||||
js_type.alias = aliased_symbol
|
|
||||||
for sub_type in js_type.IterTypes():
|
|
||||||
_SetTypeAlias(sub_type, alias_map)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Process aliases in statements in the root scope for goog.module-style
|
|
||||||
# aliases.
|
|
||||||
global_alias_map = {}
|
|
||||||
for context in root_context.children:
|
|
||||||
if context.type == ecmametadatapass.EcmaContext.STATEMENT:
|
|
||||||
for statement_child in context.children:
|
|
||||||
if statement_child.type == ecmametadatapass.EcmaContext.VAR:
|
|
||||||
match = scopeutil.MatchModuleAlias(statement_child)
|
|
||||||
if match:
|
|
||||||
# goog.require aliases cannot use further aliases, the symbol is
|
|
||||||
# the second part of match, directly.
|
|
||||||
symbol = match[1]
|
|
||||||
if scopeutil.IsInClosurizedNamespace(symbol,
|
|
||||||
self._closurized_namespaces):
|
|
||||||
global_alias_map[match[0]] = symbol
|
|
||||||
|
|
||||||
# Process each block to find aliases.
|
|
||||||
for context in root_context.children:
|
|
||||||
self._ProcessBlock(context, global_alias_map)
|
|
||||||
|
|
||||||
def _ProcessBlock(self, context, global_alias_map):
|
|
||||||
"""Scans a goog.scope block to find aliases and mark alias tokens."""
|
|
||||||
alias_map = global_alias_map.copy()
|
|
||||||
|
|
||||||
# Iterate over every token in the context. 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 = context.start_token
|
|
||||||
while token and self._IsTokenInParentBlock(token, context):
|
|
||||||
token_context = token.metadata.context if token.metadata else None
|
|
||||||
|
|
||||||
# Check to see if this token is an alias.
|
|
||||||
if token_context and 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
|
|
||||||
scopeutil.IsGoogScopeBlock(token_context.parent.parent)):
|
|
||||||
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
|
|
||||||
|
|
||||||
elif token.type == javascripttokens.JavaScriptTokenType.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
if flag and flag.HasType() and flag.jstype:
|
|
||||||
_SetTypeAlias(flag.jstype, alias_map)
|
|
||||||
|
|
||||||
token = token.next # Get next token
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the aliaspass module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
|
|
||||||
def _GetTokenByLineAndString(start_token, string, line_number):
|
|
||||||
for token in start_token:
|
|
||||||
if token.line_number == line_number and token.string == string:
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class AliasPassTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testInvalidGoogScopeCall(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCOPE_SCRIPT)
|
|
||||||
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
alias_pass = aliaspass.AliasPass(
|
|
||||||
error_handler=error_accumulator)
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_errors = error_accumulator.GetErrors()
|
|
||||||
self.assertEquals(1, len(alias_errors))
|
|
||||||
|
|
||||||
alias_error = alias_errors[0]
|
|
||||||
|
|
||||||
self.assertEquals(errors.INVALID_USE_OF_GOOG_SCOPE, alias_error.code)
|
|
||||||
self.assertEquals('goog.scope', alias_error.token.string)
|
|
||||||
|
|
||||||
def testAliasedIdentifiers(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_ALIAS_SCRIPT)
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog', 'myproject']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_token = _GetTokenByLineAndString(start_token, 'Event', 4)
|
|
||||||
self.assertTrue(alias_token.metadata.is_alias_definition)
|
|
||||||
|
|
||||||
my_class_token = _GetTokenByLineAndString(start_token, 'myClass', 9)
|
|
||||||
self.assertIsNone(my_class_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
component_token = _GetTokenByLineAndString(start_token, 'Component', 17)
|
|
||||||
self.assertEquals('goog.ui.Component',
|
|
||||||
component_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
event_token = _GetTokenByLineAndString(start_token, 'Event.Something', 17)
|
|
||||||
self.assertEquals('goog.events.Event.Something',
|
|
||||||
event_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
non_closurized_token = _GetTokenByLineAndString(
|
|
||||||
start_token, 'NonClosurizedClass', 18)
|
|
||||||
self.assertIsNone(non_closurized_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
long_start_token = _GetTokenByLineAndString(start_token, 'Event', 24)
|
|
||||||
self.assertEquals('goog.events.Event.MultilineIdentifier.someMethod',
|
|
||||||
long_start_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
def testAliasedDoctypes(self):
|
|
||||||
"""Tests that aliases are correctly expanded within type annotations."""
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_ALIAS_SCRIPT)
|
|
||||||
tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
tracker.DocFlagPass(start_token, error_handler=None)
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog', 'myproject']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
flag_token = _GetTokenByLineAndString(start_token, '@type', 22)
|
|
||||||
self.assertEquals(
|
|
||||||
'goog.events.Event.<goog.ui.Component,Array<myproject.foo.MyClass>>',
|
|
||||||
repr(flag_token.attached_object.jstype))
|
|
||||||
|
|
||||||
def testModuleAlias(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass("""
|
|
||||||
goog.module('goog.test');
|
|
||||||
var Alias = goog.require('goog.Alias');
|
|
||||||
Alias.use();
|
|
||||||
""")
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
alias_token = _GetTokenByLineAndString(start_token, 'Alias', 3)
|
|
||||||
self.assertTrue(alias_token.metadata.is_alias_definition)
|
|
||||||
|
|
||||||
def testMultipleGoogScopeCalls(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(
|
|
||||||
_TEST_MULTIPLE_SCOPE_SCRIPT)
|
|
||||||
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(
|
|
||||||
set(['goog', 'myproject']),
|
|
||||||
error_handler=error_accumulator)
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_errors = error_accumulator.GetErrors()
|
|
||||||
|
|
||||||
self.assertEquals(3, len(alias_errors))
|
|
||||||
|
|
||||||
error = alias_errors[0]
|
|
||||||
self.assertEquals(errors.INVALID_USE_OF_GOOG_SCOPE, error.code)
|
|
||||||
self.assertEquals(7, error.token.line_number)
|
|
||||||
|
|
||||||
error = alias_errors[1]
|
|
||||||
self.assertEquals(errors.EXTRA_GOOG_SCOPE_USAGE, error.code)
|
|
||||||
self.assertEquals(7, error.token.line_number)
|
|
||||||
|
|
||||||
error = alias_errors[2]
|
|
||||||
self.assertEquals(errors.EXTRA_GOOG_SCOPE_USAGE, error.code)
|
|
||||||
self.assertEquals(11, error.token.line_number)
|
|
||||||
|
|
||||||
|
|
||||||
_TEST_ALIAS_SCRIPT = """
|
|
||||||
goog.scope(function() {
|
|
||||||
var events = goog.events; // scope alias
|
|
||||||
var Event = events.
|
|
||||||
Event; // nested multiline scope alias
|
|
||||||
|
|
||||||
// This should not be registered as an aliased identifier because
|
|
||||||
// it appears before the alias.
|
|
||||||
var myClass = new MyClass();
|
|
||||||
|
|
||||||
var Component = goog.ui.Component; // scope alias
|
|
||||||
var MyClass = myproject.foo.MyClass; // scope alias
|
|
||||||
|
|
||||||
// Scope alias of non-Closurized namespace.
|
|
||||||
var NonClosurizedClass = aaa.bbb.NonClosurizedClass;
|
|
||||||
|
|
||||||
var component = new Component(Event.Something);
|
|
||||||
var nonClosurized = NonClosurizedClass();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A created namespace with a really long identifier.
|
|
||||||
* @type {events.Event.<Component,Array<MyClass>}
|
|
||||||
*/
|
|
||||||
Event.
|
|
||||||
MultilineIdentifier.
|
|
||||||
someMethod = function() {};
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEST_SCOPE_SCRIPT = """
|
|
||||||
function foo () {
|
|
||||||
// This goog.scope call is invalid.
|
|
||||||
goog.scope(function() {
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEST_MULTIPLE_SCOPE_SCRIPT = """
|
|
||||||
goog.scope(function() {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
|
|
||||||
function foo() {
|
|
||||||
var test = goog.scope; // We should not see goog.scope mentioned.
|
|
||||||
}
|
|
||||||
|
|
||||||
// This goog.scope invalid. There can be only one.
|
|
||||||
goog.scope(function() {
|
|
||||||
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Core methods for checking JS files for common style guide violations."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import checkerbase
|
|
||||||
from closure_linter import closurizednamespacesinfo
|
|
||||||
from closure_linter import javascriptlintrules
|
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_list('closurized_namespaces', '',
|
|
||||||
'Namespace prefixes, used for testing of'
|
|
||||||
'goog.provide/require')
|
|
||||||
flags.DEFINE_list('ignored_extra_namespaces', '',
|
|
||||||
'Fully qualified namespaces that should be not be reported '
|
|
||||||
'as extra by the linter.')
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptStyleChecker(checkerbase.CheckerBase):
|
|
||||||
"""Checker that applies JavaScriptLintRules."""
|
|
||||||
|
|
||||||
def __init__(self, state_tracker, error_handler):
|
|
||||||
"""Initialize an JavaScriptStyleChecker object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_tracker: State tracker.
|
|
||||||
error_handler: Error handler to pass all errors to.
|
|
||||||
"""
|
|
||||||
self._namespaces_info = None
|
|
||||||
self._alias_pass = None
|
|
||||||
if flags.FLAGS.closurized_namespaces:
|
|
||||||
self._namespaces_info = (
|
|
||||||
closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
flags.FLAGS.closurized_namespaces,
|
|
||||||
flags.FLAGS.ignored_extra_namespaces))
|
|
||||||
|
|
||||||
self._alias_pass = aliaspass.AliasPass(
|
|
||||||
flags.FLAGS.closurized_namespaces, error_handler)
|
|
||||||
|
|
||||||
checkerbase.CheckerBase.__init__(
|
|
||||||
self,
|
|
||||||
error_handler=error_handler,
|
|
||||||
lint_rules=javascriptlintrules.JavaScriptLintRules(
|
|
||||||
self._namespaces_info),
|
|
||||||
state_tracker=state_tracker)
|
|
||||||
|
|
||||||
def Check(self, start_token, limited_doc_checks=False, is_html=False,
|
|
||||||
stop_token=None):
|
|
||||||
"""Checks a token stream for lint warnings/errors.
|
|
||||||
|
|
||||||
Adds a separate pass for computing dependency information based on
|
|
||||||
goog.require and goog.provide statements prior to the main linting pass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The first token in the token stream.
|
|
||||||
limited_doc_checks: Whether to perform limited checks.
|
|
||||||
is_html: Whether this token stream is HTML.
|
|
||||||
stop_token: If given, checks should stop at this token.
|
|
||||||
"""
|
|
||||||
self._lint_rules.Initialize(self, limited_doc_checks, is_html)
|
|
||||||
|
|
||||||
self._state_tracker.DocFlagPass(start_token, self._error_handler)
|
|
||||||
|
|
||||||
if self._alias_pass:
|
|
||||||
self._alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
# To maximize the amount of errors that get reported before a parse error
|
|
||||||
# is displayed, don't run the dependency pass if a parse error exists.
|
|
||||||
if self._namespaces_info:
|
|
||||||
self._namespaces_info.Reset()
|
|
||||||
self._ExecutePass(start_token, self._DependencyPass, stop_token)
|
|
||||||
|
|
||||||
self._ExecutePass(start_token, self._LintPass, stop_token)
|
|
||||||
|
|
||||||
# If we have a stop_token, we didn't end up reading the whole file and,
|
|
||||||
# thus, don't call Finalize to do end-of-file checks.
|
|
||||||
if not stop_token:
|
|
||||||
self._lint_rules.Finalize(self._state_tracker)
|
|
||||||
|
|
||||||
def _DependencyPass(self, token):
|
|
||||||
"""Processes an individual token for dependency information.
|
|
||||||
|
|
||||||
Used to encapsulate the logic needed to process an individual token so that
|
|
||||||
it can be passed to _ExecutePass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to process.
|
|
||||||
"""
|
|
||||||
self._namespaces_info.ProcessToken(token, self._state_tracker)
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Base classes for writing checkers that operate on tokens."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'jacobr@google.com (Jacob Richman)')
|
|
||||||
|
|
||||||
from closure_linter import errorrules
|
|
||||||
from closure_linter.common import error
|
|
||||||
|
|
||||||
|
|
||||||
class LintRulesBase(object):
|
|
||||||
"""Base class for all classes defining the lint rules for a language."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__checker = None
|
|
||||||
|
|
||||||
def Initialize(self, checker, limited_doc_checks, is_html):
|
|
||||||
"""Initializes to prepare to check a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
checker: Class to report errors to.
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
is_html: Whether the file is an HTML file with extracted contents.
|
|
||||||
"""
|
|
||||||
self.__checker = checker
|
|
||||||
self._limited_doc_checks = limited_doc_checks
|
|
||||||
self._is_html = is_html
|
|
||||||
|
|
||||||
def _HandleError(self, code, message, token, position=None,
|
|
||||||
fix_data=None):
|
|
||||||
"""Call the HandleError function for the checker we are associated with."""
|
|
||||||
if errorrules.ShouldReportError(code):
|
|
||||||
self.__checker.HandleError(code, message, token, position, fix_data)
|
|
||||||
|
|
||||||
def _SetLimitedDocChecks(self, limited_doc_checks):
|
|
||||||
"""Sets whether doc checking is relaxed for this file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
"""
|
|
||||||
self._limited_doc_checks = limited_doc_checks
|
|
||||||
|
|
||||||
def CheckToken(self, token, parser_state):
|
|
||||||
"""Checks a token, given the current parser_state, for warnings and errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration.
|
|
||||||
parser_state: Object that indicates the parser state in the page.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
raise TypeError('Abstract method CheckToken not implemented')
|
|
||||||
|
|
||||||
def Finalize(self, parser_state):
|
|
||||||
"""Perform all checks that need to occur after all lines are processed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parser_state: State of the parser after parsing all tokens
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
raise TypeError('Abstract method Finalize not implemented')
|
|
||||||
|
|
||||||
|
|
||||||
class CheckerBase(object):
|
|
||||||
"""This class handles checking a LintRules object against a file."""
|
|
||||||
|
|
||||||
def __init__(self, error_handler, lint_rules, state_tracker):
|
|
||||||
"""Initialize a checker object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error_handler: Object that handles errors.
|
|
||||||
lint_rules: LintRules object defining lint errors given a token
|
|
||||||
and state_tracker object.
|
|
||||||
state_tracker: Object that tracks the current state in the token stream.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._error_handler = error_handler
|
|
||||||
self._lint_rules = lint_rules
|
|
||||||
self._state_tracker = state_tracker
|
|
||||||
|
|
||||||
self._has_errors = False
|
|
||||||
|
|
||||||
def HandleError(self, code, message, token, position=None,
|
|
||||||
fix_data=None):
|
|
||||||
"""Prints out the given error message including a line number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: The error code.
|
|
||||||
message: The error to print.
|
|
||||||
token: The token where the error occurred, or None if it was a file-wide
|
|
||||||
issue.
|
|
||||||
position: The position of the error, defaults to None.
|
|
||||||
fix_data: Metadata used for fixing the error.
|
|
||||||
"""
|
|
||||||
self._has_errors = True
|
|
||||||
self._error_handler.HandleError(
|
|
||||||
error.Error(code, message, token, position, fix_data))
|
|
||||||
|
|
||||||
def HasErrors(self):
|
|
||||||
"""Returns true if the style checker has found any errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the style checker has found any errors.
|
|
||||||
"""
|
|
||||||
return self._has_errors
|
|
||||||
|
|
||||||
def Check(self, start_token, limited_doc_checks=False, is_html=False,
|
|
||||||
stop_token=None):
|
|
||||||
"""Checks a token stream, reporting errors to the error reporter.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: First token in token stream.
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
is_html: Whether the file being checked is an HTML file with extracted
|
|
||||||
contents.
|
|
||||||
stop_token: If given, check should stop at this token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._lint_rules.Initialize(self, limited_doc_checks, is_html)
|
|
||||||
self._ExecutePass(start_token, self._LintPass, stop_token=stop_token)
|
|
||||||
self._lint_rules.Finalize(self._state_tracker)
|
|
||||||
|
|
||||||
def _LintPass(self, token):
|
|
||||||
"""Checks an individual token for lint warnings/errors.
|
|
||||||
|
|
||||||
Used to encapsulate the logic needed to check an individual token so that it
|
|
||||||
can be passed to _ExecutePass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to check.
|
|
||||||
"""
|
|
||||||
self._lint_rules.CheckToken(token, self._state_tracker)
|
|
||||||
|
|
||||||
def _ExecutePass(self, token, pass_function, stop_token=None):
|
|
||||||
"""Calls the given function for every token in the given token stream.
|
|
||||||
|
|
||||||
As each token is passed to the given function, state is kept up to date and,
|
|
||||||
depending on the error_trace flag, errors are either caught and reported, or
|
|
||||||
allowed to bubble up so developers can see the full stack trace. If a parse
|
|
||||||
error is specified, the pass will proceed as normal until the token causing
|
|
||||||
the parse error is reached.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
pass_function: The function to call for each token in the token stream.
|
|
||||||
stop_token: The last token to check (if given).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If any error occurred while calling the given function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._state_tracker.Reset()
|
|
||||||
while token:
|
|
||||||
# When we are looking at a token and decided to delete the whole line, we
|
|
||||||
# will delete all of them in the "HandleToken()" below. So the current
|
|
||||||
# token and subsequent ones may already be deleted here. The way we
|
|
||||||
# delete a token does not wipe out the previous and next pointers of the
|
|
||||||
# deleted token. So we need to check the token itself to make sure it is
|
|
||||||
# not deleted.
|
|
||||||
if not token.is_deleted:
|
|
||||||
# End the pass at the stop token
|
|
||||||
if stop_token and token is stop_token:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._state_tracker.HandleToken(
|
|
||||||
token, self._state_tracker.GetLastNonSpaceToken())
|
|
||||||
pass_function(token)
|
|
||||||
self._state_tracker.HandleAfterToken(token)
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
@@ -1,578 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Logic for computing dependency information for closurized JavaScript files.
|
|
||||||
|
|
||||||
Closurized JavaScript files express dependencies using goog.require and
|
|
||||||
goog.provide statements. In order for the linter to detect when a statement is
|
|
||||||
missing or unnecessary, all identifiers in the JavaScript file must first be
|
|
||||||
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=g-bad-name
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
DEFAULT_EXTRA_NAMESPACES = [
|
|
||||||
'goog.testing.asserts',
|
|
||||||
'goog.testing.jsunit',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UsedNamespace(object):
|
|
||||||
"""A type for information about a used namespace."""
|
|
||||||
|
|
||||||
def __init__(self, namespace, identifier, token, alias_definition):
|
|
||||||
"""Initializes the instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
namespace: the namespace of an identifier used in the file
|
|
||||||
identifier: the complete identifier
|
|
||||||
token: the token that uses the namespace
|
|
||||||
alias_definition: a boolean stating whether the namespace is only to used
|
|
||||||
for an alias definition and should not be required.
|
|
||||||
"""
|
|
||||||
self.namespace = namespace
|
|
||||||
self.identifier = identifier
|
|
||||||
self.token = token
|
|
||||||
self.alias_definition = alias_definition
|
|
||||||
|
|
||||||
def GetLine(self):
|
|
||||||
return self.token.line_number
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'UsedNamespace(%s)' % ', '.join(
|
|
||||||
['%s=%s' % (k, repr(v)) for k, v in self.__dict__.iteritems()])
|
|
||||||
|
|
||||||
|
|
||||||
class ClosurizedNamespacesInfo(object):
|
|
||||||
"""Dependency information for closurized JavaScript files.
|
|
||||||
|
|
||||||
Processes token streams for dependency creation or usage and provides logic
|
|
||||||
for determining if a given require or provide statement is unnecessary or if
|
|
||||||
there are missing require or provide statements.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, closurized_namespaces, ignored_extra_namespaces):
|
|
||||||
"""Initializes an instance the ClosurizedNamespacesInfo class.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
closurized_namespaces: A list of namespace prefixes that should be
|
|
||||||
processed for dependency information. Non-matching namespaces are
|
|
||||||
ignored.
|
|
||||||
ignored_extra_namespaces: A list of namespaces that should not be reported
|
|
||||||
as extra regardless of whether they are actually used.
|
|
||||||
"""
|
|
||||||
self._closurized_namespaces = closurized_namespaces
|
|
||||||
self._ignored_extra_namespaces = (ignored_extra_namespaces +
|
|
||||||
DEFAULT_EXTRA_NAMESPACES)
|
|
||||||
self.Reset()
|
|
||||||
|
|
||||||
def Reset(self):
|
|
||||||
"""Resets the internal state to prepare for processing a new file."""
|
|
||||||
|
|
||||||
# A list of goog.provide tokens in the order they appeared in the file.
|
|
||||||
self._provide_tokens = []
|
|
||||||
|
|
||||||
# A list of goog.require tokens in the order they appeared in the file.
|
|
||||||
self._require_tokens = []
|
|
||||||
|
|
||||||
# Namespaces that are already goog.provided.
|
|
||||||
self._provided_namespaces = []
|
|
||||||
|
|
||||||
# Namespaces that are already goog.required.
|
|
||||||
self._required_namespaces = []
|
|
||||||
|
|
||||||
# Note that created_namespaces and used_namespaces contain both namespaces
|
|
||||||
# and identifiers because there are many existing cases where a method or
|
|
||||||
# constant is provided directly instead of its namespace. Ideally, these
|
|
||||||
# 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, the second is the identifier itself and the third is
|
|
||||||
# the line number where it's created.
|
|
||||||
self._created_namespaces = []
|
|
||||||
|
|
||||||
# A list of UsedNamespace instances.
|
|
||||||
self._used_namespaces = []
|
|
||||||
|
|
||||||
# A list of seemingly-unnecessary namespaces that are goog.required() and
|
|
||||||
# annotated with @suppress {extraRequire}.
|
|
||||||
self._suppressed_requires = []
|
|
||||||
|
|
||||||
# A list of goog.provide tokens which are duplicates.
|
|
||||||
self._duplicate_provide_tokens = []
|
|
||||||
|
|
||||||
# A list of goog.require tokens which are duplicates.
|
|
||||||
self._duplicate_require_tokens = []
|
|
||||||
|
|
||||||
# Whether this file is in a goog.scope. Someday, we may add support
|
|
||||||
# for checking scopified namespaces, but for now let's just fail
|
|
||||||
# in a more reasonable way.
|
|
||||||
self._scopified_file = False
|
|
||||||
|
|
||||||
# TODO(user): Handle the case where there are 2 different requires
|
|
||||||
# that can satisfy the same dependency, but only one is necessary.
|
|
||||||
|
|
||||||
def GetProvidedNamespaces(self):
|
|
||||||
"""Returns the namespaces which are already provided by this file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of strings where each string is a 'namespace' corresponding to an
|
|
||||||
existing goog.provide statement in the file being checked.
|
|
||||||
"""
|
|
||||||
return set(self._provided_namespaces)
|
|
||||||
|
|
||||||
def GetRequiredNamespaces(self):
|
|
||||||
"""Returns the namespaces which are already required by this file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of strings where each string is a 'namespace' corresponding to an
|
|
||||||
existing goog.require statement in the file being checked.
|
|
||||||
"""
|
|
||||||
return set(self._required_namespaces)
|
|
||||||
|
|
||||||
def IsExtraProvide(self, token):
|
|
||||||
"""Returns whether the given goog.provide token is unnecessary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A goog.provide token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the given token corresponds to an unnecessary goog.provide
|
|
||||||
statement, otherwise False.
|
|
||||||
"""
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
if self.GetClosurizedNamespace(namespace) is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token in self._duplicate_provide_tokens:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO(user): There's probably a faster way to compute this.
|
|
||||||
for created_namespace, created_identifier, _ in self._created_namespaces:
|
|
||||||
if namespace == created_namespace or namespace == created_identifier:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def IsExtraRequire(self, token):
|
|
||||||
"""Returns whether the given goog.require token is unnecessary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A goog.require token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the given token corresponds to an unnecessary goog.require
|
|
||||||
statement, otherwise False.
|
|
||||||
"""
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
if self.GetClosurizedNamespace(namespace) is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if namespace in self._ignored_extra_namespaces:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token in self._duplicate_require_tokens:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if namespace in self._suppressed_requires:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If the namespace contains a component that is initial caps, then that
|
|
||||||
# must be the last component of the namespace.
|
|
||||||
parts = namespace.split('.')
|
|
||||||
if len(parts) > 1 and parts[-2][0].isupper():
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO(user): There's probably a faster way to compute this.
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
if (not ns.alias_definition and (
|
|
||||||
namespace == ns.namespace or namespace == ns.identifier)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def GetMissingProvides(self):
|
|
||||||
"""Returns the dict of missing provided namespaces for the current file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
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 and
|
|
||||||
namespace not in missing_provides):
|
|
||||||
missing_provides[namespace] = line_number
|
|
||||||
|
|
||||||
return missing_provides
|
|
||||||
|
|
||||||
def GetMissingRequires(self):
|
|
||||||
"""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.
|
|
||||||
goog.require statements can satisfy the identifier by requiring either the
|
|
||||||
namespace of the identifier or the identifier itself. goog.provide
|
|
||||||
statements can satisfy the identifier by providing the namespace of the
|
|
||||||
identifier. A created identifier can only satisfy the used identifier if
|
|
||||||
it matches it exactly (necessary since things can be defined on a
|
|
||||||
namespace in more than one file). Note that provided namespaces should be
|
|
||||||
a subset of created namespaces, but we check both because in some cases we
|
|
||||||
can't always detect the creation of the namespace.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
external_dependencies = set(self._required_namespaces)
|
|
||||||
|
|
||||||
# Assume goog namespace is always available.
|
|
||||||
external_dependencies.add('goog')
|
|
||||||
# goog.module is treated as a builtin, too (for goog.module.get).
|
|
||||||
external_dependencies.add('goog.module')
|
|
||||||
|
|
||||||
created_identifiers = set()
|
|
||||||
for unused_namespace, identifier, unused_line_number in (
|
|
||||||
self._created_namespaces):
|
|
||||||
created_identifiers.add(identifier)
|
|
||||||
|
|
||||||
missing_requires = dict()
|
|
||||||
illegal_alias_statements = dict()
|
|
||||||
|
|
||||||
def ShouldRequireNamespace(namespace, identifier):
|
|
||||||
"""Checks if a namespace would normally be required."""
|
|
||||||
return (
|
|
||||||
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 and
|
|
||||||
namespace not in missing_requires)
|
|
||||||
|
|
||||||
# First check all the used identifiers where we know that their namespace
|
|
||||||
# needs to be provided (unless they are optional).
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
namespace = ns.namespace
|
|
||||||
identifier = ns.identifier
|
|
||||||
if (not ns.alias_definition and
|
|
||||||
ShouldRequireNamespace(namespace, identifier)):
|
|
||||||
missing_requires[namespace] = ns.GetLine()
|
|
||||||
|
|
||||||
# Now that all required namespaces are known, we can check if the alias
|
|
||||||
# definitions (that are likely being used for typeannotations that don't
|
|
||||||
# need explicit goog.require statements) are already covered. If not
|
|
||||||
# the user shouldn't use the alias.
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
if (not ns.alias_definition or
|
|
||||||
not ShouldRequireNamespace(ns.namespace, ns.identifier)):
|
|
||||||
continue
|
|
||||||
if self._FindNamespace(ns.identifier, self._provided_namespaces,
|
|
||||||
created_identifiers, external_dependencies,
|
|
||||||
missing_requires):
|
|
||||||
continue
|
|
||||||
namespace = ns.identifier.rsplit('.', 1)[0]
|
|
||||||
illegal_alias_statements[namespace] = ns.token
|
|
||||||
|
|
||||||
return missing_requires, illegal_alias_statements
|
|
||||||
|
|
||||||
def _FindNamespace(self, identifier, *namespaces_list):
|
|
||||||
"""Finds the namespace of an identifier given a list of other namespaces.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: An identifier whose parent needs to be defined.
|
|
||||||
e.g. for goog.bar.foo we search something that provides
|
|
||||||
goog.bar.
|
|
||||||
*namespaces_list: var args of iterables of namespace identifiers
|
|
||||||
Returns:
|
|
||||||
The namespace that the given identifier is part of or None.
|
|
||||||
"""
|
|
||||||
identifier = identifier.rsplit('.', 1)[0]
|
|
||||||
identifier_prefix = identifier + '.'
|
|
||||||
for namespaces in namespaces_list:
|
|
||||||
for namespace in namespaces:
|
|
||||||
if namespace == identifier or namespace.startswith(identifier_prefix):
|
|
||||||
return namespace
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _IsPrivateIdentifier(self, identifier):
|
|
||||||
"""Returns whether the given identifier is private."""
|
|
||||||
pieces = identifier.split('.')
|
|
||||||
for piece in pieces:
|
|
||||||
if piece.endswith('_'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def IsFirstProvide(self, token):
|
|
||||||
"""Returns whether token is the first provide token."""
|
|
||||||
return self._provide_tokens and token == self._provide_tokens[0]
|
|
||||||
|
|
||||||
def IsFirstRequire(self, token):
|
|
||||||
"""Returns whether token is the first require token."""
|
|
||||||
return self._require_tokens and token == self._require_tokens[0]
|
|
||||||
|
|
||||||
def IsLastProvide(self, token):
|
|
||||||
"""Returns whether token is the last provide token."""
|
|
||||||
return self._provide_tokens and token == self._provide_tokens[-1]
|
|
||||||
|
|
||||||
def IsLastRequire(self, token):
|
|
||||||
"""Returns whether token is the last require token."""
|
|
||||||
return self._require_tokens and token == self._require_tokens[-1]
|
|
||||||
|
|
||||||
def ProcessToken(self, token, state_tracker):
|
|
||||||
"""Processes the given token for dependency information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to process.
|
|
||||||
state_tracker: The JavaScript state tracker.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Note that this method is in the critical path for the linter and has been
|
|
||||||
# optimized for performance in the following ways:
|
|
||||||
# - Tokens are checked by type first to minimize the number of function
|
|
||||||
# calls necessary to determine if action needs to be taken for the token.
|
|
||||||
# - The most common tokens types are checked for first.
|
|
||||||
# - The number of function calls has been minimized (thus the length of this
|
|
||||||
# function.
|
|
||||||
|
|
||||||
if token.type == TokenType.IDENTIFIER:
|
|
||||||
# TODO(user): Consider saving the whole identifier in metadata.
|
|
||||||
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
|
|
||||||
# identifier, but it was not the first token of the identifier.
|
|
||||||
return
|
|
||||||
|
|
||||||
# In the odd case that a goog.require is encountered inside a function,
|
|
||||||
# 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.GetStringAfterToken(token)
|
|
||||||
if namespace in self._required_namespaces:
|
|
||||||
self._duplicate_require_tokens.append(token)
|
|
||||||
else:
|
|
||||||
self._required_namespaces.append(namespace)
|
|
||||||
|
|
||||||
# If there is a suppression for the require, add a usage for it so it
|
|
||||||
# gets treated as a regular goog.require (i.e. still gets sorted).
|
|
||||||
if self._HasSuppression(state_tracker, 'extraRequire'):
|
|
||||||
self._suppressed_requires.append(namespace)
|
|
||||||
self._AddUsedNamespace(state_tracker, namespace, token)
|
|
||||||
|
|
||||||
elif token.string == 'goog.provide':
|
|
||||||
self._provide_tokens.append(token)
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
if namespace in self._provided_namespaces:
|
|
||||||
self._duplicate_provide_tokens.append(token)
|
|
||||||
else:
|
|
||||||
self._provided_namespaces.append(namespace)
|
|
||||||
|
|
||||||
# If there is a suppression for the provide, add a creation for it so it
|
|
||||||
# gets treated as a regular goog.provide (i.e. still gets sorted).
|
|
||||||
if self._HasSuppression(state_tracker, 'extraProvide'):
|
|
||||||
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
|
|
||||||
elif (token.string == 'goog.module.get' and
|
|
||||||
not self._HasSuppression(state_tracker, 'extraRequire')):
|
|
||||||
# Cannot use _AddUsedNamespace as this is not an identifier, but
|
|
||||||
# already the entire namespace that's required.
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
namespace = UsedNamespace(namespace, namespace, token,
|
|
||||||
alias_definition=False)
|
|
||||||
self._used_namespaces.append(namespace)
|
|
||||||
if jsdoc and jsdoc.HasFlag('typedef'):
|
|
||||||
self._AddCreatedNamespace(state_tracker, whole_identifier_string,
|
|
||||||
token.line_number,
|
|
||||||
namespace=self.GetClosurizedNamespace(
|
|
||||||
whole_identifier_string))
|
|
||||||
else:
|
|
||||||
is_alias_definition = (token.metadata and
|
|
||||||
token.metadata.is_alias_definition)
|
|
||||||
self._AddUsedNamespace(state_tracker, whole_identifier_string,
|
|
||||||
token, is_alias_definition)
|
|
||||||
|
|
||||||
elif token.type == TokenType.SIMPLE_LVALUE:
|
|
||||||
identifier = token.values['identifier']
|
|
||||||
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)
|
|
||||||
elif namespace and namespace != 'goog':
|
|
||||||
self._AddCreatedNamespace(state_tracker, identifier,
|
|
||||||
token.line_number, namespace=namespace)
|
|
||||||
|
|
||||||
elif token.type == TokenType.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
flag_type = flag.flag_type
|
|
||||||
if flag and flag.HasType() and flag.jstype:
|
|
||||||
is_interface = state_tracker.GetDocComment().HasFlag('interface')
|
|
||||||
if flag_type == 'implements' or (flag_type == 'extends'
|
|
||||||
and is_interface):
|
|
||||||
identifier = flag.jstype.alias or flag.jstype.identifier
|
|
||||||
self._AddUsedNamespace(state_tracker, identifier, token)
|
|
||||||
# Since we process doctypes only for implements and extends, the
|
|
||||||
# type is a simple one and we don't need any iteration for subtypes.
|
|
||||||
|
|
||||||
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
|
|
||||||
not added.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if not namespace:
|
|
||||||
namespace = identifier
|
|
||||||
|
|
||||||
if self._HasSuppression(state_tracker, 'missingProvide'):
|
|
||||||
return
|
|
||||||
|
|
||||||
self._created_namespaces.append([namespace, identifier, line_number])
|
|
||||||
|
|
||||||
def _AddUsedNamespace(self, state_tracker, identifier, token,
|
|
||||||
is_alias_definition=False):
|
|
||||||
"""Adds the namespace of an identifier to the list of used namespaces.
|
|
||||||
|
|
||||||
If the identifier is annotated with a 'missingRequire' suppression, it is
|
|
||||||
not added.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_tracker: The JavaScriptStateTracker instance.
|
|
||||||
identifier: An identifier which has been used.
|
|
||||||
token: The token in which the namespace is used.
|
|
||||||
is_alias_definition: If the used namespace is part of an alias_definition.
|
|
||||||
Aliased symbols need their parent namespace to be available, if it is
|
|
||||||
not yet required through another symbol, an error will be thrown.
|
|
||||||
"""
|
|
||||||
if self._HasSuppression(state_tracker, 'missingRequire'):
|
|
||||||
return
|
|
||||||
|
|
||||||
namespace = self.GetClosurizedNamespace(identifier)
|
|
||||||
# b/5362203 If its a variable in scope then its not a required namespace.
|
|
||||||
if namespace and not state_tracker.IsVariableInScope(namespace):
|
|
||||||
namespace = UsedNamespace(namespace, identifier, token,
|
|
||||||
is_alias_definition)
|
|
||||||
self._used_namespaces.append(namespace)
|
|
||||||
|
|
||||||
def _HasSuppression(self, state_tracker, suppression):
|
|
||||||
jsdoc = state_tracker.GetDocComment()
|
|
||||||
return jsdoc and suppression in jsdoc.suppressions
|
|
||||||
|
|
||||||
def GetClosurizedNamespace(self, identifier):
|
|
||||||
"""Given an identifier, returns the namespace that identifier is from.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: The identifier to extract a namespace from.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The namespace the given identifier resides in, or None if one could not
|
|
||||||
be found.
|
|
||||||
"""
|
|
||||||
if identifier.startswith('goog.global'):
|
|
||||||
# Ignore goog.global, since it is, by definition, global.
|
|
||||||
return None
|
|
||||||
|
|
||||||
parts = identifier.split('.')
|
|
||||||
for namespace in self._closurized_namespaces:
|
|
||||||
if not identifier.startswith(namespace + '.'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# The namespace for a class is the shortest prefix ending in a class
|
|
||||||
# name, which starts with a capital letter but is not a capitalized word.
|
|
||||||
#
|
|
||||||
# We ultimately do not want to allow requiring or providing of inner
|
|
||||||
# classes/enums. Instead, a file should provide only the top-level class
|
|
||||||
# and users should require only that.
|
|
||||||
namespace = []
|
|
||||||
for part in parts:
|
|
||||||
if part == 'prototype' or part.isupper():
|
|
||||||
return '.'.join(namespace)
|
|
||||||
namespace.append(part)
|
|
||||||
if part[0].isupper():
|
|
||||||
return '.'.join(namespace)
|
|
||||||
|
|
||||||
# At this point, we know there's no class or enum, so the namespace is
|
|
||||||
# just the identifier with the last part removed. With the exception of
|
|
||||||
# apply, inherits, and call, which should also be stripped.
|
|
||||||
if parts[-1] in ('apply', 'inherits', 'call'):
|
|
||||||
parts.pop()
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
# If the last part ends with an underscore, it is a private variable,
|
|
||||||
# method, or enum. The namespace is whatever is before it.
|
|
||||||
if parts and parts[-1].endswith('_'):
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
return '.'.join(parts)
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,873 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Unit tests for ClosurizedNamespacesInfo."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import closurizednamespacesinfo
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
# pylint: disable=g-bad-name
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
def _ToLineDict(illegal_alias_stmts):
|
|
||||||
"""Replaces tokens with the respective line number."""
|
|
||||||
return {k: v.line_number for k, v in illegal_alias_stmts.iteritems()}
|
|
||||||
|
|
||||||
|
|
||||||
class ClosurizedNamespacesInfoTest(googletest.TestCase):
|
|
||||||
"""Tests for ClosurizedNamespacesInfo."""
|
|
||||||
|
|
||||||
_test_cases = {
|
|
||||||
'goog.global.anything': None,
|
|
||||||
'package.CONSTANT': 'package',
|
|
||||||
'package.methodName': 'package',
|
|
||||||
'package.subpackage.methodName': 'package.subpackage',
|
|
||||||
'package.subpackage.methodName.apply': 'package.subpackage',
|
|
||||||
'package.ClassName.something': 'package.ClassName',
|
|
||||||
'package.ClassName.Enum.VALUE.methodName': 'package.ClassName',
|
|
||||||
'package.ClassName.CONSTANT': 'package.ClassName',
|
|
||||||
'package.namespace.CONSTANT.methodName': 'package.namespace',
|
|
||||||
'package.ClassName.inherits': 'package.ClassName',
|
|
||||||
'package.ClassName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.methodName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.methodName.call': 'package.ClassName',
|
|
||||||
'package.ClassName.prototype.methodName': 'package.ClassName',
|
|
||||||
'package.ClassName.privateMethod_': 'package.ClassName',
|
|
||||||
'package.className.privateProperty_': 'package.className',
|
|
||||||
'package.className.privateProperty_.methodName': 'package.className',
|
|
||||||
'package.ClassName.PrivateEnum_': 'package.ClassName',
|
|
||||||
'package.ClassName.prototype.methodName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.property.subProperty': 'package.ClassName',
|
|
||||||
'package.className.prototype.something.somethingElse': 'package.className'
|
|
||||||
}
|
|
||||||
|
|
||||||
def testGetClosurizedNamespace(self):
|
|
||||||
"""Tests that the correct namespace is returned for various identifiers."""
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=['package'], ignored_extra_namespaces=[])
|
|
||||||
for identifier, expected_namespace in self._test_cases.items():
|
|
||||||
actual_namespace = namespaces_info.GetClosurizedNamespace(identifier)
|
|
||||||
self.assertEqual(
|
|
||||||
expected_namespace,
|
|
||||||
actual_namespace,
|
|
||||||
'expected namespace "' + str(expected_namespace) +
|
|
||||||
'" for identifier "' + str(identifier) + '" but was "' +
|
|
||||||
str(actual_namespace) + '"')
|
|
||||||
|
|
||||||
def testIgnoredExtraNamespaces(self):
|
|
||||||
"""Tests that ignored_extra_namespaces are ignored."""
|
|
||||||
token = self._GetRequireTokens('package.Something')
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=['package'],
|
|
||||||
ignored_extra_namespaces=['package.Something'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be valid since it is in ignored namespaces.')
|
|
||||||
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
['package'], [])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be invalid since it is not in ignored namespaces.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_created(self):
|
|
||||||
"""Tests that provides for created namespaces are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_createdIdentifier(self):
|
|
||||||
"""Tests that provides for created identifiers are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo.methodName\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notCreated(self):
|
|
||||||
"""Tests that provides for non-created namespaces are extra."""
|
|
||||||
input_lines = ['goog.provide(\'package.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is not created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notCreatedMultipartClosurizedNamespace(self):
|
|
||||||
"""Tests that provides for non-created namespaces are extra."""
|
|
||||||
input_lines = ['goog.provide(\'multi.part.namespace.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['multi.part'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is not created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_duplicate(self):
|
|
||||||
"""Tests that providing a namespace twice makes the second one extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
# Advance to the second goog.provide token.
|
|
||||||
token = tokenutil.Search(token.next, TokenType.IDENTIFIER)
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is already provided.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notClosurized(self):
|
|
||||||
"""Tests that provides of non-closurized namespaces are not extra."""
|
|
||||||
input_lines = ['goog.provide(\'notclosurized.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is not closurized.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_used(self):
|
|
||||||
"""Tests that requires for used namespaces are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'var x = package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_usedIdentifier(self):
|
|
||||||
"""Tests that requires for used methods on classes are extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.methodName\');',
|
|
||||||
'var x = package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should require the package, not the method specifically.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notUsed(self):
|
|
||||||
"""Tests that requires for unused namespaces are extra."""
|
|
||||||
input_lines = ['goog.require(\'package.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be extra since it is not used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notUsedMultiPartClosurizedNamespace(self):
|
|
||||||
"""Tests unused require with multi-part closurized namespaces."""
|
|
||||||
|
|
||||||
input_lines = ['goog.require(\'multi.part.namespace.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['multi.part'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be extra since it is not used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notClosurized(self):
|
|
||||||
"""Tests that requires of non-closurized namespaces are not extra."""
|
|
||||||
input_lines = ['goog.require(\'notclosurized.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is not closurized.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_objectOnClass(self):
|
|
||||||
"""Tests that requiring an object on a class is extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.Enum\');',
|
|
||||||
'var x = package.Foo.Enum.VALUE1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'The whole class, not the object, should be required.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_constantOnClass(self):
|
|
||||||
"""Tests that requiring a constant on a class is extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.CONSTANT\');',
|
|
||||||
'var x = package.Foo.CONSTANT',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'The class, not the constant, should be required.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_constantNotOnClass(self):
|
|
||||||
"""Tests that requiring a constant not on a class is OK."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.subpackage.CONSTANT\');',
|
|
||||||
'var x = package.subpackage.CONSTANT',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Constants can be required except on classes.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_methodNotOnClass(self):
|
|
||||||
"""Tests that requiring a method not on a class is OK."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.subpackage.method\');',
|
|
||||||
'var x = package.subpackage.method()',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Methods can be required except on classes.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_defaults(self):
|
|
||||||
"""Tests that there are no warnings about extra requires for test utils"""
|
|
||||||
input_lines = ['goog.require(\'goog.testing.jsunit\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['goog'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is for testing.')
|
|
||||||
|
|
||||||
def testGetMissingProvides_provided(self):
|
|
||||||
"""Tests that provided functions don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_providedIdentifier(self):
|
|
||||||
"""Tests that provided identifiers don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo.methodName\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_providedParentIdentifier(self):
|
|
||||||
"""Tests that provided identifiers on a class don't cause a missing provide
|
|
||||||
on objects attached to that class."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.foo.ClassName\');',
|
|
||||||
'package.foo.ClassName.methodName = function() {};',
|
|
||||||
'package.foo.ClassName.ObjectName = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_unprovided(self):
|
|
||||||
"""Tests that unprovided functions cause a missing provide."""
|
|
||||||
input_lines = ['package.Foo = function() {};']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
self.assertEquals(1, len(missing_provides))
|
|
||||||
missing_provide = missing_provides.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_provide[0])
|
|
||||||
self.assertEquals(1, missing_provide[1])
|
|
||||||
|
|
||||||
def testGetMissingProvides_privatefunction(self):
|
|
||||||
"""Tests that unprovided private functions don't cause a missing provide."""
|
|
||||||
input_lines = ['package.Foo_ = function() {};']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_required(self):
|
|
||||||
"""Tests that required namespaces don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingRequires_required(self):
|
|
||||||
"""Tests that required namespaces don't cause a missing require."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredIdentifier(self):
|
|
||||||
"""Tests that required namespaces satisfy identifiers on that namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredNamespace(self):
|
|
||||||
"""Tests that required namespaces satisfy the namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.soy.fooTemplate\');',
|
|
||||||
'render(package.soy.fooTemplate);'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredParentClass(self):
|
|
||||||
"""Tests that requiring a parent class of an object is sufficient to prevent
|
|
||||||
a missing require on that object."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();',
|
|
||||||
'package.Foo.methodName(package.Foo.ObjectName);'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_unrequired(self):
|
|
||||||
"""Tests that unrequired namespaces cause a missing require."""
|
|
||||||
input_lines = ['package.Foo();']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires))
|
|
||||||
missing_req = missing_requires.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_req[0])
|
|
||||||
self.assertEquals(1, missing_req[1])
|
|
||||||
|
|
||||||
def testGetMissingRequires_provided(self):
|
|
||||||
"""Tests that provided namespaces satisfy identifiers on that namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_created(self):
|
|
||||||
"""Tests that created namespaces do not satisfy usage of an identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo = function();',
|
|
||||||
'package.Foo.methodName();',
|
|
||||||
'package.Foo.anotherMethodName1();',
|
|
||||||
'package.Foo.anotherMethodName2();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires))
|
|
||||||
missing_require = missing_requires.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_require[0])
|
|
||||||
# Make sure line number of first occurrence is reported
|
|
||||||
self.assertEquals(2, missing_require[1])
|
|
||||||
|
|
||||||
def testGetMissingRequires_createdIdentifier(self):
|
|
||||||
"""Tests that created identifiers satisfy usage of the identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo.methodName = function();',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_implements(self):
|
|
||||||
"""Tests that a parametrized type requires the correct identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'/** @constructor @implements {package.Bar<T>} */',
|
|
||||||
'package.Foo = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertItemsEqual({'package.Bar': 1}, missing_requires)
|
|
||||||
|
|
||||||
def testGetMissingRequires_objectOnClass(self):
|
|
||||||
"""Tests that we should require a class, not the object on the class."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.Enum\');',
|
|
||||||
'var x = package.Foo.Enum.VALUE1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires),
|
|
||||||
'The whole class, not the object, should be required.')
|
|
||||||
|
|
||||||
def testGetMissingRequires_variableWithSameName(self):
|
|
||||||
"""Tests that we should not goog.require variables and parameters.
|
|
||||||
|
|
||||||
b/5362203 Variables in scope are not missing namespaces.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'Foo\');',
|
|
||||||
'Foo.A = function();',
|
|
||||||
'Foo.A.prototype.method = function(ab) {',
|
|
||||||
' if (ab) {',
|
|
||||||
' var docs;',
|
|
||||||
' var lvalue = new Obj();',
|
|
||||||
' // Variable in scope hence not goog.require here.',
|
|
||||||
' docs.foo.abc = 1;',
|
|
||||||
' lvalue.next();',
|
|
||||||
' }',
|
|
||||||
' // Since js is function scope this should also not goog.require.',
|
|
||||||
' docs.foo.func();',
|
|
||||||
' // Its not a variable in scope hence goog.require.',
|
|
||||||
' dummy.xyz.reset();',
|
|
||||||
' return this.method2();',
|
|
||||||
'};',
|
|
||||||
'Foo.A.prototype.method1 = function(docs, abcd, xyz) {',
|
|
||||||
' // Parameter hence not goog.require.',
|
|
||||||
' docs.nodes.length = 2;',
|
|
||||||
' lvalue.abc.reset();',
|
|
||||||
'};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['Foo',
|
|
||||||
'docs',
|
|
||||||
'lvalue',
|
|
||||||
'dummy'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(2, len(missing_requires))
|
|
||||||
self.assertItemsEqual(
|
|
||||||
{'dummy.xyz': 14,
|
|
||||||
'lvalue.abc': 20}, missing_requires)
|
|
||||||
|
|
||||||
def testIsFirstProvide(self):
|
|
||||||
"""Tests operation of the isFirstProvide method."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
self.assertTrue(namespaces_info.IsFirstProvide(token))
|
|
||||||
|
|
||||||
def testGetWholeIdentifierString(self):
|
|
||||||
"""Tests that created identifiers satisfy usage of the identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo.',
|
|
||||||
' veryLong.',
|
|
||||||
' identifier;'
|
|
||||||
]
|
|
||||||
|
|
||||||
token = testutil.TokenizeSource(input_lines)
|
|
||||||
|
|
||||||
self.assertEquals('package.Foo.veryLong.identifier',
|
|
||||||
tokenutil.GetIdentifierForToken(token))
|
|
||||||
|
|
||||||
self.assertEquals(None,
|
|
||||||
tokenutil.GetIdentifierForToken(token.next))
|
|
||||||
|
|
||||||
def testScopified(self):
|
|
||||||
"""Tests that a goog.scope call is noticed."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertTrue(namespaces_info._scopified_file)
|
|
||||||
|
|
||||||
def testScope_unusedAlias(self):
|
|
||||||
"""Tests that an unused alias symbol is illegal."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_usedMultilevelAlias(self):
|
|
||||||
"""Tests that an used alias symbol in a deep namespace is ok."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.Events\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.Events.DeepNamespace.Event;',
|
|
||||||
'Event();',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_usedAlias(self):
|
|
||||||
"""Tests that aliased symbols result in correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'var dom = goog.dom;',
|
|
||||||
'Event(dom.classes.get);',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
self.assertEquals({'goog.dom.classes': 4, 'goog.events.Event': 4},
|
|
||||||
missing_requires)
|
|
||||||
|
|
||||||
def testModule_alias(self):
|
|
||||||
"""Tests that goog.module style aliases are supported."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.module(\'test.module\');',
|
|
||||||
'var Unused = goog.require(\'goog.Unused\');',
|
|
||||||
'var AliasedClass = goog.require(\'goog.AliasedClass\');',
|
|
||||||
'var x = new AliasedClass();',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
namespaceToken = self._GetRequireTokens('goog.AliasedClass')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(namespaceToken),
|
|
||||||
'AliasedClass should be marked as used')
|
|
||||||
unusedToken = self._GetRequireTokens('goog.Unused')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(unusedToken),
|
|
||||||
'Unused should be marked as not used')
|
|
||||||
|
|
||||||
def testModule_aliasInScope(self):
|
|
||||||
"""Tests that goog.module style aliases are supported."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.module(\'test.module\');',
|
|
||||||
'var AliasedClass = goog.require(\'goog.AliasedClass\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var x = new AliasedClass();',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
namespaceToken = self._GetRequireTokens('goog.AliasedClass')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(namespaceToken),
|
|
||||||
'AliasedClass should be marked as used')
|
|
||||||
|
|
||||||
def testModule_getAlwaysProvided(self):
|
|
||||||
"""Tests that goog.module.get is recognized as a built-in."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'test.MyClass\');',
|
|
||||||
'goog.require(\'goog.someModule\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var someModule = goog.module.get(\'goog.someModule\');',
|
|
||||||
'test.MyClass = function() {};',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertEquals({}, namespaces_info.GetMissingRequires()[0])
|
|
||||||
|
|
||||||
def testModule_requireForGet(self):
|
|
||||||
"""Tests that goog.module.get needs a goog.require call."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'test.MyClass\');',
|
|
||||||
'function foo() {',
|
|
||||||
' var someModule = goog.module.get(\'goog.someModule\');',
|
|
||||||
' someModule.doSth();',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertEquals({'goog.someModule': 3},
|
|
||||||
namespaces_info.GetMissingRequires()[0])
|
|
||||||
|
|
||||||
def testScope_usedTypeAlias(self):
|
|
||||||
"""Tests aliased symbols in type annotations."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'/** @type {Event} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_partialAlias_typeOnly(self):
|
|
||||||
"""Tests a partial alias only used in type annotations.
|
|
||||||
|
|
||||||
In this example, some goog.events namespace would need to be required
|
|
||||||
so that evaluating goog.events.bar doesn't throw an error.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Foo} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_partialAlias(self):
|
|
||||||
"""Tests a partial alias in conjunction with a type annotation.
|
|
||||||
|
|
||||||
In this example, the partial alias is already defined by another type,
|
|
||||||
therefore the doc-only type doesn't need to be required.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({'goog.events.bar.EventType': 4}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_partialAliasRequires(self):
|
|
||||||
"""Tests partial aliases with correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.bar.EventType\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_partialAliasRequiresBoth(self):
|
|
||||||
"""Tests partial aliases with correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.bar.Event\');',
|
|
||||||
'goog.require(\'goog.events.bar.EventType\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
event_token = self._GetRequireTokens('goog.events.bar.Event')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(event_token))
|
|
||||||
|
|
||||||
def testScope_partialAliasNoSubtypeRequires(self):
|
|
||||||
"""Tests that partial aliases don't yield subtype requires (regression)."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'goog.events.Foo\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'goog.events.Foo = {};',
|
|
||||||
'var Foo = goog.events.Foo;'
|
|
||||||
'Foo.CssName_ = {};'
|
|
||||||
'var CssName_ = Foo.CssName_;'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
|
|
||||||
def testScope_aliasNamespace(self):
|
|
||||||
"""Tests that an unused alias namespace is not required when available.
|
|
||||||
|
|
||||||
In the example goog.events.Bar is not required, because the namespace
|
|
||||||
goog.events is already defined because goog.events.Foo is required.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.Foo\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Bar = goog.events.Bar;',
|
|
||||||
'/** @type {Bar} */;',
|
|
||||||
'goog.events.Foo;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_aliasNamespaceIllegal(self):
|
|
||||||
"""Tests that an unused alias namespace is not required when available."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Bar = goog.events.Bar;',
|
|
||||||
'/** @type {Bar} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_provides(self):
|
|
||||||
"""Tests that aliased symbols result in correct provides."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'goog.bar = {};',
|
|
||||||
'var bar = goog.bar;',
|
|
||||||
'bar.Foo = {};',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
self.assertEquals({'goog.bar.Foo': 4}, missing_provides)
|
|
||||||
_, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testSetTestOnlyNamespaces(self):
|
|
||||||
"""Tests that a namespace in setTestOnly makes it a valid provide."""
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript([
|
|
||||||
'goog.setTestOnly(\'goog.foo.barTest\');'
|
|
||||||
], ['goog'])
|
|
||||||
|
|
||||||
token = self._GetProvideTokens('goog.foo.barTest')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token))
|
|
||||||
|
|
||||||
token = self._GetProvideTokens('goog.foo.bazTest')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token))
|
|
||||||
|
|
||||||
def testSetTestOnlyComment(self):
|
|
||||||
"""Ensure a comment in setTestOnly does not cause a created namespace."""
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript([
|
|
||||||
'goog.setTestOnly(\'this is a comment\');'
|
|
||||||
], ['goog'])
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
[], namespaces_info._created_namespaces,
|
|
||||||
'A comment in setTestOnly should not modify created namespaces.')
|
|
||||||
|
|
||||||
def _GetNamespacesInfoForScript(self, script, closurized_namespaces=None):
|
|
||||||
_, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
script, closurized_namespaces)
|
|
||||||
|
|
||||||
return namespaces_info
|
|
||||||
|
|
||||||
def _GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
self, script, closurized_namespaces):
|
|
||||||
|
|
||||||
token = testutil.TokenizeSource(script)
|
|
||||||
return token, self._GetInitializedNamespacesInfo(
|
|
||||||
token, closurized_namespaces, [])
|
|
||||||
|
|
||||||
def _GetInitializedNamespacesInfo(self, token, closurized_namespaces,
|
|
||||||
ignored_extra_namespaces):
|
|
||||||
"""Returns a namespaces info initialized with the given token stream."""
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=closurized_namespaces,
|
|
||||||
ignored_extra_namespaces=ignored_extra_namespaces)
|
|
||||||
state_tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
|
|
||||||
ecma_pass = ecmametadatapass.EcmaMetaDataPass()
|
|
||||||
ecma_pass.Process(token)
|
|
||||||
|
|
||||||
state_tracker.DocFlagPass(token, error_handler=None)
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(closurized_namespaces)
|
|
||||||
alias_pass.Process(token)
|
|
||||||
|
|
||||||
while token:
|
|
||||||
state_tracker.HandleToken(token, state_tracker.GetLastNonSpaceToken())
|
|
||||||
namespaces_info.ProcessToken(token, state_tracker)
|
|
||||||
state_tracker.HandleAfterToken(token)
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
return namespaces_info
|
|
||||||
|
|
||||||
def _GetProvideTokens(self, namespace):
|
|
||||||
"""Returns a list of tokens for a goog.require of the given namespace."""
|
|
||||||
line_text = 'goog.require(\'' + namespace + '\');\n'
|
|
||||||
return testutil.TokenizeSource([line_text])
|
|
||||||
|
|
||||||
def _GetRequireTokens(self, namespace):
|
|
||||||
"""Returns a list of tokens for a goog.require of the given namespace."""
|
|
||||||
line_text = 'goog.require(\'' + namespace + '\');\n'
|
|
||||||
return testutil.TokenizeSource([line_text])
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Package indicator for gjslint.common."""
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Error object commonly used in linters."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class Error(object):
|
|
||||||
"""Object representing a style error."""
|
|
||||||
|
|
||||||
def __init__(self, code, message, token=None, position=None, fix_data=None):
|
|
||||||
"""Initialize the error object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: The numeric error code.
|
|
||||||
message: The error message string.
|
|
||||||
token: The tokens.Token where the error occurred.
|
|
||||||
position: The position of the error within the token.
|
|
||||||
fix_data: Data to be used in autofixing. Codes with fix_data are:
|
|
||||||
GOOG_REQUIRES_NOT_ALPHABETIZED - List of string value tokens that are
|
|
||||||
class names in goog.requires calls.
|
|
||||||
"""
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
self.token = token
|
|
||||||
self.position = position
|
|
||||||
if token:
|
|
||||||
self.start_index = token.start_index
|
|
||||||
else:
|
|
||||||
self.start_index = 0
|
|
||||||
self.fix_data = fix_data
|
|
||||||
if self.position:
|
|
||||||
self.start_index += self.position.start
|
|
||||||
|
|
||||||
def Compare(a, b):
|
|
||||||
"""Compare two error objects, by source code order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
a: First error object.
|
|
||||||
b: Second error object.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Negative/0/Positive number when a is before/the same as/after b.
|
|
||||||
"""
|
|
||||||
line_diff = a.token.line_number - b.token.line_number
|
|
||||||
if line_diff:
|
|
||||||
return line_diff
|
|
||||||
|
|
||||||
return a.start_index - b.start_index
|
|
||||||
Compare = staticmethod(Compare)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Linter error handler class that accumulates an array of errors."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
from closure_linter.common import errorhandler
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorAccumulator(errorhandler.ErrorHandler):
|
|
||||||
"""Error handler object that accumulates errors in a list."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._errors = []
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Append the error to the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
self._errors.append(error)
|
|
||||||
|
|
||||||
def GetErrors(self):
|
|
||||||
"""Returns the accumulated errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of errors.
|
|
||||||
"""
|
|
||||||
return self._errors
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Interface for a linter error handler.
|
|
||||||
|
|
||||||
Error handlers aggregate a set of errors from multiple files and can optionally
|
|
||||||
perform some action based on the reported errors, for example, logging the error
|
|
||||||
or automatically fixing it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorHandler(object):
|
|
||||||
"""Error handler interface."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if self.__class__ == ErrorHandler:
|
|
||||||
raise NotImplementedError('class ErrorHandler is abstract')
|
|
||||||
|
|
||||||
def HandleFile(self, filename, first_token):
|
|
||||||
"""Notifies this ErrorHandler that subsequent errors are in filename.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The file being linted.
|
|
||||||
first_token: The first token of the file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Append the error to the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
|
|
||||||
def FinishFile(self):
|
|
||||||
"""Finishes handling the current file.
|
|
||||||
|
|
||||||
Should be called after all errors in a file have been handled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def GetErrors(self):
|
|
||||||
"""Returns the accumulated errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of errors.
|
|
||||||
"""
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Utility functions to format errors."""
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
|
|
||||||
def GetUnixErrorOutput(filename, error, new_error=False):
|
|
||||||
"""Get a output line for an error in UNIX format."""
|
|
||||||
|
|
||||||
line = ''
|
|
||||||
|
|
||||||
if error.token:
|
|
||||||
line = '%d' % error.token.line_number
|
|
||||||
|
|
||||||
error_code = '%04d' % error.code
|
|
||||||
if new_error:
|
|
||||||
error_code = 'New Error ' + error_code
|
|
||||||
return '%s:%s:(%s) %s' % (filename, line, error_code, error.message)
|
|
||||||
|
|
||||||
|
|
||||||
def GetErrorOutput(error, new_error=False):
|
|
||||||
"""Get a output line for an error in regular format."""
|
|
||||||
|
|
||||||
line = ''
|
|
||||||
if error.token:
|
|
||||||
line = 'Line %d, ' % error.token.line_number
|
|
||||||
|
|
||||||
code = 'E:%04d' % error.code
|
|
||||||
|
|
||||||
error_message = error.message
|
|
||||||
if new_error:
|
|
||||||
error_message = 'New Error ' + error_message
|
|
||||||
|
|
||||||
return '%s%s: %s' % (line, code, error.message)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Test case that runs a checker on a file, matching errors against annotations.
|
|
||||||
|
|
||||||
Runs the given checker on the given file, accumulating all errors. The list
|
|
||||||
of errors is then matched against those annotated in the file. Based heavily
|
|
||||||
on devtools/javascript/gpylint/full_test.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotatedFileTestCase(googletest.TestCase):
|
|
||||||
"""Test case to run a linter against a single file."""
|
|
||||||
|
|
||||||
# Matches an all caps letters + underscores error identifer
|
|
||||||
_MESSAGE = {'msg': '[A-Z][A-Z_]+'}
|
|
||||||
# Matches a //, followed by an optional line number with a +/-, followed by a
|
|
||||||
# list of message IDs. Used to extract expected messages from testdata files.
|
|
||||||
# TODO(robbyw): Generalize to use different commenting patterns.
|
|
||||||
_EXPECTED_RE = re.compile(r'\s*//\s*(?:(?P<line>[+-]?[0-9]+):)?'
|
|
||||||
r'\s*(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)' % _MESSAGE)
|
|
||||||
|
|
||||||
def __init__(self, filename, lint_callable, converter):
|
|
||||||
"""Create a single file lint test case.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to test.
|
|
||||||
lint_callable: Callable that lints a file. This is usually runner.Run().
|
|
||||||
converter: Function taking an error string and returning an error code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
googletest.TestCase.__init__(self, 'runTest')
|
|
||||||
self._filename = filename
|
|
||||||
self._messages = []
|
|
||||||
self._lint_callable = lint_callable
|
|
||||||
self._converter = converter
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = True
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = False
|
|
||||||
|
|
||||||
def shortDescription(self):
|
|
||||||
"""Provides a description for the test."""
|
|
||||||
return 'Run linter on %s' % self._filename
|
|
||||||
|
|
||||||
def runTest(self):
|
|
||||||
"""Runs the test."""
|
|
||||||
try:
|
|
||||||
filename = self._filename
|
|
||||||
stream = open(filename)
|
|
||||||
except IOError as ex:
|
|
||||||
raise IOError('Could not find testdata resource for %s: %s' %
|
|
||||||
(self._filename, ex))
|
|
||||||
|
|
||||||
expected = self._GetExpectedMessages(stream)
|
|
||||||
got = self._ProcessFileAndGetMessages(filename)
|
|
||||||
self.assertEqual(expected, got)
|
|
||||||
|
|
||||||
def _GetExpectedMessages(self, stream):
|
|
||||||
"""Parse a file and get a sorted list of expected messages."""
|
|
||||||
messages = []
|
|
||||||
for i, line in enumerate(stream):
|
|
||||||
match = self._EXPECTED_RE.search(line)
|
|
||||||
if match:
|
|
||||||
line = match.group('line')
|
|
||||||
msg_ids = match.group('msgs')
|
|
||||||
if line is None:
|
|
||||||
line = i + 1
|
|
||||||
elif line.startswith('+') or line.startswith('-'):
|
|
||||||
line = i + 1 + int(line)
|
|
||||||
else:
|
|
||||||
line = int(line)
|
|
||||||
for msg_id in msg_ids.split(','):
|
|
||||||
# Ignore a spurious message from the license preamble.
|
|
||||||
if msg_id != 'WITHOUT':
|
|
||||||
messages.append((line, self._converter(msg_id.strip())))
|
|
||||||
stream.seek(0)
|
|
||||||
messages.sort()
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def _ProcessFileAndGetMessages(self, filename):
|
|
||||||
"""Trap gjslint's output parse it to get messages added."""
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
self._lint_callable(filename, error_accumulator)
|
|
||||||
|
|
||||||
errors = error_accumulator.GetErrors()
|
|
||||||
|
|
||||||
# Convert to expected tuple format.
|
|
||||||
|
|
||||||
error_msgs = [(error.token.line_number, error.code) for error in errors]
|
|
||||||
error_msgs.sort()
|
|
||||||
return error_msgs
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Utilities for dealing with HTML."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)')
|
|
||||||
|
|
||||||
import cStringIO
|
|
||||||
import formatter
|
|
||||||
import htmllib
|
|
||||||
import HTMLParser
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptExtractor(htmllib.HTMLParser):
|
|
||||||
"""Subclass of HTMLParser that extracts script contents from an HTML file.
|
|
||||||
|
|
||||||
Also inserts appropriate blank lines so that line numbers in the extracted
|
|
||||||
code match the line numbers in the original HTML.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize a ScriptExtractor."""
|
|
||||||
htmllib.HTMLParser.__init__(self, formatter.NullFormatter())
|
|
||||||
self._in_script = False
|
|
||||||
self._text = ''
|
|
||||||
|
|
||||||
def start_script(self, attrs):
|
|
||||||
"""Internal handler for the start of a script tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
attrs: The attributes of the script tag, as a list of tuples.
|
|
||||||
"""
|
|
||||||
for attribute in attrs:
|
|
||||||
if attribute[0].lower() == 'src':
|
|
||||||
# Skip script tags with a src specified.
|
|
||||||
return
|
|
||||||
self._in_script = True
|
|
||||||
|
|
||||||
def end_script(self):
|
|
||||||
"""Internal handler for the end of a script tag."""
|
|
||||||
self._in_script = False
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
"""Internal handler for character data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The character data from the HTML file.
|
|
||||||
"""
|
|
||||||
if self._in_script:
|
|
||||||
# If the last line contains whitespace only, i.e. is just there to
|
|
||||||
# properly align a </script> tag, strip the whitespace.
|
|
||||||
if data.rstrip(' \t') != data.rstrip(' \t\n\r\f'):
|
|
||||||
data = data.rstrip(' \t')
|
|
||||||
self._text += data
|
|
||||||
else:
|
|
||||||
self._AppendNewlines(data)
|
|
||||||
|
|
||||||
def handle_comment(self, data):
|
|
||||||
"""Internal handler for HTML comments.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The text of the comment.
|
|
||||||
"""
|
|
||||||
self._AppendNewlines(data)
|
|
||||||
|
|
||||||
def _AppendNewlines(self, data):
|
|
||||||
"""Count the number of newlines in the given string and append them.
|
|
||||||
|
|
||||||
This ensures line numbers are correct for reported errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The data to count newlines in.
|
|
||||||
"""
|
|
||||||
# We append 'x' to both sides of the string to ensure that splitlines
|
|
||||||
# gives us an accurate count.
|
|
||||||
for i in xrange(len(('x' + data + 'x').splitlines()) - 1):
|
|
||||||
self._text += '\n'
|
|
||||||
|
|
||||||
def GetScriptLines(self):
|
|
||||||
"""Return the extracted script lines.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The extracted script lines as a list of strings.
|
|
||||||
"""
|
|
||||||
return self._text.splitlines()
|
|
||||||
|
|
||||||
|
|
||||||
def GetScriptLines(f):
|
|
||||||
"""Extract script tag contents from the given HTML file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
f: The HTML file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Lines in the HTML file that are from script tags.
|
|
||||||
"""
|
|
||||||
extractor = ScriptExtractor()
|
|
||||||
|
|
||||||
# The HTML parser chokes on text like Array.<!string>, so we patch
|
|
||||||
# that bug by replacing the < with < - escaping all text inside script
|
|
||||||
# tags would be better but it's a bit of a catch 22.
|
|
||||||
contents = f.read()
|
|
||||||
contents = re.sub(r'<([^\s\w/])',
|
|
||||||
lambda x: '<%s' % x.group(1),
|
|
||||||
contents)
|
|
||||||
|
|
||||||
extractor.feed(contents)
|
|
||||||
extractor.close()
|
|
||||||
return extractor.GetScriptLines()
|
|
||||||
|
|
||||||
|
|
||||||
def StripTags(str):
|
|
||||||
"""Returns the string with HTML tags stripped.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
str: An html string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The html string with all tags stripped. If there was a parse error, returns
|
|
||||||
the text successfully parsed so far.
|
|
||||||
"""
|
|
||||||
# Brute force approach to stripping as much HTML as possible. If there is a
|
|
||||||
# parsing error, don't strip text before parse error position, and continue
|
|
||||||
# trying from there.
|
|
||||||
final_text = ''
|
|
||||||
finished = False
|
|
||||||
while not finished:
|
|
||||||
try:
|
|
||||||
strip = _HtmlStripper()
|
|
||||||
strip.feed(str)
|
|
||||||
strip.close()
|
|
||||||
str = strip.get_output()
|
|
||||||
final_text += str
|
|
||||||
finished = True
|
|
||||||
except HTMLParser.HTMLParseError, e:
|
|
||||||
final_text += str[:e.offset]
|
|
||||||
str = str[e.offset + 1:]
|
|
||||||
|
|
||||||
return final_text
|
|
||||||
|
|
||||||
|
|
||||||
class _HtmlStripper(HTMLParser.HTMLParser):
|
|
||||||
"""Simple class to strip tags from HTML.
|
|
||||||
|
|
||||||
Does so by doing nothing when encountering tags, and appending character data
|
|
||||||
to a buffer when that is encountered.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.reset()
|
|
||||||
self.__output = cStringIO.StringIO()
|
|
||||||
|
|
||||||
def handle_data(self, d):
|
|
||||||
self.__output.write(d)
|
|
||||||
|
|
||||||
def get_output(self):
|
|
||||||
return self.__output.getvalue()
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Interface for a lint running wrapper."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class LintRunner(object):
|
|
||||||
"""Interface for a lint running wrapper."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if self.__class__ == LintRunner:
|
|
||||||
raise NotImplementedError('class LintRunner is abstract')
|
|
||||||
|
|
||||||
def Run(self, filenames, error_handler):
|
|
||||||
"""Run a linter on the given filenames.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filenames: The filenames to check
|
|
||||||
error_handler: An ErrorHandler object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The error handler, which may have been used to collect error info.
|
|
||||||
"""
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Regular expression based JavaScript matcher classes."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
from closure_linter.common import position
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Token = tokens.Token
|
|
||||||
Position = position.Position
|
|
||||||
|
|
||||||
|
|
||||||
class Matcher(object):
|
|
||||||
"""A token matcher.
|
|
||||||
|
|
||||||
Specifies a pattern to match, the type of token it represents, what mode the
|
|
||||||
token changes to, and what mode the token applies to.
|
|
||||||
|
|
||||||
Modes allow more advanced grammars to be incorporated, and are also necessary
|
|
||||||
to tokenize line by line. We can have different patterns apply to different
|
|
||||||
modes - i.e. looking for documentation while in comment mode.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
regex: The regular expression representing this matcher.
|
|
||||||
type: The type of token indicated by a successful match.
|
|
||||||
result_mode: The mode to move to after a successful match.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, regex, token_type, result_mode=None, line_start=False):
|
|
||||||
"""Create a new matcher template.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
regex: The regular expression to match.
|
|
||||||
token_type: The type of token a successful match indicates.
|
|
||||||
result_mode: What mode to change to after a successful match. Defaults to
|
|
||||||
None, which means to not change the current mode.
|
|
||||||
line_start: Whether this matcher should only match string at the start
|
|
||||||
of a line.
|
|
||||||
"""
|
|
||||||
self.regex = regex
|
|
||||||
self.type = token_type
|
|
||||||
self.result_mode = result_mode
|
|
||||||
self.line_start = line_start
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Classes to represent positions within strings."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
|
||||||
"""Object representing a segment of a string.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
start: The index in to the string where the segment starts.
|
|
||||||
length: The length of the string segment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, start, length):
|
|
||||||
"""Initialize the position object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start: The start index.
|
|
||||||
length: The number of characters to include.
|
|
||||||
"""
|
|
||||||
self.start = start
|
|
||||||
self.length = length
|
|
||||||
|
|
||||||
def Get(self, string):
|
|
||||||
"""Returns this range of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to slice.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The string within the range specified by this object.
|
|
||||||
"""
|
|
||||||
return string[self.start:self.start + self.length]
|
|
||||||
|
|
||||||
def Set(self, target, source):
|
|
||||||
"""Sets this range within the target string to the source string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target: The target string.
|
|
||||||
source: The source string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resulting string
|
|
||||||
"""
|
|
||||||
return target[:self.start] + source + target[self.start + self.length:]
|
|
||||||
|
|
||||||
def AtEnd(string):
|
|
||||||
"""Create a Position representing the end of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to represent the end of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(len(string), 0)
|
|
||||||
AtEnd = staticmethod(AtEnd)
|
|
||||||
|
|
||||||
def IsAtEnd(self, string):
|
|
||||||
"""Returns whether this position is at the end of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to test for the end of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether this position is at the end of the given string.
|
|
||||||
"""
|
|
||||||
return self.start == len(string) and self.length == 0
|
|
||||||
|
|
||||||
def AtBeginning():
|
|
||||||
"""Create a Position representing the beginning of any string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(0, 0)
|
|
||||||
AtBeginning = staticmethod(AtBeginning)
|
|
||||||
|
|
||||||
def IsAtBeginning(self):
|
|
||||||
"""Returns whether this position is at the beginning of any string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether this position is at the beginning of any string.
|
|
||||||
"""
|
|
||||||
return self.start == 0 and self.length == 0
|
|
||||||
|
|
||||||
def All(string):
|
|
||||||
"""Create a Position representing the entire string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to represent the entirety of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(0, len(string))
|
|
||||||
All = staticmethod(All)
|
|
||||||
|
|
||||||
def Index(index):
|
|
||||||
"""Returns a Position object for the specified index.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
index: The index to select, inclusively.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(index, 1)
|
|
||||||
Index = staticmethod(Index)
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Determines the list of files to be checked from command line arguments."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
flags.DEFINE_multistring(
|
|
||||||
'recurse',
|
|
||||||
None,
|
|
||||||
'Recurse in to the subdirectories of the given path',
|
|
||||||
short_name='r')
|
|
||||||
flags.DEFINE_list(
|
|
||||||
'exclude_directories',
|
|
||||||
('_demos'),
|
|
||||||
'Exclude the specified directories (only applicable along with -r or '
|
|
||||||
'--presubmit)',
|
|
||||||
short_name='e')
|
|
||||||
flags.DEFINE_list(
|
|
||||||
'exclude_files',
|
|
||||||
('deps.js'),
|
|
||||||
'Exclude the specified files',
|
|
||||||
short_name='x')
|
|
||||||
|
|
||||||
|
|
||||||
def MatchesSuffixes(filename, suffixes):
|
|
||||||
"""Returns whether the given filename matches one of the given suffixes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to check.
|
|
||||||
suffixes: Sequence of suffixes to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the given filename matches one of the given suffixes.
|
|
||||||
"""
|
|
||||||
suffix = filename[filename.rfind('.'):]
|
|
||||||
return suffix in suffixes
|
|
||||||
|
|
||||||
|
|
||||||
def _GetUserSpecifiedFiles(argv, suffixes):
|
|
||||||
"""Returns files to be linted, specified directly on the command line.
|
|
||||||
|
|
||||||
Can handle the '*' wildcard in filenames, but no other wildcards.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments. The second and following arguments
|
|
||||||
are assumed to be files that should be linted.
|
|
||||||
suffixes: Expected suffixes for the file type being checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of files to be linted.
|
|
||||||
"""
|
|
||||||
files = argv[1:] or []
|
|
||||||
all_files = []
|
|
||||||
lint_files = []
|
|
||||||
|
|
||||||
# Perform any necessary globs.
|
|
||||||
for f in files:
|
|
||||||
if f.find('*') != -1:
|
|
||||||
for result in glob.glob(f):
|
|
||||||
all_files.append(result)
|
|
||||||
else:
|
|
||||||
all_files.append(f)
|
|
||||||
|
|
||||||
for f in all_files:
|
|
||||||
if MatchesSuffixes(f, suffixes):
|
|
||||||
lint_files.append(f)
|
|
||||||
return lint_files
|
|
||||||
|
|
||||||
|
|
||||||
def _GetRecursiveFiles(suffixes):
|
|
||||||
"""Returns files to be checked specified by the --recurse flag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
suffixes: Expected suffixes for the file type being checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of files to be checked.
|
|
||||||
"""
|
|
||||||
lint_files = []
|
|
||||||
# Perform any request recursion
|
|
||||||
if FLAGS.recurse:
|
|
||||||
for start in FLAGS.recurse:
|
|
||||||
for root, subdirs, files in os.walk(start):
|
|
||||||
for f in files:
|
|
||||||
if MatchesSuffixes(f, suffixes):
|
|
||||||
lint_files.append(os.path.join(root, f))
|
|
||||||
return lint_files
|
|
||||||
|
|
||||||
|
|
||||||
def GetAllSpecifiedFiles(argv, suffixes):
|
|
||||||
"""Returns all files specified by the user on the commandline.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments. The second and following arguments
|
|
||||||
are assumed to be files that should be linted.
|
|
||||||
suffixes: Expected suffixes for the file type
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of all files specified directly or indirectly (via flags) on the
|
|
||||||
command line by the user.
|
|
||||||
"""
|
|
||||||
files = _GetUserSpecifiedFiles(argv, suffixes)
|
|
||||||
|
|
||||||
if FLAGS.recurse:
|
|
||||||
files += _GetRecursiveFiles(suffixes)
|
|
||||||
|
|
||||||
return FilterFiles(files)
|
|
||||||
|
|
||||||
|
|
||||||
def FilterFiles(files):
|
|
||||||
"""Filters the list of files to be linted be removing any excluded files.
|
|
||||||
|
|
||||||
Filters out files excluded using --exclude_files and --exclude_directories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files: Sequence of files that needs filtering.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Filtered list of files to be linted.
|
|
||||||
"""
|
|
||||||
num_files = len(files)
|
|
||||||
|
|
||||||
ignore_dirs_regexs = []
|
|
||||||
for ignore in FLAGS.exclude_directories:
|
|
||||||
ignore_dirs_regexs.append(re.compile(r'(^|[\\/])%s[\\/]' % ignore))
|
|
||||||
|
|
||||||
result_files = []
|
|
||||||
for f in files:
|
|
||||||
add_file = True
|
|
||||||
for exclude in FLAGS.exclude_files:
|
|
||||||
if f.endswith('/' + exclude) or f == exclude:
|
|
||||||
add_file = False
|
|
||||||
break
|
|
||||||
for ignore in ignore_dirs_regexs:
|
|
||||||
if ignore.search(f):
|
|
||||||
# Break out of ignore loop so we don't add to
|
|
||||||
# filtered files.
|
|
||||||
add_file = False
|
|
||||||
break
|
|
||||||
if add_file:
|
|
||||||
# Convert everything to absolute paths so we can easily remove duplicates
|
|
||||||
# using a set.
|
|
||||||
result_files.append(os.path.abspath(f))
|
|
||||||
|
|
||||||
skipped = num_files - len(result_files)
|
|
||||||
if skipped:
|
|
||||||
print 'Skipping %d file(s).' % skipped
|
|
||||||
|
|
||||||
return set(result_files)
|
|
||||||
|
|
||||||
|
|
||||||
def GetFileList(argv, file_type, suffixes):
|
|
||||||
"""Parse the flags and return the list of files to check.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
suffixes: Sequence of acceptable suffixes for the file type.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The list of files to check.
|
|
||||||
"""
|
|
||||||
return sorted(GetAllSpecifiedFiles(argv, suffixes))
|
|
||||||
|
|
||||||
|
|
||||||
def IsEmptyArgumentList(argv):
|
|
||||||
return not (len(argv[1:]) or FLAGS.recurse)
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Regular expression based lexer."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = tokens.TokenType
|
|
||||||
|
|
||||||
|
|
||||||
class Tokenizer(object):
|
|
||||||
"""General purpose tokenizer.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
mode: The latest mode of the tokenizer. This allows patterns to distinguish
|
|
||||||
if they are mid-comment, mid-parameter list, etc.
|
|
||||||
matchers: Dictionary of modes to sequences of matchers that define the
|
|
||||||
patterns to check at any given time.
|
|
||||||
default_types: Dictionary of modes to types, defining what type to give
|
|
||||||
non-matched text when in the given mode. Defaults to Type.NORMAL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, starting_mode, matchers, default_types):
|
|
||||||
"""Initialize the tokenizer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
starting_mode: Mode to start in.
|
|
||||||
matchers: Dictionary of modes to sequences of matchers that defines the
|
|
||||||
patterns to check at any given time.
|
|
||||||
default_types: Dictionary of modes to types, defining what type to give
|
|
||||||
non-matched text when in the given mode. Defaults to Type.NORMAL.
|
|
||||||
"""
|
|
||||||
self.__starting_mode = starting_mode
|
|
||||||
self.matchers = matchers
|
|
||||||
self.default_types = default_types
|
|
||||||
|
|
||||||
def TokenizeFile(self, file):
|
|
||||||
"""Tokenizes the given file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file: An iterable that yields one line of the file at a time.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token in the file
|
|
||||||
"""
|
|
||||||
# The current mode.
|
|
||||||
self.mode = self.__starting_mode
|
|
||||||
# The first token in the stream.
|
|
||||||
self.__first_token = None
|
|
||||||
# The last token added to the token stream.
|
|
||||||
self.__last_token = None
|
|
||||||
# The current line number.
|
|
||||||
self.__line_number = 0
|
|
||||||
|
|
||||||
for line in file:
|
|
||||||
self.__line_number += 1
|
|
||||||
self.__TokenizeLine(line)
|
|
||||||
|
|
||||||
return self.__first_token
|
|
||||||
|
|
||||||
def _CreateToken(self, string, token_type, line, line_number, values=None):
|
|
||||||
"""Creates a new Token object (or subclass).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string of input the token represents.
|
|
||||||
token_type: The type of token.
|
|
||||||
line: The text of the line this token is in.
|
|
||||||
line_number: The line number of the token.
|
|
||||||
values: A dict of named values within the token. For instance, a
|
|
||||||
function declaration may have a value called 'name' which captures the
|
|
||||||
name of the function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The newly created Token object.
|
|
||||||
"""
|
|
||||||
return tokens.Token(string, token_type, line, line_number, values,
|
|
||||||
line_number)
|
|
||||||
|
|
||||||
def __TokenizeLine(self, line):
|
|
||||||
"""Tokenizes the given line.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line: The contents of the line.
|
|
||||||
"""
|
|
||||||
string = line.rstrip('\n\r\f')
|
|
||||||
line_number = self.__line_number
|
|
||||||
self.__start_index = 0
|
|
||||||
|
|
||||||
if not string:
|
|
||||||
self.__AddToken(self._CreateToken('', Type.BLANK_LINE, line, line_number))
|
|
||||||
return
|
|
||||||
|
|
||||||
normal_token = ''
|
|
||||||
index = 0
|
|
||||||
while index < len(string):
|
|
||||||
for matcher in self.matchers[self.mode]:
|
|
||||||
if matcher.line_start and index > 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
match = matcher.regex.match(string, index)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
if normal_token:
|
|
||||||
self.__AddToken(
|
|
||||||
self.__CreateNormalToken(self.mode, normal_token, line,
|
|
||||||
line_number))
|
|
||||||
normal_token = ''
|
|
||||||
|
|
||||||
# Add the match.
|
|
||||||
self.__AddToken(self._CreateToken(match.group(), matcher.type, line,
|
|
||||||
line_number, match.groupdict()))
|
|
||||||
|
|
||||||
# Change the mode to the correct one for after this match.
|
|
||||||
self.mode = matcher.result_mode or self.mode
|
|
||||||
|
|
||||||
# Shorten the string to be matched.
|
|
||||||
index = match.end()
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
# If the for loop finishes naturally (i.e. no matches) we just add the
|
|
||||||
# first character to the string of consecutive non match characters.
|
|
||||||
# These will constitute a NORMAL token.
|
|
||||||
if string:
|
|
||||||
normal_token += string[index:index + 1]
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
if normal_token:
|
|
||||||
self.__AddToken(
|
|
||||||
self.__CreateNormalToken(self.mode, normal_token, line, line_number))
|
|
||||||
|
|
||||||
def __CreateNormalToken(self, mode, string, line, line_number):
|
|
||||||
"""Creates a normal token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mode: The current mode.
|
|
||||||
string: The string to tokenize.
|
|
||||||
line: The line of text.
|
|
||||||
line_number: The line number within the file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Token object, of the default type for the current mode.
|
|
||||||
"""
|
|
||||||
type = Type.NORMAL
|
|
||||||
if mode in self.default_types:
|
|
||||||
type = self.default_types[mode]
|
|
||||||
return self._CreateToken(string, type, line, line_number)
|
|
||||||
|
|
||||||
def __AddToken(self, token):
|
|
||||||
"""Add the given token to the token stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to add.
|
|
||||||
"""
|
|
||||||
# Store the first token, or point the previous token to this one.
|
|
||||||
if not self.__first_token:
|
|
||||||
self.__first_token = token
|
|
||||||
else:
|
|
||||||
self.__last_token.next = token
|
|
||||||
|
|
||||||
# Establish the doubly linked list
|
|
||||||
token.previous = self.__last_token
|
|
||||||
self.__last_token = token
|
|
||||||
|
|
||||||
# Compute the character indices
|
|
||||||
token.start_index = self.__start_index
|
|
||||||
self.__start_index += token.length
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Classes to represent tokens and positions within them."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class TokenType(object):
|
|
||||||
"""Token types common to all languages."""
|
|
||||||
NORMAL = 'normal'
|
|
||||||
WHITESPACE = 'whitespace'
|
|
||||||
BLANK_LINE = 'blank line'
|
|
||||||
|
|
||||||
|
|
||||||
class Token(object):
|
|
||||||
"""Token class for intelligent text splitting.
|
|
||||||
|
|
||||||
The token class represents a string of characters and an identifying type.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The type of token.
|
|
||||||
string: The characters the token comprises.
|
|
||||||
length: The length of the token.
|
|
||||||
line: The text of the line the token is found in.
|
|
||||||
line_number: The number of the line the token is found in.
|
|
||||||
values: Dictionary of values returned from the tokens regex match.
|
|
||||||
previous: The token before this one.
|
|
||||||
next: The token after this one.
|
|
||||||
start_index: The character index in the line where this token starts.
|
|
||||||
attached_object: Object containing more information about this token.
|
|
||||||
metadata: Object containing metadata about this token. Must be added by
|
|
||||||
a separate metadata pass.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, string, token_type, line, line_number, values=None,
|
|
||||||
orig_line_number=None):
|
|
||||||
"""Creates a new Token object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string of input the token contains.
|
|
||||||
token_type: The type of token.
|
|
||||||
line: The text of the line this token is in.
|
|
||||||
line_number: The line number of the token.
|
|
||||||
values: A dict of named values within the token. For instance, a
|
|
||||||
function declaration may have a value called 'name' which captures the
|
|
||||||
name of the function.
|
|
||||||
orig_line_number: The line number of the original file this token comes
|
|
||||||
from. This should be only set during the tokenization process. For newly
|
|
||||||
created error fix tokens after that, it should be None.
|
|
||||||
"""
|
|
||||||
self.type = token_type
|
|
||||||
self.string = string
|
|
||||||
self.length = len(string)
|
|
||||||
self.line = line
|
|
||||||
self.line_number = line_number
|
|
||||||
self.orig_line_number = orig_line_number
|
|
||||||
self.values = values
|
|
||||||
self.is_deleted = False
|
|
||||||
|
|
||||||
# These parts can only be computed when the file is fully tokenized
|
|
||||||
self.previous = None
|
|
||||||
self.next = None
|
|
||||||
self.start_index = None
|
|
||||||
|
|
||||||
# This part is set in statetracker.py
|
|
||||||
# TODO(robbyw): Wrap this in to metadata
|
|
||||||
self.attached_object = None
|
|
||||||
|
|
||||||
# This part is set in *metadatapass.py
|
|
||||||
self.metadata = None
|
|
||||||
|
|
||||||
def IsFirstInLine(self):
|
|
||||||
"""Tests if this token is the first token in its line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the token is the first token in its line.
|
|
||||||
"""
|
|
||||||
return not self.previous or self.previous.line_number != self.line_number
|
|
||||||
|
|
||||||
def IsLastInLine(self):
|
|
||||||
"""Tests if this token is the last token in its line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the token is the last token in its line.
|
|
||||||
"""
|
|
||||||
return not self.next or self.next.line_number != self.line_number
|
|
||||||
|
|
||||||
def IsType(self, token_type):
|
|
||||||
"""Tests if this token is of the given type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_type: The type to test for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the type of this token matches the type passed in.
|
|
||||||
"""
|
|
||||||
return self.type == token_type
|
|
||||||
|
|
||||||
def IsAnyType(self, *token_types):
|
|
||||||
"""Tests if this token is any of the given types.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_types: The types to check. Also accepts a single array.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the type of this token is any of the types passed in.
|
|
||||||
"""
|
|
||||||
if not isinstance(token_types[0], basestring):
|
|
||||||
return self.type in token_types[0]
|
|
||||||
else:
|
|
||||||
return self.type in token_types
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Token: %s, "%s", %r, %d, %r>' % (self.type, self.string,
|
|
||||||
self.values, self.line_number,
|
|
||||||
self.metadata)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Returns a token iterator."""
|
|
||||||
node = self
|
|
||||||
while node:
|
|
||||||
yield node
|
|
||||||
node = node.next
|
|
||||||
|
|
||||||
def __reversed__(self):
|
|
||||||
"""Returns a reverse-direction token iterator."""
|
|
||||||
node = self
|
|
||||||
while node:
|
|
||||||
yield node
|
|
||||||
node = node.previous
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'nnaze@google.com (Nathan Naze)'
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _CreateDummyToken():
|
|
||||||
return tokens.Token('foo', None, 1, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def _CreateDummyTokens(count):
|
|
||||||
dummy_tokens = []
|
|
||||||
for _ in xrange(count):
|
|
||||||
dummy_tokens.append(_CreateDummyToken())
|
|
||||||
return dummy_tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _SetTokensAsNeighbors(neighbor_tokens):
|
|
||||||
for i in xrange(len(neighbor_tokens)):
|
|
||||||
prev_index = i - 1
|
|
||||||
next_index = i + 1
|
|
||||||
|
|
||||||
if prev_index >= 0:
|
|
||||||
neighbor_tokens[i].previous = neighbor_tokens[prev_index]
|
|
||||||
|
|
||||||
if next_index < len(neighbor_tokens):
|
|
||||||
neighbor_tokens[i].next = neighbor_tokens[next_index]
|
|
||||||
|
|
||||||
|
|
||||||
class TokensTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testIsFirstInLine(self):
|
|
||||||
|
|
||||||
# First token in file (has no previous).
|
|
||||||
self.assertTrue(_CreateDummyToken().IsFirstInLine())
|
|
||||||
|
|
||||||
a, b = _CreateDummyTokens(2)
|
|
||||||
_SetTokensAsNeighbors([a, b])
|
|
||||||
|
|
||||||
# Tokens on same line
|
|
||||||
a.line_number = 30
|
|
||||||
b.line_number = 30
|
|
||||||
|
|
||||||
self.assertFalse(b.IsFirstInLine())
|
|
||||||
|
|
||||||
# Tokens on different lines
|
|
||||||
b.line_number = 31
|
|
||||||
self.assertTrue(b.IsFirstInLine())
|
|
||||||
|
|
||||||
def testIsLastInLine(self):
|
|
||||||
# Last token in file (has no next).
|
|
||||||
self.assertTrue(_CreateDummyToken().IsLastInLine())
|
|
||||||
|
|
||||||
a, b = _CreateDummyTokens(2)
|
|
||||||
_SetTokensAsNeighbors([a, b])
|
|
||||||
|
|
||||||
# Tokens on same line
|
|
||||||
a.line_number = 30
|
|
||||||
b.line_number = 30
|
|
||||||
self.assertFalse(a.IsLastInLine())
|
|
||||||
|
|
||||||
b.line_number = 31
|
|
||||||
self.assertTrue(a.IsLastInLine())
|
|
||||||
|
|
||||||
def testIsType(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertTrue(a.IsType('fakeType1'))
|
|
||||||
self.assertFalse(a.IsType('fakeType2'))
|
|
||||||
|
|
||||||
def testIsAnyType(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertTrue(a.IsAnyType(['fakeType1', 'fakeType2']))
|
|
||||||
self.assertFalse(a.IsAnyType(['fakeType3', 'fakeType4']))
|
|
||||||
|
|
||||||
def testRepr(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertEquals('<Token: fakeType1, "foo", None, 1, None>', str(a))
|
|
||||||
|
|
||||||
def testIter(self):
|
|
||||||
dummy_tokens = _CreateDummyTokens(5)
|
|
||||||
_SetTokensAsNeighbors(dummy_tokens)
|
|
||||||
a, b, c, d, e = dummy_tokens
|
|
||||||
|
|
||||||
i = iter(a)
|
|
||||||
self.assertListEqual([a, b, c, d, e], list(i))
|
|
||||||
|
|
||||||
def testReverseIter(self):
|
|
||||||
dummy_tokens = _CreateDummyTokens(5)
|
|
||||||
_SetTokensAsNeighbors(dummy_tokens)
|
|
||||||
a, b, c, d, e = dummy_tokens
|
|
||||||
|
|
||||||
ri = reversed(e)
|
|
||||||
self.assertListEqual([e, d, c, b, a], list(ri))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,844 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Core methods for checking EcmaScript files for common style guide violations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'jacobr@google.com (Jacob Richman)')
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import checkerbase
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errorrules
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import indentation
|
|
||||||
from closure_linter import javascripttokenizer
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import statetracker
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import position
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow')
|
|
||||||
# TODO(user): When flipping this to True, remove logic from unit tests
|
|
||||||
# that overrides this flag.
|
|
||||||
flags.DEFINE_boolean('dot_on_next_line', False, 'Require dots to be'
|
|
||||||
'placed on the next line for wrapped expressions')
|
|
||||||
|
|
||||||
# TODO(robbyw): Check for extra parens on return statements
|
|
||||||
# TODO(robbyw): Check for 0px in strings
|
|
||||||
# TODO(robbyw): Ensure inline jsDoc is in {}
|
|
||||||
# TODO(robbyw): Check for valid JS types in parameter docs
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Context = ecmametadatapass.EcmaContext
|
|
||||||
Error = error.Error
|
|
||||||
Modes = javascripttokenizer.JavaScriptModes
|
|
||||||
Position = position.Position
|
|
||||||
Rule = error_check.Rule
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaScriptLintRules(checkerbase.LintRulesBase):
|
|
||||||
"""EmcaScript lint style checking rules.
|
|
||||||
|
|
||||||
Can be used to find common style errors in JavaScript, ActionScript and other
|
|
||||||
Ecma like scripting languages. Style checkers for Ecma scripting languages
|
|
||||||
should inherit from this style checker.
|
|
||||||
Please do not add any state to EcmaScriptLintRules or to any subclasses.
|
|
||||||
|
|
||||||
All state should be added to the StateTracker subclass used for a particular
|
|
||||||
language.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# It will be initialized in constructor so the flags are initialized.
|
|
||||||
max_line_length = -1
|
|
||||||
|
|
||||||
# Static constants.
|
|
||||||
MISSING_PARAMETER_SPACE = re.compile(r',\S')
|
|
||||||
|
|
||||||
EXTRA_SPACE = re.compile(r'(\(\s|\s\))')
|
|
||||||
|
|
||||||
ENDS_WITH_SPACE = re.compile(r'\s$')
|
|
||||||
|
|
||||||
ILLEGAL_TAB = re.compile(r'\t')
|
|
||||||
|
|
||||||
# Regex used to split up complex types to check for invalid use of ? and |.
|
|
||||||
TYPE_SPLIT = re.compile(r'[,<>()]')
|
|
||||||
|
|
||||||
# Regex for form of author lines after the @author tag.
|
|
||||||
AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)')
|
|
||||||
|
|
||||||
# Acceptable tokens to remove for line too long testing.
|
|
||||||
LONG_LINE_IGNORE = frozenset(
|
|
||||||
['*', '//', '@see'] +
|
|
||||||
['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE])
|
|
||||||
|
|
||||||
JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([
|
|
||||||
'@fileoverview', '@param', '@return', '@returns'])
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize this lint rule object."""
|
|
||||||
checkerbase.LintRulesBase.__init__(self)
|
|
||||||
if EcmaScriptLintRules.max_line_length == -1:
|
|
||||||
EcmaScriptLintRules.max_line_length = errorrules.GetMaxLineLength()
|
|
||||||
|
|
||||||
def Initialize(self, checker, limited_doc_checks, is_html):
|
|
||||||
"""Initialize this lint rule object before parsing a new file."""
|
|
||||||
checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks,
|
|
||||||
is_html)
|
|
||||||
self._indentation = indentation.IndentationRules()
|
|
||||||
|
|
||||||
def HandleMissingParameterDoc(self, token, param_name):
|
|
||||||
"""Handle errors associated with a parameter missing a @param tag."""
|
|
||||||
raise TypeError('Abstract method HandleMissingParameterDoc not implemented')
|
|
||||||
|
|
||||||
def _CheckLineLength(self, last_token, state):
|
|
||||||
"""Checks whether the line is too long.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
last_token: The last token in the line.
|
|
||||||
state: parser_state object that indicates the current state in the page
|
|
||||||
"""
|
|
||||||
# Start from the last token so that we have the flag object attached to
|
|
||||||
# and DOC_FLAG tokens.
|
|
||||||
line_number = last_token.line_number
|
|
||||||
token = last_token
|
|
||||||
|
|
||||||
# Build a representation of the string where spaces indicate potential
|
|
||||||
# line-break locations.
|
|
||||||
line = []
|
|
||||||
while token and token.line_number == line_number:
|
|
||||||
if state.IsTypeToken(token):
|
|
||||||
line.insert(0, 'x' * len(token.string))
|
|
||||||
elif token.type in (Type.IDENTIFIER, Type.OPERATOR):
|
|
||||||
# Dots are acceptable places to wrap (may be tokenized as identifiers).
|
|
||||||
line.insert(0, token.string.replace('.', ' '))
|
|
||||||
else:
|
|
||||||
line.insert(0, token.string)
|
|
||||||
token = token.previous
|
|
||||||
|
|
||||||
line = ''.join(line)
|
|
||||||
line = line.rstrip('\n\r\f')
|
|
||||||
try:
|
|
||||||
length = len(unicode(line, 'utf-8'))
|
|
||||||
except (LookupError, UnicodeDecodeError):
|
|
||||||
# Unknown encoding. The line length may be wrong, as was originally the
|
|
||||||
# case for utf-8 (see bug 1735846). For now just accept the default
|
|
||||||
# length, but as we find problems we can either add test for other
|
|
||||||
# possible encodings or return without an error to protect against
|
|
||||||
# false positives at the cost of more false negatives.
|
|
||||||
length = len(line)
|
|
||||||
|
|
||||||
if length > EcmaScriptLintRules.max_line_length:
|
|
||||||
|
|
||||||
# If the line matches one of the exceptions, then it's ok.
|
|
||||||
for long_line_regexp in self.GetLongLineExceptions():
|
|
||||||
if long_line_regexp.match(last_token.line):
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the line consists of only one "word", or multiple words but all
|
|
||||||
# except one are ignoreable, then it's ok.
|
|
||||||
parts = set(line.split())
|
|
||||||
|
|
||||||
# We allow two "words" (type and name) when the line contains @param
|
|
||||||
max_parts = 1
|
|
||||||
if '@param' in parts:
|
|
||||||
max_parts = 2
|
|
||||||
|
|
||||||
# Custom tags like @requires may have url like descriptions, so ignore
|
|
||||||
# the tag, similar to how we handle @see.
|
|
||||||
custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags])
|
|
||||||
if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags))
|
|
||||||
> max_parts):
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_TOO_LONG,
|
|
||||||
'Line too long (%d characters).' % len(line), last_token)
|
|
||||||
|
|
||||||
def _CheckJsDocType(self, token, js_type):
|
|
||||||
"""Checks the given type for style errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The DOC_FLAG token for the flag whose type to check.
|
|
||||||
js_type: The flag's typeannotation.TypeAnnotation instance.
|
|
||||||
"""
|
|
||||||
if not js_type: return
|
|
||||||
|
|
||||||
if js_type.type_group and len(js_type.sub_types) == 2:
|
|
||||||
identifiers = [t.identifier for t in js_type.sub_types]
|
|
||||||
if 'null' in identifiers:
|
|
||||||
# Don't warn if the identifier is a template type (e.g. {TYPE|null}.
|
|
||||||
if not identifiers[0].isupper() and not identifiers[1].isupper():
|
|
||||||
self._HandleError(
|
|
||||||
errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL,
|
|
||||||
'Prefer "?Type" to "Type|null": "%s"' % js_type, token)
|
|
||||||
|
|
||||||
# TODO(user): We should report an error for wrong usage of '?' and '|'
|
|
||||||
# e.g. {?number|string|null} etc.
|
|
||||||
|
|
||||||
for sub_type in js_type.IterTypes():
|
|
||||||
self._CheckJsDocType(token, sub_type)
|
|
||||||
|
|
||||||
def _CheckForMissingSpaceBeforeToken(self, token):
|
|
||||||
"""Checks for a missing space at the beginning of a token.
|
|
||||||
|
|
||||||
Reports a MISSING_SPACE error if the token does not begin with a space or
|
|
||||||
the previous token doesn't end with a space and the previous token is on the
|
|
||||||
same line as the token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token being checked
|
|
||||||
"""
|
|
||||||
# TODO(user): Check if too many spaces?
|
|
||||||
if (len(token.string) == len(token.string.lstrip()) and
|
|
||||||
token.previous and token.line_number == token.previous.line_number and
|
|
||||||
len(token.previous.string) - len(token.previous.string.rstrip()) == 0):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SPACE,
|
|
||||||
'Missing space before "%s"' % token.string,
|
|
||||||
token,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
def _CheckOperator(self, token):
|
|
||||||
"""Checks an operator for spacing and line style.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The operator token.
|
|
||||||
"""
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
|
|
||||||
if not self._ExpectSpaceBeforeOperator(token):
|
|
||||||
if (token.previous and token.previous.type == Type.WHITESPACE and
|
|
||||||
last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER) and
|
|
||||||
last_code.line_number == token.line_number):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "%s"' % token.string,
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif (token.previous and
|
|
||||||
not token.previous.IsComment() and
|
|
||||||
not tokenutil.IsDot(token) and
|
|
||||||
token.previous.type in Type.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._HandleError(errors.MISSING_SPACE,
|
|
||||||
'Missing space before "%s"' % token.string, token,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
# Check wrapping of operators.
|
|
||||||
next_code = tokenutil.GetNextCodeToken(token)
|
|
||||||
|
|
||||||
is_dot = tokenutil.IsDot(token)
|
|
||||||
wrapped_before = last_code and last_code.line_number != token.line_number
|
|
||||||
wrapped_after = next_code and next_code.line_number != token.line_number
|
|
||||||
|
|
||||||
if FLAGS.dot_on_next_line and is_dot and wrapped_after:
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_ENDS_WITH_DOT,
|
|
||||||
'"." must go on the following line',
|
|
||||||
token)
|
|
||||||
if (not is_dot and wrapped_before and
|
|
||||||
not token.metadata.IsUnaryOperator()):
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_STARTS_WITH_OPERATOR,
|
|
||||||
'Binary operator must go on previous line "%s"' % token.string,
|
|
||||||
token)
|
|
||||||
|
|
||||||
def _IsLabel(self, token):
|
|
||||||
# A ':' token is considered part of a label if it occurs in a case
|
|
||||||
# statement, a plain label, or an object literal, i.e. is not part of a
|
|
||||||
# ternary.
|
|
||||||
|
|
||||||
return (token.string == ':' and
|
|
||||||
token.metadata.context.type in (Context.LITERAL_ELEMENT,
|
|
||||||
Context.CASE_BLOCK,
|
|
||||||
Context.STATEMENT))
|
|
||||||
|
|
||||||
def _ExpectSpaceBeforeOperator(self, token):
|
|
||||||
"""Returns whether a space should appear before the given operator token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The operator token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether there should be a space before the token.
|
|
||||||
"""
|
|
||||||
if token.string == ',' or token.metadata.IsUnaryPostOperator():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if tokenutil.IsDot(token):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Colons should appear in labels, object literals, the case of a switch
|
|
||||||
# statement, and ternary operator. Only want a space in the case of the
|
|
||||||
# ternary operator.
|
|
||||||
if self._IsLabel(token):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token.metadata.IsUnaryOperator() and token.IsFirstInLine():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def CheckToken(self, token, state):
|
|
||||||
"""Checks a token, given the current parser_state, for warnings and errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration
|
|
||||||
state: parser_state object that indicates the current state in the page
|
|
||||||
"""
|
|
||||||
# Store some convenience variables
|
|
||||||
first_in_line = token.IsFirstInLine()
|
|
||||||
last_in_line = token.IsLastInLine()
|
|
||||||
last_non_space_token = state.GetLastNonSpaceToken()
|
|
||||||
|
|
||||||
token_type = token.type
|
|
||||||
|
|
||||||
# Process the line change.
|
|
||||||
if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION):
|
|
||||||
# TODO(robbyw): Support checking indentation in HTML files.
|
|
||||||
indentation_errors = self._indentation.CheckToken(token, state)
|
|
||||||
for indentation_error in indentation_errors:
|
|
||||||
self._HandleError(*indentation_error)
|
|
||||||
|
|
||||||
if last_in_line:
|
|
||||||
self._CheckLineLength(token, state)
|
|
||||||
|
|
||||||
if token_type == Type.PARAMETERS:
|
|
||||||
# Find missing spaces in parameter lists.
|
|
||||||
if self.MISSING_PARAMETER_SPACE.search(token.string):
|
|
||||||
fix_data = ', '.join([s.strip() for s in token.string.split(',')])
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space after ","',
|
|
||||||
token, position=None, fix_data=fix_data.strip())
|
|
||||||
|
|
||||||
# Find extra spaces at the beginning of parameter lists. Make sure
|
|
||||||
# we aren't at the beginning of a continuing multi-line list.
|
|
||||||
if not first_in_line:
|
|
||||||
space_count = len(token.string) - len(token.string.lstrip())
|
|
||||||
if space_count:
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("',
|
|
||||||
token, position=Position(0, space_count))
|
|
||||||
|
|
||||||
elif (token_type == Type.START_BLOCK and
|
|
||||||
token.metadata.context.type == Context.BLOCK):
|
|
||||||
self._CheckForMissingSpaceBeforeToken(token)
|
|
||||||
|
|
||||||
elif token_type == Type.END_BLOCK:
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if state.InFunction() and state.IsFunctionClose():
|
|
||||||
if state.InTopLevelFunction():
|
|
||||||
# A semicolons should not be included at the end of a function
|
|
||||||
# declaration.
|
|
||||||
if not state.InAssignedFunction():
|
|
||||||
if not last_in_line and token.next.type == Type.SEMICOLON:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
'Illegal semicolon after function declaration',
|
|
||||||
token.next, position=Position.All(token.next.string))
|
|
||||||
|
|
||||||
# A semicolon should be included at the end of a function expression
|
|
||||||
# that is not immediately called or used by a dot operator.
|
|
||||||
if (state.InAssignedFunction() and token.next
|
|
||||||
and token.next.type != Type.SEMICOLON):
|
|
||||||
next_token = tokenutil.GetNextCodeToken(token)
|
|
||||||
is_immediately_used = (next_token.type == Type.START_PAREN or
|
|
||||||
tokenutil.IsDot(next_token))
|
|
||||||
if not is_immediately_used:
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
'Missing semicolon after function assigned to a variable',
|
|
||||||
token, position=Position.AtEnd(token.string))
|
|
||||||
|
|
||||||
if state.InInterfaceMethod() and last_code.type != Type.START_BLOCK:
|
|
||||||
self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE,
|
|
||||||
'Interface methods cannot contain code', last_code)
|
|
||||||
|
|
||||||
elif (state.IsBlockClose() and
|
|
||||||
token.next and token.next.type == Type.SEMICOLON):
|
|
||||||
if (last_code.metadata.context.parent.type != Context.OBJECT_LITERAL
|
|
||||||
and last_code.metadata.context.type != Context.OBJECT_LITERAL):
|
|
||||||
self._HandleError(
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
'No semicolon is required to end a code block',
|
|
||||||
token.next, position=Position.All(token.next.string))
|
|
||||||
|
|
||||||
elif token_type == Type.SEMICOLON:
|
|
||||||
if token.previous and token.previous.type == Type.WHITESPACE:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before ";"',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
if token.next and token.next.line_number == token.line_number:
|
|
||||||
if token.metadata.context.type != Context.FOR_GROUP_BLOCK:
|
|
||||||
# TODO(robbyw): Error about no multi-statement lines.
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif token.next.type not in (
|
|
||||||
Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SPACE,
|
|
||||||
'Missing space after ";" in for statement',
|
|
||||||
token.next,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if last_code and last_code.type == Type.SEMICOLON:
|
|
||||||
# Allow a single double semi colon in for loops for cases like:
|
|
||||||
# for (;;) { }.
|
|
||||||
# NOTE(user): This is not a perfect check, and will not throw an error
|
|
||||||
# for cases like: for (var i = 0;; i < n; i++) {}, but then your code
|
|
||||||
# probably won't work either.
|
|
||||||
for_token = tokenutil.CustomSearch(
|
|
||||||
last_code,
|
|
||||||
lambda token: token.type == Type.KEYWORD and token.string == 'for',
|
|
||||||
end_func=lambda token: token.type == Type.SEMICOLON,
|
|
||||||
distance=None,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
if not for_token:
|
|
||||||
self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
|
|
||||||
elif token_type == Type.START_PAREN:
|
|
||||||
# Ensure that opening parentheses have a space before any keyword
|
|
||||||
# that is not being invoked like a member function.
|
|
||||||
if (token.previous and token.previous.type == Type.KEYWORD and
|
|
||||||
(not token.previous.metadata or
|
|
||||||
not token.previous.metadata.last_code or
|
|
||||||
not token.previous.metadata.last_code.string or
|
|
||||||
token.previous.metadata.last_code.string[-1:] != '.')):
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space before "("',
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
elif token.previous and token.previous.type == Type.WHITESPACE:
|
|
||||||
before_space = token.previous.previous
|
|
||||||
# Ensure that there is no extra space before a function invocation,
|
|
||||||
# even if the function being invoked happens to be a keyword.
|
|
||||||
if (before_space and before_space.line_number == token.line_number and
|
|
||||||
before_space.type == Type.IDENTIFIER or
|
|
||||||
(before_space.type == Type.KEYWORD and before_space.metadata and
|
|
||||||
before_space.metadata.last_code and
|
|
||||||
before_space.metadata.last_code.string and
|
|
||||||
before_space.metadata.last_code.string[-1:] == '.')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "("',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif token_type == Type.START_BRACKET:
|
|
||||||
self._HandleStartBracket(token, last_non_space_token)
|
|
||||||
elif token_type in (Type.END_PAREN, Type.END_BRACKET):
|
|
||||||
# Ensure there is no space before closing parentheses, except when
|
|
||||||
# it's in a for statement with an omitted section, or when it's at the
|
|
||||||
# beginning of a line.
|
|
||||||
if (token.previous and token.previous.type == Type.WHITESPACE and
|
|
||||||
not token.previous.IsFirstInLine() and
|
|
||||||
not (last_non_space_token and last_non_space_token.line_number ==
|
|
||||||
token.line_number and
|
|
||||||
last_non_space_token.type == Type.SEMICOLON)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "%s"' %
|
|
||||||
token.string, token.previous,
|
|
||||||
position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif token_type == Type.WHITESPACE:
|
|
||||||
if self.ILLEGAL_TAB.search(token.string):
|
|
||||||
if token.IsFirstInLine():
|
|
||||||
if token.next:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace before "%s"' % token.next.string,
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
else:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
else:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace after "%s"' % token.previous.string,
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
|
|
||||||
# Check whitespace length if it's not the first token of the line and
|
|
||||||
# if it's not immediately before a comment.
|
|
||||||
if last_in_line:
|
|
||||||
# Check for extra whitespace at the end of a line.
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
elif not first_in_line and not token.next.IsComment():
|
|
||||||
if token.length > 1:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space after "%s"' %
|
|
||||||
token.previous.string, token,
|
|
||||||
position=Position(1, len(token.string) - 1))
|
|
||||||
|
|
||||||
elif token_type == Type.OPERATOR:
|
|
||||||
self._CheckOperator(token)
|
|
||||||
elif token_type == Type.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
|
|
||||||
if flag.flag_type == 'bug':
|
|
||||||
# TODO(robbyw): Check for exactly 1 space on the left.
|
|
||||||
string = token.next.string.lstrip()
|
|
||||||
string = string.split(' ', 1)[0]
|
|
||||||
|
|
||||||
if not string.isdigit():
|
|
||||||
self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG,
|
|
||||||
'@bug should be followed by a bug number', token)
|
|
||||||
|
|
||||||
elif flag.flag_type == 'suppress':
|
|
||||||
if flag.type is None:
|
|
||||||
# A syntactically invalid suppress tag will get tokenized as a normal
|
|
||||||
# flag, indicating an error.
|
|
||||||
self._HandleError(
|
|
||||||
errors.INCORRECT_SUPPRESS_SYNTAX,
|
|
||||||
'Invalid suppress syntax: should be @suppress {errortype}. '
|
|
||||||
'Spaces matter.', token)
|
|
||||||
else:
|
|
||||||
for suppress_type in flag.jstype.IterIdentifiers():
|
|
||||||
if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES:
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_SUPPRESS_TYPE,
|
|
||||||
'Invalid suppression type: %s' % suppress_type, token)
|
|
||||||
|
|
||||||
elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and
|
|
||||||
flag.flag_type == 'author'):
|
|
||||||
# TODO(user): In non strict mode check the author tag for as much as
|
|
||||||
# it exists, though the full form checked below isn't required.
|
|
||||||
string = token.next.string
|
|
||||||
result = self.AUTHOR_SPEC.match(string)
|
|
||||||
if not result:
|
|
||||||
self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION,
|
|
||||||
'Author tag line should be of the form: '
|
|
||||||
'@author foo@somewhere.com (Your Name)',
|
|
||||||
token.next)
|
|
||||||
else:
|
|
||||||
# Check spacing between email address and name. Do this before
|
|
||||||
# checking earlier spacing so positions are easier to calculate for
|
|
||||||
# autofixing.
|
|
||||||
num_spaces = len(result.group(2))
|
|
||||||
if num_spaces < 1:
|
|
||||||
self._HandleError(errors.MISSING_SPACE,
|
|
||||||
'Missing space after email address',
|
|
||||||
token.next, position=Position(result.start(2), 0))
|
|
||||||
elif num_spaces > 1:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space after email address',
|
|
||||||
token.next,
|
|
||||||
position=Position(result.start(2) + 1, num_spaces - 1))
|
|
||||||
|
|
||||||
# Check for extra spaces before email address. Can't be too few, if
|
|
||||||
# not at least one we wouldn't match @author tag.
|
|
||||||
num_spaces = len(result.group(1))
|
|
||||||
if num_spaces > 1:
|
|
||||||
self._HandleError(errors.EXTRA_SPACE,
|
|
||||||
'Extra space before email address',
|
|
||||||
token.next, position=Position(1, num_spaces - 1))
|
|
||||||
|
|
||||||
elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and
|
|
||||||
not self._limited_doc_checks):
|
|
||||||
if flag.flag_type == 'param':
|
|
||||||
if flag.name is None:
|
|
||||||
self._HandleError(errors.MISSING_JSDOC_PARAM_NAME,
|
|
||||||
'Missing name in @param tag', token)
|
|
||||||
|
|
||||||
if not flag.description or flag.description is None:
|
|
||||||
flag_name = token.type
|
|
||||||
if 'name' in token.values:
|
|
||||||
flag_name = '@' + token.values['name']
|
|
||||||
|
|
||||||
if flag_name not in self.JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED:
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_JSDOC_TAG_DESCRIPTION,
|
|
||||||
'Missing description in %s tag' % flag_name, token)
|
|
||||||
else:
|
|
||||||
self._CheckForMissingSpaceBeforeToken(flag.description_start_token)
|
|
||||||
|
|
||||||
if flag.HasType():
|
|
||||||
if flag.type_start_token is not None:
|
|
||||||
self._CheckForMissingSpaceBeforeToken(
|
|
||||||
token.attached_object.type_start_token)
|
|
||||||
|
|
||||||
if flag.jstype and not flag.jstype.IsEmpty():
|
|
||||||
self._CheckJsDocType(token, flag.jstype)
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and (
|
|
||||||
flag.type_start_token.type != Type.DOC_START_BRACE or
|
|
||||||
flag.type_end_token.type != Type.DOC_END_BRACE):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_BRACES_AROUND_TYPE,
|
|
||||||
'Type must always be surrounded by curly braces.', token)
|
|
||||||
|
|
||||||
if token_type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
|
|
||||||
if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and
|
|
||||||
token.values['name'] not in FLAGS.custom_jsdoc_tags):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_JSDOC_TAG,
|
|
||||||
'Invalid JsDoc tag: %s' % token.values['name'], token)
|
|
||||||
|
|
||||||
if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and
|
|
||||||
token.values['name'] == 'inheritDoc' and
|
|
||||||
token_type == Type.DOC_INLINE_FLAG):
|
|
||||||
self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC,
|
|
||||||
'Unnecessary braces around @inheritDoc',
|
|
||||||
token)
|
|
||||||
|
|
||||||
elif token_type == Type.SIMPLE_LVALUE:
|
|
||||||
identifier = token.values['identifier']
|
|
||||||
|
|
||||||
if ((not state.InFunction() or state.InConstructor()) and
|
|
||||||
state.InTopLevel() and not state.InObjectLiteralDescendant()):
|
|
||||||
jsdoc = state.GetDocComment()
|
|
||||||
if not state.HasDocComment(identifier):
|
|
||||||
# Only test for documentation on identifiers with .s in them to
|
|
||||||
# avoid checking things like simple variables. We don't require
|
|
||||||
# documenting assignments to .prototype itself (bug 1880803).
|
|
||||||
if (not state.InConstructor() and
|
|
||||||
identifier.find('.') != -1 and not
|
|
||||||
identifier.endswith('.prototype') and not
|
|
||||||
self._limited_doc_checks):
|
|
||||||
comment = state.GetLastComment()
|
|
||||||
if not (comment and comment.lower().count('jsdoc inherited')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_MEMBER_DOCUMENTATION,
|
|
||||||
"No docs found for member '%s'" % identifier,
|
|
||||||
token)
|
|
||||||
elif jsdoc and (not state.InConstructor() or
|
|
||||||
identifier.startswith('this.')):
|
|
||||||
# We are at the top level and the function/member is documented.
|
|
||||||
if identifier.endswith('_') and not identifier.endswith('__'):
|
|
||||||
# Can have a private class which inherits documentation from a
|
|
||||||
# public superclass.
|
|
||||||
#
|
|
||||||
# @inheritDoc is deprecated in favor of using @override, and they
|
|
||||||
if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor')
|
|
||||||
and ('accessControls' not in jsdoc.suppressions)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_OVERRIDE_PRIVATE,
|
|
||||||
'%s should not override a private member.' % identifier,
|
|
||||||
jsdoc.GetFlag('override').flag_token)
|
|
||||||
if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor')
|
|
||||||
and ('accessControls' not in jsdoc.suppressions)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_INHERIT_DOC_PRIVATE,
|
|
||||||
'%s should not inherit from a private member.' % identifier,
|
|
||||||
jsdoc.GetFlag('inheritDoc').flag_token)
|
|
||||||
if (not jsdoc.HasFlag('private') and
|
|
||||||
('underscore' not in jsdoc.suppressions) and not
|
|
||||||
((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and
|
|
||||||
('accessControls' in jsdoc.suppressions))):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_PRIVATE,
|
|
||||||
'Member "%s" must have @private JsDoc.' %
|
|
||||||
identifier, token)
|
|
||||||
if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions:
|
|
||||||
self._HandleError(
|
|
||||||
errors.UNNECESSARY_SUPPRESS,
|
|
||||||
'@suppress {underscore} is not necessary with @private',
|
|
||||||
jsdoc.suppressions['underscore'])
|
|
||||||
elif (jsdoc.HasFlag('private') and
|
|
||||||
not self.InExplicitlyTypedLanguage()):
|
|
||||||
# It is convention to hide public fields in some ECMA
|
|
||||||
# implementations from documentation using the @private tag.
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_PRIVATE,
|
|
||||||
'Member "%s" must not have @private JsDoc' %
|
|
||||||
identifier, token)
|
|
||||||
|
|
||||||
# These flags are only legal on localizable message definitions;
|
|
||||||
# such variables always begin with the prefix MSG_.
|
|
||||||
for f in ('desc', 'hidden', 'meaning'):
|
|
||||||
if (jsdoc.HasFlag(f)
|
|
||||||
and not identifier.startswith('MSG_')
|
|
||||||
and identifier.find('.MSG_') == -1):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_USE_OF_DESC_TAG,
|
|
||||||
'Member "%s" should not have @%s JsDoc' % (identifier, f),
|
|
||||||
token)
|
|
||||||
|
|
||||||
# Check for illegaly assigning live objects as prototype property values.
|
|
||||||
index = identifier.find('.prototype.')
|
|
||||||
# Ignore anything with additional .s after the prototype.
|
|
||||||
if index != -1 and identifier.find('.', index + 11) == -1:
|
|
||||||
equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
|
|
||||||
next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES)
|
|
||||||
if next_code and (
|
|
||||||
next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or
|
|
||||||
next_code.IsOperator('new')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE,
|
|
||||||
'Member %s cannot have a non-primitive value' % identifier,
|
|
||||||
token)
|
|
||||||
|
|
||||||
elif token_type == Type.END_PARAMETERS:
|
|
||||||
# Find extra space at the end of parameter lists. We check the token
|
|
||||||
# prior to the current one when it is a closing paren.
|
|
||||||
if (token.previous and token.previous.type == Type.PARAMETERS
|
|
||||||
and self.ENDS_WITH_SPACE.search(token.previous.string)):
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"',
|
|
||||||
token.previous)
|
|
||||||
|
|
||||||
jsdoc = state.GetDocComment()
|
|
||||||
if state.GetFunction().is_interface:
|
|
||||||
if token.previous and token.previous.type == Type.PARAMETERS:
|
|
||||||
self._HandleError(
|
|
||||||
errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS,
|
|
||||||
'Interface constructor cannot have parameters',
|
|
||||||
token.previous)
|
|
||||||
elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see')
|
|
||||||
and not jsdoc.InheritsDocumentation()
|
|
||||||
and not state.InObjectLiteralDescendant() and not
|
|
||||||
jsdoc.IsInvalidated()):
|
|
||||||
distance, edit = jsdoc.CompareParameters(state.GetParams())
|
|
||||||
if distance:
|
|
||||||
params_iter = iter(state.GetParams())
|
|
||||||
docs_iter = iter(jsdoc.ordered_params)
|
|
||||||
|
|
||||||
for op in edit:
|
|
||||||
if op == 'I':
|
|
||||||
# Insertion.
|
|
||||||
# Parsing doc comments is the same for all languages
|
|
||||||
# but some languages care about parameters that don't have
|
|
||||||
# doc comments and some languages don't care.
|
|
||||||
# Languages that don't allow variables to by typed such as
|
|
||||||
# JavaScript care but languages such as ActionScript or Java
|
|
||||||
# that allow variables to be typed don't care.
|
|
||||||
if not self._limited_doc_checks:
|
|
||||||
self.HandleMissingParameterDoc(token, params_iter.next())
|
|
||||||
|
|
||||||
elif op == 'D':
|
|
||||||
# Deletion
|
|
||||||
self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION,
|
|
||||||
'Found docs for non-existing parameter: "%s"' %
|
|
||||||
docs_iter.next(), token)
|
|
||||||
elif op == 'S':
|
|
||||||
# Substitution
|
|
||||||
if not self._limited_doc_checks:
|
|
||||||
self._HandleError(
|
|
||||||
errors.WRONG_PARAMETER_DOCUMENTATION,
|
|
||||||
'Parameter mismatch: got "%s", expected "%s"' %
|
|
||||||
(params_iter.next(), docs_iter.next()), token)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Equality - just advance the iterators
|
|
||||||
params_iter.next()
|
|
||||||
docs_iter.next()
|
|
||||||
|
|
||||||
elif token_type == Type.STRING_TEXT:
|
|
||||||
# If this is the first token after the start of the string, but it's at
|
|
||||||
# the end of a line, we know we have a multi-line string.
|
|
||||||
if token.previous.type in (
|
|
||||||
Type.SINGLE_QUOTE_STRING_START,
|
|
||||||
Type.DOUBLE_QUOTE_STRING_START) and last_in_line:
|
|
||||||
self._HandleError(errors.MULTI_LINE_STRING,
|
|
||||||
'Multi-line strings are not allowed', token)
|
|
||||||
|
|
||||||
# This check is orthogonal to the ones above, and repeats some types, so
|
|
||||||
# it is a plain if and not an elif.
|
|
||||||
if token.type in Type.COMMENT_TYPES:
|
|
||||||
if self.ILLEGAL_TAB.search(token.string):
|
|
||||||
self._HandleError(errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in comment "%s"' % token.string, token)
|
|
||||||
|
|
||||||
trimmed = token.string.rstrip()
|
|
||||||
if last_in_line and token.string != trimmed:
|
|
||||||
# Check for extra whitespace at the end of a line.
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space at end of line', token,
|
|
||||||
position=Position(len(trimmed), len(token.string) - len(trimmed)))
|
|
||||||
|
|
||||||
# This check is also orthogonal since it is based on metadata.
|
|
||||||
if token.metadata.is_implied_semicolon:
|
|
||||||
self._HandleError(errors.MISSING_SEMICOLON,
|
|
||||||
'Missing semicolon at end of line', token)
|
|
||||||
|
|
||||||
def _HandleStartBracket(self, token, last_non_space_token):
|
|
||||||
"""Handles a token that is an open bracket.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to handle.
|
|
||||||
last_non_space_token: The last token that was not a space.
|
|
||||||
"""
|
|
||||||
if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and
|
|
||||||
last_non_space_token and
|
|
||||||
last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "["',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
# If the [ token is the first token in a line we shouldn't complain
|
|
||||||
# about a missing space before [. This is because some Ecma script
|
|
||||||
# languages allow syntax like:
|
|
||||||
# [Annotation]
|
|
||||||
# class MyClass {...}
|
|
||||||
# So we don't want to blindly warn about missing spaces before [.
|
|
||||||
# In the the future, when rules for computing exactly how many spaces
|
|
||||||
# lines should be indented are added, then we can return errors for
|
|
||||||
# [ tokens that are improperly indented.
|
|
||||||
# For example:
|
|
||||||
# var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName =
|
|
||||||
# [a,b,c];
|
|
||||||
# should trigger a proper indentation warning message as [ is not indented
|
|
||||||
# by four spaces.
|
|
||||||
elif (not token.IsFirstInLine() and token.previous and
|
|
||||||
token.previous.type not in (
|
|
||||||
[Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] +
|
|
||||||
Type.EXPRESSION_ENDER_TYPES)):
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space before "["',
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
def Finalize(self, state):
|
|
||||||
"""Perform all checks that need to occur after all lines are processed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: State of the parser after parsing all tokens
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
last_non_space_token = state.GetLastNonSpaceToken()
|
|
||||||
# Check last line for ending with newline.
|
|
||||||
if state.GetLastLine() and not (
|
|
||||||
state.GetLastLine().isspace() or
|
|
||||||
state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()):
|
|
||||||
self._HandleError(
|
|
||||||
errors.FILE_MISSING_NEWLINE,
|
|
||||||
'File does not end with new line. (%s)' % state.GetLastLine(),
|
|
||||||
last_non_space_token)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._indentation.Finalize()
|
|
||||||
except Exception, e:
|
|
||||||
self._HandleError(
|
|
||||||
errors.FILE_DOES_NOT_PARSE,
|
|
||||||
str(e),
|
|
||||||
last_non_space_token)
|
|
||||||
|
|
||||||
def GetLongLineExceptions(self):
|
|
||||||
"""Gets a list of regexps for lines which can be longer than the limit.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of regexps, used as matches (rather than searches).
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def InExplicitlyTypedLanguage(self):
|
|
||||||
"""Returns whether this ecma implementation is explicitly typed."""
|
|
||||||
return False
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Metadata pass for annotating tokens in EcmaScript files."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)')
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
|
||||||
"""Exception indicating a parse error at the given token.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
token: The token where the parse error occurred.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, token, message=None):
|
|
||||||
"""Initialize a parse error at the given token with an optional message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token where the parse error occurred.
|
|
||||||
message: A message describing the parse error.
|
|
||||||
"""
|
|
||||||
Exception.__init__(self, message)
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaContext(object):
|
|
||||||
"""Context object for EcmaScript languages.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
end_token: The token where this context ends.
|
|
||||||
parent: The parent context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The root context.
|
|
||||||
ROOT = 'root'
|
|
||||||
|
|
||||||
# A block of code.
|
|
||||||
BLOCK = 'block'
|
|
||||||
|
|
||||||
# A pseudo-block of code for a given case or default section.
|
|
||||||
CASE_BLOCK = 'case_block'
|
|
||||||
|
|
||||||
# Block of statements in a for loop's parentheses.
|
|
||||||
FOR_GROUP_BLOCK = 'for_block'
|
|
||||||
|
|
||||||
# An implied block of code for 1 line if, while, and for statements
|
|
||||||
IMPLIED_BLOCK = 'implied_block'
|
|
||||||
|
|
||||||
# An index in to an array or object.
|
|
||||||
INDEX = 'index'
|
|
||||||
|
|
||||||
# An array literal in [].
|
|
||||||
ARRAY_LITERAL = 'array_literal'
|
|
||||||
|
|
||||||
# An object literal in {}.
|
|
||||||
OBJECT_LITERAL = 'object_literal'
|
|
||||||
|
|
||||||
# An individual element in an array or object literal.
|
|
||||||
LITERAL_ELEMENT = 'literal_element'
|
|
||||||
|
|
||||||
# The portion of a ternary statement between ? and :
|
|
||||||
TERNARY_TRUE = 'ternary_true'
|
|
||||||
|
|
||||||
# The portion of a ternary statment after :
|
|
||||||
TERNARY_FALSE = 'ternary_false'
|
|
||||||
|
|
||||||
# The entire switch statment. This will contain a GROUP with the variable
|
|
||||||
# and a BLOCK with the code.
|
|
||||||
|
|
||||||
# Since that BLOCK is not a normal block, it can not contain statements except
|
|
||||||
# for case and default.
|
|
||||||
SWITCH = 'switch'
|
|
||||||
|
|
||||||
# A normal comment.
|
|
||||||
COMMENT = 'comment'
|
|
||||||
|
|
||||||
# A JsDoc comment.
|
|
||||||
DOC = 'doc'
|
|
||||||
|
|
||||||
# An individual statement.
|
|
||||||
STATEMENT = 'statement'
|
|
||||||
|
|
||||||
# Code within parentheses.
|
|
||||||
GROUP = 'group'
|
|
||||||
|
|
||||||
# Parameter names in a function declaration.
|
|
||||||
PARAMETERS = 'parameters'
|
|
||||||
|
|
||||||
# A set of variable declarations appearing after the 'var' keyword.
|
|
||||||
VAR = 'var'
|
|
||||||
|
|
||||||
# Context types that are blocks.
|
|
||||||
BLOCK_TYPES = frozenset([
|
|
||||||
ROOT, BLOCK, CASE_BLOCK, FOR_GROUP_BLOCK, IMPLIED_BLOCK])
|
|
||||||
|
|
||||||
def __init__(self, context_type, start_token, parent=None):
|
|
||||||
"""Initializes the context object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context_type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
parent: The parent context.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
end_token: The token where this context ends.
|
|
||||||
parent: The parent context.
|
|
||||||
children: The child contexts of this context, in order.
|
|
||||||
"""
|
|
||||||
self.type = context_type
|
|
||||||
self.start_token = start_token
|
|
||||||
self.end_token = None
|
|
||||||
|
|
||||||
self.parent = None
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
parent.AddChild(self)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Returns a string representation of the context object."""
|
|
||||||
stack = []
|
|
||||||
context = self
|
|
||||||
while context:
|
|
||||||
stack.append(context.type)
|
|
||||||
context = context.parent
|
|
||||||
return 'Context(%s)' % ' > '.join(stack)
|
|
||||||
|
|
||||||
def AddChild(self, child):
|
|
||||||
"""Adds a child to this context and sets child's parent to this context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
child: A child EcmaContext. The child's parent will be set to this
|
|
||||||
context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
child.parent = self
|
|
||||||
|
|
||||||
self.children.append(child)
|
|
||||||
self.children.sort(EcmaContext._CompareContexts)
|
|
||||||
|
|
||||||
def GetRoot(self):
|
|
||||||
"""Get the root context that contains this context, if any."""
|
|
||||||
context = self
|
|
||||||
while context:
|
|
||||||
if context.type is EcmaContext.ROOT:
|
|
||||||
return context
|
|
||||||
context = context.parent
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _CompareContexts(context1, context2):
|
|
||||||
"""Sorts contexts 1 and 2 by start token document position."""
|
|
||||||
return tokenutil.Compare(context1.start_token, context2.start_token)
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaMetaData(object):
|
|
||||||
"""Token metadata for EcmaScript languages.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
last_code: The last code token to appear before this one.
|
|
||||||
context: The context this token appears in.
|
|
||||||
operator_type: The operator type, will be one of the *_OPERATOR constants
|
|
||||||
defined below.
|
|
||||||
aliased_symbol: The full symbol being identified, as a string (e.g. an
|
|
||||||
'XhrIo' alias for 'goog.net.XhrIo'). Only applicable to identifier
|
|
||||||
tokens. This is set in aliaspass.py and is a best guess.
|
|
||||||
is_alias_definition: True if the symbol is part of an alias definition.
|
|
||||||
If so, these symbols won't be counted towards goog.requires/provides.
|
|
||||||
"""
|
|
||||||
|
|
||||||
UNARY_OPERATOR = 'unary'
|
|
||||||
|
|
||||||
UNARY_POST_OPERATOR = 'unary_post'
|
|
||||||
|
|
||||||
BINARY_OPERATOR = 'binary'
|
|
||||||
|
|
||||||
TERNARY_OPERATOR = 'ternary'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initializes a token metadata object."""
|
|
||||||
self.last_code = None
|
|
||||||
self.context = None
|
|
||||||
self.operator_type = None
|
|
||||||
self.is_implied_semicolon = False
|
|
||||||
self.is_implied_block = False
|
|
||||||
self.is_implied_block_close = False
|
|
||||||
self.aliased_symbol = None
|
|
||||||
self.is_alias_definition = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Returns a string representation of the context object."""
|
|
||||||
parts = ['%r' % self.context]
|
|
||||||
if self.operator_type:
|
|
||||||
parts.append('optype: %r' % self.operator_type)
|
|
||||||
if self.is_implied_semicolon:
|
|
||||||
parts.append('implied;')
|
|
||||||
if self.aliased_symbol:
|
|
||||||
parts.append('alias for: %s' % self.aliased_symbol)
|
|
||||||
return 'MetaData(%s)' % ', '.join(parts)
|
|
||||||
|
|
||||||
def IsUnaryOperator(self):
|
|
||||||
return self.operator_type in (EcmaMetaData.UNARY_OPERATOR,
|
|
||||||
EcmaMetaData.UNARY_POST_OPERATOR)
|
|
||||||
|
|
||||||
def IsUnaryPostOperator(self):
|
|
||||||
return self.operator_type == EcmaMetaData.UNARY_POST_OPERATOR
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaMetaDataPass(object):
|
|
||||||
"""A pass that iterates over all tokens and builds metadata about them."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the meta data pass object."""
|
|
||||||
self.Reset()
|
|
||||||
|
|
||||||
def Reset(self):
|
|
||||||
"""Resets the metadata pass to prepare for the next file."""
|
|
||||||
self._token = None
|
|
||||||
self._context = None
|
|
||||||
self._AddContext(EcmaContext.ROOT)
|
|
||||||
self._last_code = None
|
|
||||||
|
|
||||||
def _CreateContext(self, context_type):
|
|
||||||
"""Overridable by subclasses to create the appropriate context type."""
|
|
||||||
return EcmaContext(context_type, self._token, self._context)
|
|
||||||
|
|
||||||
def _CreateMetaData(self):
|
|
||||||
"""Overridable by subclasses to create the appropriate metadata type."""
|
|
||||||
return EcmaMetaData()
|
|
||||||
|
|
||||||
def _AddContext(self, context_type):
|
|
||||||
"""Adds a context of the given type to the context stack.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context_type: The type of context to create
|
|
||||||
"""
|
|
||||||
self._context = self._CreateContext(context_type)
|
|
||||||
|
|
||||||
def _PopContext(self):
|
|
||||||
"""Moves up one level in the context stack.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The former context.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ParseError: If the root context is popped.
|
|
||||||
"""
|
|
||||||
top_context = self._context
|
|
||||||
top_context.end_token = self._token
|
|
||||||
self._context = top_context.parent
|
|
||||||
if self._context:
|
|
||||||
return top_context
|
|
||||||
else:
|
|
||||||
raise ParseError(self._token)
|
|
||||||
|
|
||||||
def _PopContextType(self, *stop_types):
|
|
||||||
"""Pops the context stack until a context of the given type is popped.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
*stop_types: The types of context to pop to - stops at the first match.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The context object of the given type that was popped.
|
|
||||||
"""
|
|
||||||
last = None
|
|
||||||
while not last or last.type not in stop_types:
|
|
||||||
last = self._PopContext()
|
|
||||||
return last
|
|
||||||
|
|
||||||
def _EndStatement(self):
|
|
||||||
"""Process the end of a statement."""
|
|
||||||
self._PopContextType(EcmaContext.STATEMENT)
|
|
||||||
if self._context.type == EcmaContext.IMPLIED_BLOCK:
|
|
||||||
self._token.metadata.is_implied_block_close = True
|
|
||||||
self._PopContext()
|
|
||||||
|
|
||||||
def _ProcessContext(self):
|
|
||||||
"""Process the context at the current token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The context that should be assigned to the current token, or None if
|
|
||||||
the current context after this method should be used.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ParseError: When the token appears in an invalid context.
|
|
||||||
"""
|
|
||||||
token = self._token
|
|
||||||
token_type = token.type
|
|
||||||
|
|
||||||
if self._context.type in EcmaContext.BLOCK_TYPES:
|
|
||||||
# Whenever we're in a block, we add a statement context. We make an
|
|
||||||
# exception for switch statements since they can only contain case: and
|
|
||||||
# default: and therefore don't directly contain statements.
|
|
||||||
# The block we add here may be immediately removed in some cases, but
|
|
||||||
# that causes no harm.
|
|
||||||
parent = self._context.parent
|
|
||||||
if not parent or parent.type != EcmaContext.SWITCH:
|
|
||||||
self._AddContext(EcmaContext.STATEMENT)
|
|
||||||
|
|
||||||
elif self._context.type == EcmaContext.ARRAY_LITERAL:
|
|
||||||
self._AddContext(EcmaContext.LITERAL_ELEMENT)
|
|
||||||
|
|
||||||
if token_type == TokenType.START_PAREN:
|
|
||||||
if self._last_code and self._last_code.IsKeyword('for'):
|
|
||||||
# for loops contain multiple statements in the group unlike while,
|
|
||||||
# switch, if, etc.
|
|
||||||
self._AddContext(EcmaContext.FOR_GROUP_BLOCK)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.GROUP)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_PAREN:
|
|
||||||
result = self._PopContextType(EcmaContext.GROUP,
|
|
||||||
EcmaContext.FOR_GROUP_BLOCK)
|
|
||||||
keyword_token = result.start_token.metadata.last_code
|
|
||||||
# keyword_token will not exist if the open paren is the first line of the
|
|
||||||
# file, for example if all code is wrapped in an immediately executed
|
|
||||||
# annonymous function.
|
|
||||||
if keyword_token and keyword_token.string in ('if', 'for', 'while'):
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
if next_code.type != TokenType.START_BLOCK:
|
|
||||||
# Check for do-while.
|
|
||||||
is_do_while = False
|
|
||||||
pre_keyword_token = keyword_token.metadata.last_code
|
|
||||||
if (pre_keyword_token and
|
|
||||||
pre_keyword_token.type == TokenType.END_BLOCK):
|
|
||||||
start_block_token = pre_keyword_token.metadata.context.start_token
|
|
||||||
is_do_while = start_block_token.metadata.last_code.string == 'do'
|
|
||||||
|
|
||||||
# If it's not do-while, it's an implied block.
|
|
||||||
if not is_do_while:
|
|
||||||
self._AddContext(EcmaContext.IMPLIED_BLOCK)
|
|
||||||
token.metadata.is_implied_block = True
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
# else (not else if) with no open brace after it should be considered the
|
|
||||||
# start of an implied block, similar to the case with if, for, and while
|
|
||||||
# above.
|
|
||||||
elif (token_type == TokenType.KEYWORD and
|
|
||||||
token.string == 'else'):
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
if (next_code.type != TokenType.START_BLOCK and
|
|
||||||
(next_code.type != TokenType.KEYWORD or next_code.string != 'if')):
|
|
||||||
self._AddContext(EcmaContext.IMPLIED_BLOCK)
|
|
||||||
token.metadata.is_implied_block = True
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_PARAMETERS:
|
|
||||||
self._AddContext(EcmaContext.PARAMETERS)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_PARAMETERS:
|
|
||||||
return self._PopContextType(EcmaContext.PARAMETERS)
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_BRACKET:
|
|
||||||
if (self._last_code and
|
|
||||||
self._last_code.type in TokenType.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._AddContext(EcmaContext.INDEX)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.ARRAY_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_BRACKET:
|
|
||||||
return self._PopContextType(EcmaContext.INDEX, EcmaContext.ARRAY_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_BLOCK:
|
|
||||||
if (self._last_code.type in (TokenType.END_PAREN,
|
|
||||||
TokenType.END_PARAMETERS) or
|
|
||||||
self._last_code.IsKeyword('else') or
|
|
||||||
self._last_code.IsKeyword('do') or
|
|
||||||
self._last_code.IsKeyword('try') or
|
|
||||||
self._last_code.IsKeyword('finally') or
|
|
||||||
(self._last_code.IsOperator(':') and
|
|
||||||
self._last_code.metadata.context.type == EcmaContext.CASE_BLOCK)):
|
|
||||||
# else, do, try, and finally all might have no () before {.
|
|
||||||
# Also, handle the bizzare syntax case 10: {...}.
|
|
||||||
self._AddContext(EcmaContext.BLOCK)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.OBJECT_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_BLOCK:
|
|
||||||
context = self._PopContextType(EcmaContext.BLOCK,
|
|
||||||
EcmaContext.OBJECT_LITERAL)
|
|
||||||
if self._context.type == EcmaContext.SWITCH:
|
|
||||||
# The end of the block also means the end of the switch statement it
|
|
||||||
# applies to.
|
|
||||||
return self._PopContext()
|
|
||||||
return context
|
|
||||||
|
|
||||||
elif token.IsKeyword('switch'):
|
|
||||||
self._AddContext(EcmaContext.SWITCH)
|
|
||||||
|
|
||||||
elif (token_type == TokenType.KEYWORD and
|
|
||||||
token.string in ('case', 'default') and
|
|
||||||
self._context.type != EcmaContext.OBJECT_LITERAL):
|
|
||||||
# Pop up to but not including the switch block.
|
|
||||||
while self._context.parent.type != EcmaContext.SWITCH:
|
|
||||||
self._PopContext()
|
|
||||||
if self._context.parent is None:
|
|
||||||
raise ParseError(token, 'Encountered case/default statement '
|
|
||||||
'without switch statement')
|
|
||||||
|
|
||||||
elif token.IsOperator('?'):
|
|
||||||
self._AddContext(EcmaContext.TERNARY_TRUE)
|
|
||||||
|
|
||||||
elif token.IsOperator(':'):
|
|
||||||
if self._context.type == EcmaContext.OBJECT_LITERAL:
|
|
||||||
self._AddContext(EcmaContext.LITERAL_ELEMENT)
|
|
||||||
|
|
||||||
elif self._context.type == EcmaContext.TERNARY_TRUE:
|
|
||||||
self._PopContext()
|
|
||||||
self._AddContext(EcmaContext.TERNARY_FALSE)
|
|
||||||
|
|
||||||
# Handle nested ternary statements like:
|
|
||||||
# foo = bar ? baz ? 1 : 2 : 3
|
|
||||||
# When we encounter the second ":" the context is
|
|
||||||
# ternary_false > ternary_true > statement > root
|
|
||||||
elif (self._context.type == EcmaContext.TERNARY_FALSE and
|
|
||||||
self._context.parent.type == EcmaContext.TERNARY_TRUE):
|
|
||||||
self._PopContext() # Leave current ternary false context.
|
|
||||||
self._PopContext() # Leave current parent ternary true
|
|
||||||
self._AddContext(EcmaContext.TERNARY_FALSE)
|
|
||||||
|
|
||||||
elif self._context.parent.type == EcmaContext.SWITCH:
|
|
||||||
self._AddContext(EcmaContext.CASE_BLOCK)
|
|
||||||
|
|
||||||
elif token.IsKeyword('var'):
|
|
||||||
self._AddContext(EcmaContext.VAR)
|
|
||||||
|
|
||||||
elif token.IsOperator(','):
|
|
||||||
while self._context.type not in (EcmaContext.VAR,
|
|
||||||
EcmaContext.ARRAY_LITERAL,
|
|
||||||
EcmaContext.OBJECT_LITERAL,
|
|
||||||
EcmaContext.STATEMENT,
|
|
||||||
EcmaContext.PARAMETERS,
|
|
||||||
EcmaContext.GROUP):
|
|
||||||
self._PopContext()
|
|
||||||
|
|
||||||
elif token_type == TokenType.SEMICOLON:
|
|
||||||
self._EndStatement()
|
|
||||||
|
|
||||||
def Process(self, first_token):
|
|
||||||
"""Processes the token stream starting with the given token."""
|
|
||||||
self._token = first_token
|
|
||||||
while self._token:
|
|
||||||
self._ProcessToken()
|
|
||||||
|
|
||||||
if self._token.IsCode():
|
|
||||||
self._last_code = self._token
|
|
||||||
|
|
||||||
self._token = self._token.next
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._PopContextType(self, EcmaContext.ROOT)
|
|
||||||
except ParseError:
|
|
||||||
# Ignore the "popped to root" error.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _ProcessToken(self):
|
|
||||||
"""Process the given token."""
|
|
||||||
token = self._token
|
|
||||||
token.metadata = self._CreateMetaData()
|
|
||||||
context = (self._ProcessContext() or self._context)
|
|
||||||
token.metadata.context = context
|
|
||||||
token.metadata.last_code = self._last_code
|
|
||||||
|
|
||||||
# Determine the operator type of the token, if applicable.
|
|
||||||
if token.type == TokenType.OPERATOR:
|
|
||||||
token.metadata.operator_type = self._GetOperatorType(token)
|
|
||||||
|
|
||||||
# Determine if there is an implied semicolon after the token.
|
|
||||||
if token.type != TokenType.SEMICOLON:
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
# A statement like if (x) does not need a semicolon after it
|
|
||||||
is_implied_block = self._context == EcmaContext.IMPLIED_BLOCK
|
|
||||||
is_last_code_in_line = token.IsCode() and (
|
|
||||||
not next_code or next_code.line_number != token.line_number)
|
|
||||||
is_continued_operator = (token.type == TokenType.OPERATOR and
|
|
||||||
not token.metadata.IsUnaryPostOperator())
|
|
||||||
is_continued_dot = token.string == '.'
|
|
||||||
next_code_is_operator = next_code and next_code.type == TokenType.OPERATOR
|
|
||||||
is_end_of_block = (
|
|
||||||
token.type == TokenType.END_BLOCK and
|
|
||||||
token.metadata.context.type != EcmaContext.OBJECT_LITERAL)
|
|
||||||
is_multiline_string = token.type == TokenType.STRING_TEXT
|
|
||||||
is_continued_var_decl = (token.IsKeyword('var') and
|
|
||||||
next_code and
|
|
||||||
(next_code.type in [TokenType.IDENTIFIER,
|
|
||||||
TokenType.SIMPLE_LVALUE]) and
|
|
||||||
token.line_number < next_code.line_number)
|
|
||||||
next_code_is_block = next_code and next_code.type == TokenType.START_BLOCK
|
|
||||||
if (is_last_code_in_line and
|
|
||||||
self._StatementCouldEndInContext() and
|
|
||||||
not is_multiline_string and
|
|
||||||
not is_end_of_block and
|
|
||||||
not is_continued_var_decl and
|
|
||||||
not is_continued_operator and
|
|
||||||
not is_continued_dot and
|
|
||||||
not next_code_is_operator and
|
|
||||||
not is_implied_block and
|
|
||||||
not next_code_is_block):
|
|
||||||
token.metadata.is_implied_semicolon = True
|
|
||||||
self._EndStatement()
|
|
||||||
|
|
||||||
def _StatementCouldEndInContext(self):
|
|
||||||
"""Returns if the current statement (if any) may end in this context."""
|
|
||||||
# In the basic statement or variable declaration context, statement can
|
|
||||||
# always end in this context.
|
|
||||||
if self._context.type in (EcmaContext.STATEMENT, EcmaContext.VAR):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# End of a ternary false branch inside a statement can also be the
|
|
||||||
# end of the statement, for example:
|
|
||||||
# var x = foo ? foo.bar() : null
|
|
||||||
# In this case the statement ends after the null, when the context stack
|
|
||||||
# looks like ternary_false > var > statement > root.
|
|
||||||
if (self._context.type == EcmaContext.TERNARY_FALSE and
|
|
||||||
self._context.parent.type in (EcmaContext.STATEMENT, EcmaContext.VAR)):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# In all other contexts like object and array literals, ternary true, etc.
|
|
||||||
# the statement can't yet end.
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _GetOperatorType(self, token):
|
|
||||||
"""Returns the operator type of the given operator token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to get arity for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The type of the operator. One of the *_OPERATOR constants defined in
|
|
||||||
EcmaMetaData.
|
|
||||||
"""
|
|
||||||
if token.string == '?':
|
|
||||||
return EcmaMetaData.TERNARY_OPERATOR
|
|
||||||
|
|
||||||
if token.string in TokenType.UNARY_OPERATORS:
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if not last_code or last_code.type == TokenType.END_BLOCK:
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
if (token.string in TokenType.UNARY_POST_OPERATORS and
|
|
||||||
last_code.type in TokenType.EXPRESSION_ENDER_TYPES):
|
|
||||||
return EcmaMetaData.UNARY_POST_OPERATOR
|
|
||||||
|
|
||||||
if (token.string in TokenType.UNARY_OK_OPERATORS and
|
|
||||||
last_code.type not in TokenType.EXPRESSION_ENDER_TYPES and
|
|
||||||
last_code.string not in TokenType.UNARY_POST_OPERATORS):
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
return EcmaMetaData.BINARY_OPERATOR
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
|
|
||||||
"""Specific JSLint errors checker."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
|
|
||||||
class Rule(object):
|
|
||||||
"""Different rules to check."""
|
|
||||||
|
|
||||||
# Documentations for specific rules goes in flag definition.
|
|
||||||
BLANK_LINES_AT_TOP_LEVEL = 'blank_lines_at_top_level'
|
|
||||||
INDENTATION = 'indentation'
|
|
||||||
WELL_FORMED_AUTHOR = 'well_formed_author'
|
|
||||||
NO_BRACES_AROUND_INHERIT_DOC = 'no_braces_around_inherit_doc'
|
|
||||||
BRACES_AROUND_TYPE = 'braces_around_type'
|
|
||||||
OPTIONAL_TYPE_MARKER = 'optional_type_marker'
|
|
||||||
VARIABLE_ARG_MARKER = 'variable_arg_marker'
|
|
||||||
UNUSED_PRIVATE_MEMBERS = 'unused_private_members'
|
|
||||||
UNUSED_LOCAL_VARIABLES = 'unused_local_variables'
|
|
||||||
|
|
||||||
# Rule to raise all known errors.
|
|
||||||
ALL = 'all'
|
|
||||||
|
|
||||||
# All rules that are to be checked when using the strict flag. E.g. the rules
|
|
||||||
# that are specific to the stricter Closure style.
|
|
||||||
CLOSURE_RULES = frozenset([BLANK_LINES_AT_TOP_LEVEL,
|
|
||||||
INDENTATION,
|
|
||||||
WELL_FORMED_AUTHOR,
|
|
||||||
NO_BRACES_AROUND_INHERIT_DOC,
|
|
||||||
BRACES_AROUND_TYPE,
|
|
||||||
OPTIONAL_TYPE_MARKER,
|
|
||||||
VARIABLE_ARG_MARKER])
|
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_boolean('strict', False,
|
|
||||||
'Whether to validate against the stricter Closure style. '
|
|
||||||
'This includes ' + (', '.join(Rule.CLOSURE_RULES)) + '.')
|
|
||||||
flags.DEFINE_multistring('jslint_error', [],
|
|
||||||
'List of specific lint errors to check. Here is a list'
|
|
||||||
' of accepted values:\n'
|
|
||||||
' - ' + Rule.ALL + ': enables all following errors.\n'
|
|
||||||
' - ' + Rule.BLANK_LINES_AT_TOP_LEVEL + ': validates'
|
|
||||||
'number of blank lines between blocks at top level.\n'
|
|
||||||
' - ' + Rule.INDENTATION + ': checks correct '
|
|
||||||
'indentation of code.\n'
|
|
||||||
' - ' + Rule.WELL_FORMED_AUTHOR + ': validates the '
|
|
||||||
'@author JsDoc tags.\n'
|
|
||||||
' - ' + Rule.NO_BRACES_AROUND_INHERIT_DOC + ': '
|
|
||||||
'forbids braces around @inheritdoc JsDoc tags.\n'
|
|
||||||
' - ' + Rule.BRACES_AROUND_TYPE + ': enforces braces '
|
|
||||||
'around types in JsDoc tags.\n'
|
|
||||||
' - ' + Rule.OPTIONAL_TYPE_MARKER + ': checks correct '
|
|
||||||
'use of optional marker = in param types.\n'
|
|
||||||
' - ' + Rule.UNUSED_PRIVATE_MEMBERS + ': checks for '
|
|
||||||
'unused private variables.\n'
|
|
||||||
' - ' + Rule.UNUSED_LOCAL_VARIABLES + ': checks for '
|
|
||||||
'unused local variables.\n')
|
|
||||||
|
|
||||||
|
|
||||||
def ShouldCheck(rule):
|
|
||||||
"""Returns whether the optional rule should be checked.
|
|
||||||
|
|
||||||
Computes different flags (strict, jslint_error, jslint_noerror) to find out if
|
|
||||||
this specific rule should be checked.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rule: Name of the rule (see Rule).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the rule should be checked according to the flags, otherwise False.
|
|
||||||
"""
|
|
||||||
if rule in FLAGS.jslint_error or Rule.ALL in FLAGS.jslint_error:
|
|
||||||
return True
|
|
||||||
# Checks strict rules.
|
|
||||||
return FLAGS.strict and rule in Rule.CLOSURE_RULES
|
|
||||||
@@ -1,618 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Main class responsible for automatically fixing simple style violations."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import requireprovidesorter
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import errorhandler
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Token = javascripttokens.JavaScriptToken
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$')
|
|
||||||
|
|
||||||
# Regex to represent common mistake inverting author name and email as
|
|
||||||
# @author User Name (user@company)
|
|
||||||
INVERTED_AUTHOR_SPEC = re.compile(r'(?P<leading_whitespace>\s*)'
|
|
||||||
r'(?P<name>[^(]+)'
|
|
||||||
r'(?P<whitespace_after_name>\s+)'
|
|
||||||
r'\('
|
|
||||||
r'(?P<email>[^\s]+@[^)\s]+)'
|
|
||||||
r'\)'
|
|
||||||
r'(?P<trailing_characters>.*)')
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('disable_indentation_fixing', False,
|
|
||||||
'Whether to disable automatic fixing of indentation.')
|
|
||||||
flags.DEFINE_list('fix_error_codes', [], 'A list of specific error codes to '
|
|
||||||
'fix. Defaults to all supported error codes when empty. '
|
|
||||||
'See errors.py for a list of error codes.')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorFixer(errorhandler.ErrorHandler):
|
|
||||||
"""Object that fixes simple style errors."""
|
|
||||||
|
|
||||||
def __init__(self, external_file=None):
|
|
||||||
"""Initialize the error fixer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
external_file: If included, all output will be directed to this file
|
|
||||||
instead of overwriting the files the errors are found in.
|
|
||||||
"""
|
|
||||||
errorhandler.ErrorHandler.__init__(self)
|
|
||||||
|
|
||||||
self._file_name = None
|
|
||||||
self._file_token = None
|
|
||||||
self._external_file = external_file
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._fix_error_codes = set([errors.ByName(error.upper()) for error in
|
|
||||||
FLAGS.fix_error_codes])
|
|
||||||
except KeyError as ke:
|
|
||||||
raise ValueError('Unknown error code ' + ke.args[0])
|
|
||||||
|
|
||||||
def HandleFile(self, filename, first_token):
|
|
||||||
"""Notifies this ErrorPrinter that subsequent errors are in filename.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The name of the file about to be checked.
|
|
||||||
first_token: The first token in the file.
|
|
||||||
"""
|
|
||||||
self._file_name = filename
|
|
||||||
self._file_is_html = filename.endswith('.html') or filename.endswith('.htm')
|
|
||||||
self._file_token = first_token
|
|
||||||
self._file_fix_count = 0
|
|
||||||
self._file_changed_lines = set()
|
|
||||||
|
|
||||||
def _AddFix(self, tokens):
|
|
||||||
"""Adds the fix to the internal count.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: The token or sequence of tokens changed to fix an error.
|
|
||||||
"""
|
|
||||||
self._file_fix_count += 1
|
|
||||||
if hasattr(tokens, 'line_number'):
|
|
||||||
self._file_changed_lines.add(tokens.line_number)
|
|
||||||
else:
|
|
||||||
for token in tokens:
|
|
||||||
self._file_changed_lines.add(token.line_number)
|
|
||||||
|
|
||||||
def _FixJsDocPipeNull(self, js_type):
|
|
||||||
"""Change number|null or null|number to ?number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
js_type: The typeannotation.TypeAnnotation instance to fix.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Recurse into all sub_types if the error was at a deeper level.
|
|
||||||
map(self._FixJsDocPipeNull, js_type.IterTypes())
|
|
||||||
|
|
||||||
if js_type.type_group and len(js_type.sub_types) == 2:
|
|
||||||
# Find and remove the null sub_type:
|
|
||||||
sub_type = None
|
|
||||||
for sub_type in js_type.sub_types:
|
|
||||||
if sub_type.identifier == 'null':
|
|
||||||
map(tokenutil.DeleteToken, sub_type.tokens)
|
|
||||||
self._AddFix(sub_type.tokens)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
first_token = js_type.FirstToken()
|
|
||||||
question_mark = Token('?', Type.DOC_TYPE_MODIFIER, first_token.line,
|
|
||||||
first_token.line_number)
|
|
||||||
tokenutil.InsertTokenBefore(question_mark, first_token)
|
|
||||||
js_type.tokens.insert(0, question_mark)
|
|
||||||
js_type.tokens.remove(sub_type)
|
|
||||||
js_type.or_null = True
|
|
||||||
|
|
||||||
# Now also remove the separator, which is in the parent's token list,
|
|
||||||
# either before or after the sub_type, there is exactly one. Scan for it.
|
|
||||||
for token in js_type.tokens:
|
|
||||||
if (token and isinstance(token, Token) and
|
|
||||||
token.type == Type.DOC_TYPE_MODIFIER and token.string == '|'):
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
break
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Attempts to fix the error.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
code = error.code
|
|
||||||
token = error.token
|
|
||||||
|
|
||||||
if self._fix_error_codes and code not in self._fix_error_codes:
|
|
||||||
return
|
|
||||||
|
|
||||||
if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL:
|
|
||||||
self._FixJsDocPipeNull(token.attached_object.jstype)
|
|
||||||
|
|
||||||
elif code == errors.JSDOC_MISSING_OPTIONAL_TYPE:
|
|
||||||
iterator = token.attached_object.type_end_token
|
|
||||||
if iterator.type == Type.DOC_END_BRACE or iterator.string.isspace():
|
|
||||||
iterator = iterator.previous
|
|
||||||
|
|
||||||
ending_space = len(iterator.string) - len(iterator.string.rstrip())
|
|
||||||
iterator.string = '%s=%s' % (iterator.string.rstrip(),
|
|
||||||
' ' * ending_space)
|
|
||||||
|
|
||||||
# Create a new flag object with updated type info.
|
|
||||||
token.attached_object = javascriptstatetracker.JsDocFlag(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.JSDOC_MISSING_VAR_ARGS_TYPE:
|
|
||||||
iterator = token.attached_object.type_start_token
|
|
||||||
if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace():
|
|
||||||
iterator = iterator.next
|
|
||||||
|
|
||||||
starting_space = len(iterator.string) - len(iterator.string.lstrip())
|
|
||||||
iterator.string = '%s...%s' % (' ' * starting_space,
|
|
||||||
iterator.string.lstrip())
|
|
||||||
|
|
||||||
# Create a new flag object with updated type info.
|
|
||||||
token.attached_object = javascriptstatetracker.JsDocFlag(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
errors.MISSING_SEMICOLON):
|
|
||||||
semicolon_token = Token(';', Type.SEMICOLON, token.line,
|
|
||||||
token.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(semicolon_token, token)
|
|
||||||
token.metadata.is_implied_semicolon = False
|
|
||||||
semicolon_token.metadata.is_implied_semicolon = False
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
errors.COMMA_AT_END_OF_LITERAL):
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.INVALID_JSDOC_TAG:
|
|
||||||
if token.string == '@returns':
|
|
||||||
token.string = '@return'
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.FILE_MISSING_NEWLINE:
|
|
||||||
# This error is fixed implicitly by the way we restore the file
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.MISSING_SPACE:
|
|
||||||
if error.fix_data:
|
|
||||||
token.string = error.fix_data
|
|
||||||
self._AddFix(token)
|
|
||||||
elif error.position:
|
|
||||||
if error.position.IsAtBeginning():
|
|
||||||
tokenutil.InsertSpaceTokenAfter(token.previous)
|
|
||||||
elif error.position.IsAtEnd(token.string):
|
|
||||||
tokenutil.InsertSpaceTokenAfter(token)
|
|
||||||
else:
|
|
||||||
token.string = error.position.Set(token.string, ' ')
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.EXTRA_SPACE:
|
|
||||||
if error.position:
|
|
||||||
token.string = error.position.Set(token.string, '')
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.MISSING_LINE:
|
|
||||||
if error.position.IsAtBeginning():
|
|
||||||
tokenutil.InsertBlankLineAfter(token.previous)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertBlankLineAfter(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.EXTRA_LINE:
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.WRONG_BLANK_LINE_COUNT:
|
|
||||||
if not token.previous:
|
|
||||||
# TODO(user): Add an insertBefore method to tokenutil.
|
|
||||||
return
|
|
||||||
|
|
||||||
num_lines = error.fix_data
|
|
||||||
should_delete = False
|
|
||||||
|
|
||||||
if num_lines < 0:
|
|
||||||
num_lines *= -1
|
|
||||||
should_delete = True
|
|
||||||
|
|
||||||
for unused_i in xrange(1, num_lines + 1):
|
|
||||||
if should_delete:
|
|
||||||
# TODO(user): DeleteToken should update line numbers.
|
|
||||||
self._DeleteToken(token.previous)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertBlankLineAfter(token.previous)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING:
|
|
||||||
end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END)
|
|
||||||
if end_quote:
|
|
||||||
single_quote_start = Token(
|
|
||||||
"'", Type.SINGLE_QUOTE_STRING_START, token.line, token.line_number)
|
|
||||||
single_quote_end = Token(
|
|
||||||
"'", Type.SINGLE_QUOTE_STRING_START, end_quote.line,
|
|
||||||
token.line_number)
|
|
||||||
|
|
||||||
tokenutil.InsertTokenAfter(single_quote_start, token)
|
|
||||||
tokenutil.InsertTokenAfter(single_quote_end, end_quote)
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._DeleteToken(end_quote)
|
|
||||||
self._AddFix([token, end_quote])
|
|
||||||
|
|
||||||
elif code == errors.MISSING_BRACES_AROUND_TYPE:
|
|
||||||
fixed_tokens = []
|
|
||||||
start_token = token.attached_object.type_start_token
|
|
||||||
|
|
||||||
if start_token.type != Type.DOC_START_BRACE:
|
|
||||||
leading_space = (
|
|
||||||
len(start_token.string) - len(start_token.string.lstrip()))
|
|
||||||
if leading_space:
|
|
||||||
start_token = tokenutil.SplitToken(start_token, leading_space)
|
|
||||||
# Fix case where start and end token were the same.
|
|
||||||
if token.attached_object.type_end_token == start_token.previous:
|
|
||||||
token.attached_object.type_end_token = start_token
|
|
||||||
|
|
||||||
new_token = Token('{', Type.DOC_START_BRACE, start_token.line,
|
|
||||||
start_token.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, start_token.previous)
|
|
||||||
token.attached_object.type_start_token = new_token
|
|
||||||
fixed_tokens.append(new_token)
|
|
||||||
|
|
||||||
end_token = token.attached_object.type_end_token
|
|
||||||
if end_token.type != Type.DOC_END_BRACE:
|
|
||||||
# If the start token was a brace, the end token will be a
|
|
||||||
# FLAG_ENDING_TYPE token, if there wasn't a starting brace then
|
|
||||||
# the end token is the last token of the actual type.
|
|
||||||
last_type = end_token
|
|
||||||
if not fixed_tokens:
|
|
||||||
last_type = end_token.previous
|
|
||||||
|
|
||||||
while last_type.string.isspace():
|
|
||||||
last_type = last_type.previous
|
|
||||||
|
|
||||||
# If there was no starting brace then a lone end brace wouldn't have
|
|
||||||
# been type end token. Now that we've added any missing start brace,
|
|
||||||
# see if the last effective type token was an end brace.
|
|
||||||
if last_type.type != Type.DOC_END_BRACE:
|
|
||||||
trailing_space = (len(last_type.string) -
|
|
||||||
len(last_type.string.rstrip()))
|
|
||||||
if trailing_space:
|
|
||||||
tokenutil.SplitToken(last_type,
|
|
||||||
len(last_type.string) - trailing_space)
|
|
||||||
|
|
||||||
new_token = Token('}', Type.DOC_END_BRACE, last_type.line,
|
|
||||||
last_type.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, last_type)
|
|
||||||
token.attached_object.type_end_token = new_token
|
|
||||||
fixed_tokens.append(new_token)
|
|
||||||
|
|
||||||
self._AddFix(fixed_tokens)
|
|
||||||
|
|
||||||
elif code == errors.LINE_STARTS_WITH_OPERATOR:
|
|
||||||
# Remove whitespace following the operator so the line starts clean.
|
|
||||||
self._StripSpace(token, before=False)
|
|
||||||
|
|
||||||
# Remove the operator.
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
insertion_point = tokenutil.GetPreviousCodeToken(token)
|
|
||||||
|
|
||||||
# Insert a space between the previous token and the new operator.
|
|
||||||
space = Token(' ', Type.WHITESPACE, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(space, insertion_point)
|
|
||||||
|
|
||||||
# Insert the operator on the end of the previous line.
|
|
||||||
new_token = Token(token.string, token.type, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, space)
|
|
||||||
self._AddFix(new_token)
|
|
||||||
|
|
||||||
elif code == errors.LINE_ENDS_WITH_DOT:
|
|
||||||
# Remove whitespace preceding the operator to remove trailing whitespace.
|
|
||||||
self._StripSpace(token, before=True)
|
|
||||||
|
|
||||||
# Remove the dot.
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
insertion_point = tokenutil.GetNextCodeToken(token)
|
|
||||||
|
|
||||||
# Insert the dot at the beginning of the next line of code.
|
|
||||||
new_token = Token(token.string, token.type, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenBefore(new_token, insertion_point)
|
|
||||||
self._AddFix(new_token)
|
|
||||||
|
|
||||||
elif code == errors.GOOG_REQUIRES_NOT_ALPHABETIZED:
|
|
||||||
require_start_token = error.fix_data
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixRequires(require_start_token)
|
|
||||||
|
|
||||||
self._AddFix(require_start_token)
|
|
||||||
|
|
||||||
elif code == errors.GOOG_PROVIDES_NOT_ALPHABETIZED:
|
|
||||||
provide_start_token = error.fix_data
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixProvides(provide_start_token)
|
|
||||||
|
|
||||||
self._AddFix(provide_start_token)
|
|
||||||
|
|
||||||
elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC:
|
|
||||||
if token.previous.string == '{' and token.next.string == '}':
|
|
||||||
self._DeleteToken(token.previous)
|
|
||||||
self._DeleteToken(token.next)
|
|
||||||
self._AddFix([token])
|
|
||||||
|
|
||||||
elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION:
|
|
||||||
match = INVERTED_AUTHOR_SPEC.match(token.string)
|
|
||||||
if match:
|
|
||||||
token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'),
|
|
||||||
match.group('email'),
|
|
||||||
match.group('whitespace_after_name'),
|
|
||||||
match.group('name'),
|
|
||||||
match.group('trailing_characters'))
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif (code == errors.WRONG_INDENTATION and
|
|
||||||
not FLAGS.disable_indentation_fixing):
|
|
||||||
token = tokenutil.GetFirstTokenInSameLine(token)
|
|
||||||
actual = error.position.start
|
|
||||||
expected = error.position.length
|
|
||||||
|
|
||||||
# Cases where first token is param but with leading spaces.
|
|
||||||
if (len(token.string.lstrip()) == len(token.string) - actual and
|
|
||||||
token.string.lstrip()):
|
|
||||||
token.string = token.string.lstrip()
|
|
||||||
actual = 0
|
|
||||||
|
|
||||||
if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0:
|
|
||||||
token.string = token.string.lstrip() + (' ' * expected)
|
|
||||||
self._AddFix([token])
|
|
||||||
else:
|
|
||||||
# We need to add indentation.
|
|
||||||
new_token = Token(' ' * expected, Type.WHITESPACE,
|
|
||||||
token.line, token.line_number)
|
|
||||||
# Note that we'll never need to add indentation at the first line,
|
|
||||||
# since it will always not be indented. Therefore it's safe to assume
|
|
||||||
# token.previous exists.
|
|
||||||
tokenutil.InsertTokenAfter(new_token, token.previous)
|
|
||||||
self._AddFix([token])
|
|
||||||
|
|
||||||
elif code in [errors.MALFORMED_END_OF_SCOPE_COMMENT,
|
|
||||||
errors.MISSING_END_OF_SCOPE_COMMENT]:
|
|
||||||
# Only fix cases where }); is found with no trailing content on the line
|
|
||||||
# other than a comment. Value of 'token' is set to } for this error.
|
|
||||||
if (token.type == Type.END_BLOCK and
|
|
||||||
token.next.type == Type.END_PAREN and
|
|
||||||
token.next.next.type == Type.SEMICOLON):
|
|
||||||
current_token = token.next.next.next
|
|
||||||
removed_tokens = []
|
|
||||||
while current_token and current_token.line_number == token.line_number:
|
|
||||||
if current_token.IsAnyType(Type.WHITESPACE,
|
|
||||||
Type.START_SINGLE_LINE_COMMENT,
|
|
||||||
Type.COMMENT):
|
|
||||||
removed_tokens.append(current_token)
|
|
||||||
current_token = current_token.next
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if removed_tokens:
|
|
||||||
self._DeleteTokens(removed_tokens[0], len(removed_tokens))
|
|
||||||
|
|
||||||
whitespace_token = Token(' ', Type.WHITESPACE, token.line,
|
|
||||||
token.line_number)
|
|
||||||
start_comment_token = Token('//', Type.START_SINGLE_LINE_COMMENT,
|
|
||||||
token.line, token.line_number)
|
|
||||||
comment_token = Token(' goog.scope', Type.COMMENT, token.line,
|
|
||||||
token.line_number)
|
|
||||||
insertion_tokens = [whitespace_token, start_comment_token,
|
|
||||||
comment_token]
|
|
||||||
|
|
||||||
tokenutil.InsertTokensAfter(insertion_tokens, token.next.next)
|
|
||||||
self._AddFix(removed_tokens + insertion_tokens)
|
|
||||||
|
|
||||||
elif code in [errors.EXTRA_GOOG_PROVIDE, errors.EXTRA_GOOG_REQUIRE]:
|
|
||||||
tokens_in_line = tokenutil.GetAllTokensInSameLine(token)
|
|
||||||
num_delete_tokens = len(tokens_in_line)
|
|
||||||
# If line being deleted is preceded and succeed with blank lines then
|
|
||||||
# delete one blank line also.
|
|
||||||
if (tokens_in_line[0].previous and tokens_in_line[-1].next
|
|
||||||
and tokens_in_line[0].previous.type == Type.BLANK_LINE
|
|
||||||
and tokens_in_line[-1].next.type == Type.BLANK_LINE):
|
|
||||||
num_delete_tokens += 1
|
|
||||||
self._DeleteTokens(tokens_in_line[0], num_delete_tokens)
|
|
||||||
self._AddFix(tokens_in_line)
|
|
||||||
|
|
||||||
elif code in [errors.MISSING_GOOG_PROVIDE, errors.MISSING_GOOG_REQUIRE]:
|
|
||||||
missing_namespaces = error.fix_data[0]
|
|
||||||
need_blank_line = error.fix_data[1] or (not token.previous)
|
|
||||||
|
|
||||||
insert_location = Token('', Type.NORMAL, '', token.line_number - 1)
|
|
||||||
dummy_first_token = insert_location
|
|
||||||
tokenutil.InsertTokenBefore(insert_location, token)
|
|
||||||
|
|
||||||
# If inserting a blank line check blank line does not exist before
|
|
||||||
# token to avoid extra blank lines.
|
|
||||||
if (need_blank_line and insert_location.previous
|
|
||||||
and insert_location.previous.type != Type.BLANK_LINE):
|
|
||||||
tokenutil.InsertBlankLineAfter(insert_location)
|
|
||||||
insert_location = insert_location.next
|
|
||||||
|
|
||||||
for missing_namespace in missing_namespaces:
|
|
||||||
new_tokens = self._GetNewRequireOrProvideTokens(
|
|
||||||
code == errors.MISSING_GOOG_PROVIDE,
|
|
||||||
missing_namespace, insert_location.line_number + 1)
|
|
||||||
tokenutil.InsertLineAfter(insert_location, new_tokens)
|
|
||||||
insert_location = new_tokens[-1]
|
|
||||||
self._AddFix(new_tokens)
|
|
||||||
|
|
||||||
# If inserting a blank line check blank line does not exist after
|
|
||||||
# token to avoid extra blank lines.
|
|
||||||
if (need_blank_line and insert_location.next
|
|
||||||
and insert_location.next.type != Type.BLANK_LINE):
|
|
||||||
tokenutil.InsertBlankLineAfter(insert_location)
|
|
||||||
|
|
||||||
tokenutil.DeleteToken(dummy_first_token)
|
|
||||||
|
|
||||||
def _StripSpace(self, token, before):
|
|
||||||
"""Strip whitespace tokens either preceding or following the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token.
|
|
||||||
before: If true, strip space before the token, if false, after it.
|
|
||||||
"""
|
|
||||||
token = token.previous if before else token.next
|
|
||||||
while token and token.type == Type.WHITESPACE:
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
token = token.previous if before else token.next
|
|
||||||
|
|
||||||
def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number):
|
|
||||||
"""Returns a list of tokens to create a goog.require/provide statement.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
is_provide: True if getting tokens for a provide, False for require.
|
|
||||||
namespace: The required or provided namespaces to get tokens for.
|
|
||||||
line_number: The line number the new require or provide statement will be
|
|
||||||
on.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tokens to create a new goog.require or goog.provide statement.
|
|
||||||
"""
|
|
||||||
string = 'goog.require'
|
|
||||||
if is_provide:
|
|
||||||
string = 'goog.provide'
|
|
||||||
line_text = string + '(\'' + namespace + '\');\n'
|
|
||||||
return [
|
|
||||||
Token(string, Type.IDENTIFIER, line_text, line_number),
|
|
||||||
Token('(', Type.START_PAREN, line_text, line_number),
|
|
||||||
Token('\'', Type.SINGLE_QUOTE_STRING_START, line_text, line_number),
|
|
||||||
Token(namespace, Type.STRING_TEXT, line_text, line_number),
|
|
||||||
Token('\'', Type.SINGLE_QUOTE_STRING_END, line_text, line_number),
|
|
||||||
Token(')', Type.END_PAREN, line_text, line_number),
|
|
||||||
Token(';', Type.SEMICOLON, line_text, line_number)
|
|
||||||
]
|
|
||||||
|
|
||||||
def _DeleteToken(self, token):
|
|
||||||
"""Deletes the specified token from the linked list of tokens.
|
|
||||||
|
|
||||||
Updates instance variables pointing to tokens such as _file_token if
|
|
||||||
they reference the deleted token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to delete.
|
|
||||||
"""
|
|
||||||
if token == self._file_token:
|
|
||||||
self._file_token = token.next
|
|
||||||
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
|
|
||||||
def _DeleteTokens(self, token, token_count):
|
|
||||||
"""Deletes the given number of tokens starting with the given token.
|
|
||||||
|
|
||||||
Updates instance variables pointing to tokens such as _file_token if
|
|
||||||
they reference the deleted token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token to delete.
|
|
||||||
token_count: The total number of tokens to delete.
|
|
||||||
"""
|
|
||||||
if token == self._file_token:
|
|
||||||
for unused_i in xrange(token_count):
|
|
||||||
self._file_token = self._file_token.next
|
|
||||||
|
|
||||||
tokenutil.DeleteTokens(token, token_count)
|
|
||||||
|
|
||||||
def FinishFile(self):
|
|
||||||
"""Called when the current file has finished style checking.
|
|
||||||
|
|
||||||
Used to go back and fix any errors in the file. It currently supports both
|
|
||||||
js and html files. For js files it does a simple dump of all tokens, but in
|
|
||||||
order to support html file, we need to merge the original file with the new
|
|
||||||
token set back together. This works because the tokenized html file is the
|
|
||||||
original html file with all non js lines kept but blanked out with one blank
|
|
||||||
line token per line of html.
|
|
||||||
"""
|
|
||||||
if self._file_fix_count:
|
|
||||||
# Get the original file content for html.
|
|
||||||
if self._file_is_html:
|
|
||||||
f = open(self._file_name, 'r')
|
|
||||||
original_lines = f.readlines()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
f = self._external_file
|
|
||||||
if not f:
|
|
||||||
error_noun = 'error' if self._file_fix_count == 1 else 'errors'
|
|
||||||
print 'Fixed %d %s in %s' % (
|
|
||||||
self._file_fix_count, error_noun, self._file_name)
|
|
||||||
f = open(self._file_name, 'w')
|
|
||||||
|
|
||||||
token = self._file_token
|
|
||||||
# Finding the first not deleted token.
|
|
||||||
while token.is_deleted:
|
|
||||||
token = token.next
|
|
||||||
# If something got inserted before first token (e.g. due to sorting)
|
|
||||||
# then move to start. Bug 8398202.
|
|
||||||
while token.previous:
|
|
||||||
token = token.previous
|
|
||||||
char_count = 0
|
|
||||||
line = ''
|
|
||||||
while token:
|
|
||||||
line += token.string
|
|
||||||
char_count += len(token.string)
|
|
||||||
|
|
||||||
if token.IsLastInLine():
|
|
||||||
# We distinguish if a blank line in html was from stripped original
|
|
||||||
# file or newly added error fix by looking at the "org_line_number"
|
|
||||||
# field on the token. It is only set in the tokenizer, so for all
|
|
||||||
# error fixes, the value should be None.
|
|
||||||
if (line or not self._file_is_html or
|
|
||||||
token.orig_line_number is None):
|
|
||||||
f.write(line)
|
|
||||||
f.write('\n')
|
|
||||||
else:
|
|
||||||
f.write(original_lines[token.orig_line_number - 1])
|
|
||||||
line = ''
|
|
||||||
if char_count > 80 and token.line_number in self._file_changed_lines:
|
|
||||||
print 'WARNING: Line %d of %s is now longer than 80 characters.' % (
|
|
||||||
token.line_number, self._file_name)
|
|
||||||
|
|
||||||
char_count = 0
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
if not self._external_file:
|
|
||||||
# Close the file if we created it
|
|
||||||
f.close()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the error_fixer module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import testutil
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorFixerTest(googletest.TestCase):
|
|
||||||
"""Unit tests for error_fixer."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.error_fixer = error_fixer.ErrorFixer()
|
|
||||||
|
|
||||||
def testDeleteToken(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)
|
|
||||||
second_token = start_token.next
|
|
||||||
self.error_fixer.HandleFile('test_file', start_token)
|
|
||||||
|
|
||||||
self.error_fixer._DeleteToken(start_token)
|
|
||||||
|
|
||||||
self.assertEqual(second_token, self.error_fixer._file_token)
|
|
||||||
|
|
||||||
def testDeleteTokens(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)
|
|
||||||
fourth_token = start_token.next.next.next
|
|
||||||
self.error_fixer.HandleFile('test_file', start_token)
|
|
||||||
|
|
||||||
self.error_fixer._DeleteTokens(start_token, 3)
|
|
||||||
|
|
||||||
self.assertEqual(fourth_token, self.error_fixer._file_token)
|
|
||||||
|
|
||||||
_TEST_SCRIPT = """\
|
|
||||||
var x = 3;
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
|
|
||||||
"""A simple, pickle-serializable class to represent a lint error."""
|
|
||||||
|
|
||||||
__author__ = 'nnaze@google.com (Nathan Naze)'
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter.common import erroroutput
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorRecord(object):
|
|
||||||
"""Record-keeping struct that can be serialized back from a process.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
path: Path to the file.
|
|
||||||
error_string: Error string for the user.
|
|
||||||
new_error: Whether this is a "new error" (see errors.NEW_ERRORS).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path, error_string, new_error):
|
|
||||||
self.path = path
|
|
||||||
self.error_string = error_string
|
|
||||||
self.new_error = new_error
|
|
||||||
|
|
||||||
|
|
||||||
def MakeErrorRecord(path, error):
|
|
||||||
"""Make an error record with correctly formatted error string.
|
|
||||||
|
|
||||||
Errors are not able to be serialized (pickled) over processes because of
|
|
||||||
their pointers to the complex token/context graph. We use an intermediary
|
|
||||||
serializable class to pass back just the relevant information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Path of file the error was found in.
|
|
||||||
error: An error.Error instance.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_ErrorRecord instance.
|
|
||||||
"""
|
|
||||||
new_error = error.code in errors.NEW_ERRORS
|
|
||||||
|
|
||||||
if FLAGS.unix_mode:
|
|
||||||
error_string = erroroutput.GetUnixErrorOutput(
|
|
||||||
path, error, new_error=new_error)
|
|
||||||
else:
|
|
||||||
error_string = erroroutput.GetErrorOutput(error, new_error=new_error)
|
|
||||||
|
|
||||||
return ErrorRecord(path, error_string, new_error)
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Linter error rules class for Closure Linter."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
from closure_linter import errors
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('jsdoc', True,
|
|
||||||
'Whether to report errors for missing JsDoc.')
|
|
||||||
flags.DEFINE_list('disable', None,
|
|
||||||
'Disable specific error. Usage Ex.: gjslint --disable 1,'
|
|
||||||
'0011 foo.js.')
|
|
||||||
flags.DEFINE_integer('max_line_length', 80, 'Maximum line length allowed '
|
|
||||||
'without warning.', lower_bound=1)
|
|
||||||
|
|
||||||
disabled_error_nums = None
|
|
||||||
|
|
||||||
|
|
||||||
def GetMaxLineLength():
|
|
||||||
"""Returns allowed maximum length of line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Length of line allowed without any warning.
|
|
||||||
"""
|
|
||||||
return FLAGS.max_line_length
|
|
||||||
|
|
||||||
|
|
||||||
def ShouldReportError(error):
|
|
||||||
"""Whether the given error should be reported.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True for all errors except missing documentation errors and disabled
|
|
||||||
errors. For missing documentation, it returns the value of the
|
|
||||||
jsdoc flag.
|
|
||||||
"""
|
|
||||||
global disabled_error_nums
|
|
||||||
if disabled_error_nums is None:
|
|
||||||
disabled_error_nums = []
|
|
||||||
if FLAGS.disable:
|
|
||||||
for error_str in FLAGS.disable:
|
|
||||||
error_num = 0
|
|
||||||
try:
|
|
||||||
error_num = int(error_str)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
disabled_error_nums.append(error_num)
|
|
||||||
|
|
||||||
return ((FLAGS.jsdoc or error not in (
|
|
||||||
errors.MISSING_PARAMETER_DOCUMENTATION,
|
|
||||||
errors.MISSING_RETURN_DOCUMENTATION,
|
|
||||||
errors.MISSING_MEMBER_DOCUMENTATION,
|
|
||||||
errors.MISSING_PRIVATE,
|
|
||||||
errors.MISSING_JSDOC_TAG_THIS)) and
|
|
||||||
(not FLAGS.disable or error not in disabled_error_nums))
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2013 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.
|
|
||||||
|
|
||||||
"""Medium tests for the gjslint errorrules.
|
|
||||||
|
|
||||||
Currently its just verifying that warnings can't be disabled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorRulesTest(googletest.TestCase):
|
|
||||||
"""Test case to for gjslint errorrules."""
|
|
||||||
|
|
||||||
def testNoMaxLineLengthFlagExists(self):
|
|
||||||
"""Tests that --max_line_length flag does not exists."""
|
|
||||||
self.assertTrue('max_line_length' not in flags.FLAGS.FlagDict())
|
|
||||||
|
|
||||||
def testGetMaxLineLength(self):
|
|
||||||
"""Tests warning are reported for line greater than 80.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# One line > 100 and one line > 80 and < 100. So should produce two
|
|
||||||
# line too long error.
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13'
|
|
||||||
' + 14 + 15 + 16 + 17 + 18 + 19 + 20;',
|
|
||||||
' dummy.aa.j = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13'
|
|
||||||
' + 14 + 15 + 16 + 17 + 18;',
|
|
||||||
'}',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
# Expect line too long.
|
|
||||||
expected = [errors.LINE_TOO_LONG, errors.LINE_TOO_LONG]
|
|
||||||
|
|
||||||
self._AssertErrors(original, expected)
|
|
||||||
|
|
||||||
def testNoDisableFlagExists(self):
|
|
||||||
"""Tests that --disable flag does not exists."""
|
|
||||||
self.assertTrue('disable' not in flags.FLAGS.FlagDict())
|
|
||||||
|
|
||||||
def testWarningsNotDisabled(self):
|
|
||||||
"""Tests warnings are reported when nothing is disabled.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [errors.GOOG_REQUIRES_NOT_ALPHABETIZED,
|
|
||||||
errors.FILE_MISSING_NEWLINE]
|
|
||||||
|
|
||||||
self._AssertErrors(original, expected)
|
|
||||||
|
|
||||||
def _AssertErrors(self, original, expected_errors, include_header=True):
|
|
||||||
"""Asserts that the error fixer corrects original to expected."""
|
|
||||||
if include_header:
|
|
||||||
original = self._GetHeader() + original
|
|
||||||
|
|
||||||
# Trap gjslint's output parse it to get messages added.
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
runner.Run('testing.js', error_accumulator, source=original)
|
|
||||||
error_nums = [e.code for e in error_accumulator.GetErrors()]
|
|
||||||
|
|
||||||
error_nums.sort()
|
|
||||||
expected_errors.sort()
|
|
||||||
self.assertListEqual(error_nums, expected_errors)
|
|
||||||
|
|
||||||
def _GetHeader(self):
|
|
||||||
"""Returns a fake header for a JavaScript file."""
|
|
||||||
return [
|
|
||||||
'// Copyright 2011 Google Inc. All Rights Reserved.',
|
|
||||||
'',
|
|
||||||
'/**',
|
|
||||||
' * @fileoverview Fake file overview.',
|
|
||||||
' * @author fake@google.com (Fake Person)',
|
|
||||||
' */',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Error codes for JavaScript style checker."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
def ByName(name):
|
|
||||||
"""Get the error code for the given error name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The name of the error
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The error code
|
|
||||||
"""
|
|
||||||
return globals()[name]
|
|
||||||
|
|
||||||
|
|
||||||
# "File-fatal" errors - these errors stop further parsing of a single file
|
|
||||||
FILE_NOT_FOUND = -1
|
|
||||||
FILE_DOES_NOT_PARSE = -2
|
|
||||||
|
|
||||||
# Spacing
|
|
||||||
EXTRA_SPACE = 1
|
|
||||||
MISSING_SPACE = 2
|
|
||||||
EXTRA_LINE = 3
|
|
||||||
MISSING_LINE = 4
|
|
||||||
ILLEGAL_TAB = 5
|
|
||||||
WRONG_INDENTATION = 6
|
|
||||||
WRONG_BLANK_LINE_COUNT = 7
|
|
||||||
|
|
||||||
# Semicolons
|
|
||||||
MISSING_SEMICOLON = 10
|
|
||||||
MISSING_SEMICOLON_AFTER_FUNCTION = 11
|
|
||||||
ILLEGAL_SEMICOLON_AFTER_FUNCTION = 12
|
|
||||||
REDUNDANT_SEMICOLON = 13
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
ILLEGAL_PROTOTYPE_MEMBER_VALUE = 100
|
|
||||||
LINE_TOO_LONG = 110
|
|
||||||
LINE_STARTS_WITH_OPERATOR = 120
|
|
||||||
COMMA_AT_END_OF_LITERAL = 121
|
|
||||||
LINE_ENDS_WITH_DOT = 122
|
|
||||||
MULTI_LINE_STRING = 130
|
|
||||||
UNNECESSARY_DOUBLE_QUOTED_STRING = 131
|
|
||||||
UNUSED_PRIVATE_MEMBER = 132
|
|
||||||
UNUSED_LOCAL_VARIABLE = 133
|
|
||||||
|
|
||||||
# Requires, provides
|
|
||||||
GOOG_REQUIRES_NOT_ALPHABETIZED = 140
|
|
||||||
GOOG_PROVIDES_NOT_ALPHABETIZED = 141
|
|
||||||
MISSING_GOOG_REQUIRE = 142
|
|
||||||
MISSING_GOOG_PROVIDE = 143
|
|
||||||
EXTRA_GOOG_REQUIRE = 144
|
|
||||||
EXTRA_GOOG_PROVIDE = 145
|
|
||||||
ALIAS_STMT_NEEDS_GOOG_REQUIRE = 146
|
|
||||||
|
|
||||||
# JsDoc
|
|
||||||
INVALID_JSDOC_TAG = 200
|
|
||||||
INVALID_USE_OF_DESC_TAG = 201
|
|
||||||
NO_BUG_NUMBER_AFTER_BUG_TAG = 202
|
|
||||||
MISSING_PARAMETER_DOCUMENTATION = 210
|
|
||||||
EXTRA_PARAMETER_DOCUMENTATION = 211
|
|
||||||
WRONG_PARAMETER_DOCUMENTATION = 212
|
|
||||||
MISSING_JSDOC_TAG_TYPE = 213
|
|
||||||
MISSING_JSDOC_TAG_DESCRIPTION = 214
|
|
||||||
MISSING_JSDOC_PARAM_NAME = 215
|
|
||||||
OUT_OF_ORDER_JSDOC_TAG_TYPE = 216
|
|
||||||
MISSING_RETURN_DOCUMENTATION = 217
|
|
||||||
UNNECESSARY_RETURN_DOCUMENTATION = 218
|
|
||||||
MISSING_BRACES_AROUND_TYPE = 219
|
|
||||||
MISSING_MEMBER_DOCUMENTATION = 220
|
|
||||||
MISSING_PRIVATE = 221
|
|
||||||
EXTRA_PRIVATE = 222
|
|
||||||
INVALID_OVERRIDE_PRIVATE = 223
|
|
||||||
INVALID_INHERIT_DOC_PRIVATE = 224
|
|
||||||
MISSING_JSDOC_TAG_THIS = 225
|
|
||||||
UNNECESSARY_BRACES_AROUND_INHERIT_DOC = 226
|
|
||||||
INVALID_AUTHOR_TAG_DESCRIPTION = 227
|
|
||||||
JSDOC_PREFER_QUESTION_TO_PIPE_NULL = 230
|
|
||||||
JSDOC_ILLEGAL_QUESTION_WITH_PIPE = 231
|
|
||||||
JSDOC_MISSING_OPTIONAL_TYPE = 232
|
|
||||||
JSDOC_MISSING_OPTIONAL_PREFIX = 233
|
|
||||||
JSDOC_MISSING_VAR_ARGS_TYPE = 234
|
|
||||||
JSDOC_MISSING_VAR_ARGS_NAME = 235
|
|
||||||
JSDOC_DOES_NOT_PARSE = 236
|
|
||||||
# TODO(robbyw): Split this in to more specific syntax problems.
|
|
||||||
INCORRECT_SUPPRESS_SYNTAX = 250
|
|
||||||
INVALID_SUPPRESS_TYPE = 251
|
|
||||||
UNNECESSARY_SUPPRESS = 252
|
|
||||||
|
|
||||||
# File ending
|
|
||||||
FILE_MISSING_NEWLINE = 300
|
|
||||||
FILE_IN_BLOCK = 301
|
|
||||||
|
|
||||||
# Interfaces
|
|
||||||
INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS = 400
|
|
||||||
INTERFACE_METHOD_CANNOT_HAVE_CODE = 401
|
|
||||||
|
|
||||||
# Comments
|
|
||||||
MISSING_END_OF_SCOPE_COMMENT = 500
|
|
||||||
MALFORMED_END_OF_SCOPE_COMMENT = 501
|
|
||||||
|
|
||||||
# goog.scope - Namespace aliasing
|
|
||||||
# TODO(nnaze) Add additional errors here and in aliaspass.py
|
|
||||||
INVALID_USE_OF_GOOG_SCOPE = 600
|
|
||||||
EXTRA_GOOG_SCOPE_USAGE = 601
|
|
||||||
|
|
||||||
# ActionScript specific errors:
|
|
||||||
# TODO(user): move these errors to their own file and move all JavaScript
|
|
||||||
# specific errors to their own file as well.
|
|
||||||
# All ActionScript specific errors should have error number at least 1000.
|
|
||||||
FUNCTION_MISSING_RETURN_TYPE = 1132
|
|
||||||
PARAMETER_MISSING_TYPE = 1133
|
|
||||||
VAR_MISSING_TYPE = 1134
|
|
||||||
PARAMETER_MISSING_DEFAULT_VALUE = 1135
|
|
||||||
IMPORTS_NOT_ALPHABETIZED = 1140
|
|
||||||
IMPORT_CONTAINS_WILDCARD = 1141
|
|
||||||
UNUSED_IMPORT = 1142
|
|
||||||
INVALID_TRACE_SEVERITY_LEVEL = 1250
|
|
||||||
MISSING_TRACE_SEVERITY_LEVEL = 1251
|
|
||||||
MISSING_TRACE_MESSAGE = 1252
|
|
||||||
REMOVE_TRACE_BEFORE_SUBMIT = 1253
|
|
||||||
REMOVE_COMMENT_BEFORE_SUBMIT = 1254
|
|
||||||
# End of list of ActionScript specific errors.
|
|
||||||
|
|
||||||
NEW_ERRORS = frozenset([
|
|
||||||
# Errors added after 2.0.2:
|
|
||||||
WRONG_INDENTATION,
|
|
||||||
MISSING_SEMICOLON,
|
|
||||||
# Errors added after 2.3.9:
|
|
||||||
JSDOC_MISSING_VAR_ARGS_TYPE,
|
|
||||||
JSDOC_MISSING_VAR_ARGS_NAME,
|
|
||||||
# Errors added after 2.3.15:
|
|
||||||
ALIAS_STMT_NEEDS_GOOG_REQUIRE,
|
|
||||||
JSDOC_DOES_NOT_PARSE,
|
|
||||||
LINE_ENDS_WITH_DOT,
|
|
||||||
# Errors added after 2.3.17:
|
|
||||||
])
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Automatically fix simple style guide violations."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import simplefileflags as fileflags
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_list('additional_extensions', None, 'List of additional file '
|
|
||||||
'extensions (not js) that should be treated as '
|
|
||||||
'JavaScript files.')
|
|
||||||
flags.DEFINE_boolean('dry_run', False, 'Do not modify the file, only print it.')
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
"""Main function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
"""
|
|
||||||
if argv is None:
|
|
||||||
argv = flags.FLAGS(sys.argv)
|
|
||||||
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
|
|
||||||
files = fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
output_buffer = None
|
|
||||||
if FLAGS.dry_run:
|
|
||||||
output_buffer = StringIO.StringIO()
|
|
||||||
|
|
||||||
fixer = error_fixer.ErrorFixer(output_buffer)
|
|
||||||
|
|
||||||
# Check the list of files.
|
|
||||||
for filename in files:
|
|
||||||
runner.Run(filename, fixer)
|
|
||||||
if FLAGS.dry_run:
|
|
||||||
print output_buffer.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,615 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Medium tests for the gpylint auto-fixer."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robby Walker)'
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import runner
|
|
||||||
|
|
||||||
|
|
||||||
_RESOURCE_PREFIX = 'closure_linter/testdata'
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
|
|
||||||
|
|
||||||
class FixJsStyleTest(googletest.TestCase):
|
|
||||||
"""Test case to for gjslint auto-fixing."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = True
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = False
|
|
||||||
|
|
||||||
def testFixJsStyle(self):
|
|
||||||
test_cases = [
|
|
||||||
['fixjsstyle.in.js', 'fixjsstyle.out.js'],
|
|
||||||
['indentation.js', 'fixjsstyle.indentation.out.js'],
|
|
||||||
['fixjsstyle.html.in.html', 'fixjsstyle.html.out.html'],
|
|
||||||
['fixjsstyle.oplineend.in.js', 'fixjsstyle.oplineend.out.js']]
|
|
||||||
for [running_input_file, running_output_file] in test_cases:
|
|
||||||
print 'Checking %s vs %s' % (running_input_file, running_output_file)
|
|
||||||
input_filename = None
|
|
||||||
golden_filename = None
|
|
||||||
current_filename = None
|
|
||||||
try:
|
|
||||||
input_filename = '%s/%s' % (_RESOURCE_PREFIX, running_input_file)
|
|
||||||
current_filename = input_filename
|
|
||||||
|
|
||||||
golden_filename = '%s/%s' % (_RESOURCE_PREFIX, running_output_file)
|
|
||||||
current_filename = golden_filename
|
|
||||||
except IOError as ex:
|
|
||||||
raise IOError('Could not find testdata resource for %s: %s' %
|
|
||||||
(current_filename, ex))
|
|
||||||
|
|
||||||
if running_input_file == 'fixjsstyle.in.js':
|
|
||||||
with open(input_filename) as f:
|
|
||||||
for line in f:
|
|
||||||
# Go to last line.
|
|
||||||
pass
|
|
||||||
self.assertTrue(line == line.rstrip(), '%s file should not end '
|
|
||||||
'with a new line.' % (input_filename))
|
|
||||||
|
|
||||||
# Autofix the file, sending output to a fake file.
|
|
||||||
actual = StringIO.StringIO()
|
|
||||||
runner.Run(input_filename, error_fixer.ErrorFixer(actual))
|
|
||||||
|
|
||||||
# Now compare the files.
|
|
||||||
actual.seek(0)
|
|
||||||
expected = open(golden_filename, 'r')
|
|
||||||
|
|
||||||
# Uncomment to generate new golden files and run
|
|
||||||
# open('/'.join(golden_filename.split('/')[4:]), 'w').write(actual.read())
|
|
||||||
# actual.seek(0)
|
|
||||||
|
|
||||||
self.assertEqual(actual.readlines(), expected.readlines())
|
|
||||||
|
|
||||||
def testAddProvideFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.provide is added."""
|
|
||||||
original = [
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testAddRequireFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.require is added."""
|
|
||||||
original = [
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteProvideAndAddProvideFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.provide is deleted and added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteProvideAndAddRequireFirstLine(self):
|
|
||||||
"""Tests handling where goog.provide is deleted and goog.require added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteRequireAndAddRequireFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.require is deleted and added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteRequireAndAddProvideFirstLine(self):
|
|
||||||
"""Tests handling where goog.require is deleted and goog.provide added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMultipleProvideInsert(self):
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'goog.provide(\'dummy.dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa.ff = 1;',
|
|
||||||
'dummy.bb.ff = 1;',
|
|
||||||
'dummy.cc.ff = 1;',
|
|
||||||
'dummy.dd.ff = 1;',
|
|
||||||
'dummy.ee.ff = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'goog.provide(\'dummy.cc\');',
|
|
||||||
'goog.provide(\'dummy.dd\');',
|
|
||||||
'goog.provide(\'dummy.ee\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa.ff = 1;',
|
|
||||||
'dummy.bb.ff = 1;',
|
|
||||||
'dummy.cc.ff = 1;',
|
|
||||||
'dummy.dd.ff = 1;',
|
|
||||||
'dummy.ee.ff = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMultipleRequireInsert(self):
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.dd\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.aa.ff;',
|
|
||||||
'b = dummy.bb.ff;',
|
|
||||||
'c = dummy.cc.ff;',
|
|
||||||
'd = dummy.dd.ff;',
|
|
||||||
'e = dummy.ee.ff;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'goog.require(\'dummy.dd\');',
|
|
||||||
'goog.require(\'dummy.ee\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.aa.ff;',
|
|
||||||
'b = dummy.bb.ff;',
|
|
||||||
'c = dummy.cc.ff;',
|
|
||||||
'd = dummy.dd.ff;',
|
|
||||||
'e = dummy.ee.ff;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testUnsortedRequires(self):
|
|
||||||
"""Tests handling of unsorted goog.require statements without header.
|
|
||||||
|
|
||||||
Bug 8398202.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMissingExtraAndUnsortedRequires(self):
|
|
||||||
"""Tests handling of missing extra and unsorted goog.require statements."""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
'dummy.Cc.someMethod();',
|
|
||||||
'dummy.aa.someMethod();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
'dummy.Cc.someMethod();',
|
|
||||||
'dummy.aa.someMethod();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testExtraRequireOnFirstLine(self):
|
|
||||||
"""Tests handling of extra goog.require statement on the first line.
|
|
||||||
|
|
||||||
There was a bug when fixjsstyle quits with an exception. It happened if
|
|
||||||
- the first line of the file is an extra goog.require() statement,
|
|
||||||
- goog.require() statements are not sorted.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.bb();',
|
|
||||||
'var y = new dummy.cc();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.bb();',
|
|
||||||
'var y = new dummy.cc();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testUnsortedProvides(self):
|
|
||||||
"""Tests handling of unsorted goog.provide statements without header.
|
|
||||||
|
|
||||||
Bug 8398202.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa = function() {};'
|
|
||||||
'dummy.Cc = function() {};'
|
|
||||||
'dummy.Dd = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa = function() {};'
|
|
||||||
'dummy.Cc = function() {};'
|
|
||||||
'dummy.Dd = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMissingExtraAndUnsortedProvides(self):
|
|
||||||
"""Tests handling of missing extra and unsorted goog.provide statements."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.Cc = function() {};',
|
|
||||||
'dummy.Bb = function() {};',
|
|
||||||
'dummy.aa.someMethod = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Bb\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.Cc = function() {};',
|
|
||||||
'dummy.Bb = function() {};',
|
|
||||||
'dummy.aa.someMethod = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testNoRequires(self):
|
|
||||||
"""Tests positioning of missing requires without existing requires."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testNoProvides(self):
|
|
||||||
"""Tests positioning of missing provides without existing provides."""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testOutputOkayWhenFirstTokenIsDeleted(self):
|
|
||||||
"""Tests that autofix output is is correct when first token is deleted.
|
|
||||||
|
|
||||||
Regression test for bug 4581567
|
|
||||||
"""
|
|
||||||
original = ['"use strict";']
|
|
||||||
expected = ["'use strict';"]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testGoogScopeIndentation(self):
|
|
||||||
"""Tests Handling a typical end-of-scope indentation fix."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
' // TODO(brain): Take over the world.',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'// TODO(brain): Take over the world.',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMissingEndOfScopeComment(self):
|
|
||||||
"""Tests Handling a missing comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMissingEndOfScopeCommentWithOtherComment(self):
|
|
||||||
"""Tests handling an irrelevant comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
"}); // I don't belong here!",
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMalformedEndOfScopeComment(self):
|
|
||||||
"""Tests Handling a malformed comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope FTW',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testEndsWithIdentifier(self):
|
|
||||||
"""Tests Handling case where script ends with identifier. Bug 7643404."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testFileStartsWithSemicolon(self):
|
|
||||||
"""Tests handling files starting with semicolon.
|
|
||||||
|
|
||||||
b/10062516
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
';goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testCodeStartsWithSemicolon(self):
|
|
||||||
"""Tests handling code in starting with semicolon after comments.
|
|
||||||
|
|
||||||
b/10062516
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
';goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def _AssertFixes(self, original, expected, include_header=True):
|
|
||||||
"""Asserts that the error fixer corrects original to expected."""
|
|
||||||
if include_header:
|
|
||||||
original = self._GetHeader() + original
|
|
||||||
expected = self._GetHeader() + expected
|
|
||||||
|
|
||||||
actual = StringIO.StringIO()
|
|
||||||
runner.Run('testing.js', error_fixer.ErrorFixer(actual), original)
|
|
||||||
actual.seek(0)
|
|
||||||
|
|
||||||
expected = [x + '\n' for x in expected]
|
|
||||||
|
|
||||||
self.assertListEqual(actual.readlines(), expected)
|
|
||||||
|
|
||||||
def _GetHeader(self):
|
|
||||||
"""Returns a fake header for a JavaScript file."""
|
|
||||||
return [
|
|
||||||
'// Copyright 2011 Google Inc. All Rights Reserved.',
|
|
||||||
'',
|
|
||||||
'/**',
|
|
||||||
' * @fileoverview Fake file overview.',
|
|
||||||
' * @author fake@google.com (Fake Person)',
|
|
||||||
' */',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Full regression-type (Medium) tests for gjslint.
|
|
||||||
|
|
||||||
Tests every error that can be thrown by gjslint. Based heavily on
|
|
||||||
devtools/javascript/gpylint/full_test.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import filetestcase
|
|
||||||
|
|
||||||
_RESOURCE_PREFIX = 'closure_linter/testdata'
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.custom_jsdoc_tags = ('customtag', 'requires')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
flags.FLAGS.limited_doc_files = ('externs.js', 'dummy.js',
|
|
||||||
'limited_doc_checks.js')
|
|
||||||
flags.FLAGS.jslint_error = error_check.Rule.ALL
|
|
||||||
|
|
||||||
# List of files under testdata to test.
|
|
||||||
# We need to list files explicitly since pyglib can't list directories.
|
|
||||||
# TODO(user): Figure out how to list the directory.
|
|
||||||
_TEST_FILES = [
|
|
||||||
'all_js_wrapped.js',
|
|
||||||
'blank_lines.js',
|
|
||||||
'ends_with_block.js',
|
|
||||||
'empty_file.js',
|
|
||||||
'externs.js',
|
|
||||||
'externs_jsdoc.js',
|
|
||||||
'goog_scope.js',
|
|
||||||
'html_parse_error.html',
|
|
||||||
'indentation.js',
|
|
||||||
'interface.js',
|
|
||||||
'jsdoc.js',
|
|
||||||
'limited_doc_checks.js',
|
|
||||||
'minimal.js',
|
|
||||||
'other.js',
|
|
||||||
'provide_blank.js',
|
|
||||||
'provide_extra.js',
|
|
||||||
'provide_missing.js',
|
|
||||||
'require_alias.js',
|
|
||||||
'require_all_caps.js',
|
|
||||||
'require_blank.js',
|
|
||||||
'require_extra.js',
|
|
||||||
'require_function.js',
|
|
||||||
'require_function_missing.js',
|
|
||||||
'require_function_through_both.js',
|
|
||||||
'require_function_through_namespace.js',
|
|
||||||
'require_interface.js',
|
|
||||||
'require_interface_alias.js',
|
|
||||||
'require_interface_base.js',
|
|
||||||
'require_lower_case.js',
|
|
||||||
'require_missing.js',
|
|
||||||
'require_numeric.js',
|
|
||||||
'require_provide_blank.js',
|
|
||||||
'require_provide_missing.js',
|
|
||||||
'require_provide_ok.js',
|
|
||||||
'semicolon_missing.js',
|
|
||||||
'simple.html',
|
|
||||||
'spaces.js',
|
|
||||||
'tokenizer.js',
|
|
||||||
'unparseable.js',
|
|
||||||
'unused_local_variables.js',
|
|
||||||
'unused_private_members.js',
|
|
||||||
'utf8.html',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class GJsLintTestSuite(unittest.TestSuite):
|
|
||||||
"""Test suite to run a GJsLintTest for each of several files.
|
|
||||||
|
|
||||||
If sys.argv[1:] is non-empty, it is interpreted as a list of filenames in
|
|
||||||
testdata to test. Otherwise, _TEST_FILES is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tests=()):
|
|
||||||
unittest.TestSuite.__init__(self, tests)
|
|
||||||
|
|
||||||
argv = sys.argv and sys.argv[1:] or []
|
|
||||||
if argv:
|
|
||||||
test_files = argv
|
|
||||||
else:
|
|
||||||
test_files = _TEST_FILES
|
|
||||||
for test_file in test_files:
|
|
||||||
resource_path = os.path.join(_RESOURCE_PREFIX, test_file)
|
|
||||||
self.addTest(
|
|
||||||
filetestcase.AnnotatedFileTestCase(
|
|
||||||
resource_path,
|
|
||||||
runner.Run,
|
|
||||||
errors.ByName))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Don't let main parse args; it happens in the TestSuite.
|
|
||||||
googletest.main(argv=sys.argv[0:1], defaultTest='GJsLintTestSuite')
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Checks JavaScript files for common style guide violations.
|
|
||||||
|
|
||||||
gjslint.py is designed to be used as a PRESUBMIT script to check for javascript
|
|
||||||
style guide violations. As of now, it checks for the following violations:
|
|
||||||
|
|
||||||
* Missing and extra spaces
|
|
||||||
* Lines longer than 80 characters
|
|
||||||
* Missing newline at end of file
|
|
||||||
* Missing semicolon after function declaration
|
|
||||||
* Valid JsDoc including parameter matching
|
|
||||||
|
|
||||||
Someday it will validate to the best of its ability against the entirety of the
|
|
||||||
JavaScript style guide.
|
|
||||||
|
|
||||||
This file is a front end that parses arguments and flags. The core of the code
|
|
||||||
is in tokenizer.py and checker.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'nnaze@google.com (Nathan Naze)',)
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import itertools
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import errorrecord
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
from closure_linter.common import simplefileflags as fileflags
|
|
||||||
|
|
||||||
# Attempt import of multiprocessing (should be available in Python 2.6 and up).
|
|
||||||
try:
|
|
||||||
# pylint: disable=g-import-not-at-top
|
|
||||||
import multiprocessing
|
|
||||||
except ImportError:
|
|
||||||
multiprocessing = None
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('unix_mode', False,
|
|
||||||
'Whether to emit warnings in standard unix format.')
|
|
||||||
flags.DEFINE_boolean('beep', True, 'Whether to beep when errors are found.')
|
|
||||||
flags.DEFINE_boolean('time', False, 'Whether to emit timing statistics.')
|
|
||||||
flags.DEFINE_boolean('quiet', False, 'Whether to minimize logged messages. '
|
|
||||||
'Most useful for per-file linting, such as that performed '
|
|
||||||
'by the presubmit linter service.')
|
|
||||||
flags.DEFINE_boolean('check_html', False,
|
|
||||||
'Whether to check javascript in html files.')
|
|
||||||
flags.DEFINE_boolean('summary', False,
|
|
||||||
'Whether to show an error count summary.')
|
|
||||||
flags.DEFINE_list('additional_extensions', None, 'List of additional file '
|
|
||||||
'extensions (not js) that should be treated as '
|
|
||||||
'JavaScript files.')
|
|
||||||
flags.DEFINE_boolean('multiprocess',
|
|
||||||
platform.system() is 'Linux' and bool(multiprocessing),
|
|
||||||
'Whether to attempt parallelized linting using the '
|
|
||||||
'multiprocessing module. Enabled by default on Linux '
|
|
||||||
'if the multiprocessing module is present (Python 2.6+). '
|
|
||||||
'Otherwise disabled by default. '
|
|
||||||
'Disabling may make debugging easier.')
|
|
||||||
flags.ADOPT_module_key_flags(fileflags)
|
|
||||||
flags.ADOPT_module_key_flags(runner)
|
|
||||||
|
|
||||||
|
|
||||||
GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time',
|
|
||||||
'--check_html', '--summary', '--quiet']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _MultiprocessCheckPaths(paths):
|
|
||||||
"""Run _CheckPath over mutltiple processes.
|
|
||||||
|
|
||||||
Tokenization, passes, and checks are expensive operations. Running in a
|
|
||||||
single process, they can only run on one CPU/core. Instead,
|
|
||||||
shard out linting over all CPUs with multiprocessing to parallelize.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paths: paths to check.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pool = multiprocessing.Pool()
|
|
||||||
|
|
||||||
path_results = pool.imap(_CheckPath, paths)
|
|
||||||
for results in path_results:
|
|
||||||
for result in results:
|
|
||||||
yield result
|
|
||||||
|
|
||||||
# Force destruct before returning, as this can sometimes raise spurious
|
|
||||||
# "interrupted system call" (EINTR), which we can ignore.
|
|
||||||
try:
|
|
||||||
pool.close()
|
|
||||||
pool.join()
|
|
||||||
del pool
|
|
||||||
except OSError as err:
|
|
||||||
if err.errno is not errno.EINTR:
|
|
||||||
raise err
|
|
||||||
|
|
||||||
|
|
||||||
def _CheckPaths(paths):
|
|
||||||
"""Run _CheckPath on all paths in one thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paths: paths to check.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
results = _CheckPath(path)
|
|
||||||
for record in results:
|
|
||||||
yield record
|
|
||||||
|
|
||||||
|
|
||||||
def _CheckPath(path):
|
|
||||||
"""Check a path and return any errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: paths to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
error_handler = erroraccumulator.ErrorAccumulator()
|
|
||||||
runner.Run(path, error_handler)
|
|
||||||
|
|
||||||
make_error_record = lambda err: errorrecord.MakeErrorRecord(path, err)
|
|
||||||
return map(make_error_record, error_handler.GetErrors())
|
|
||||||
|
|
||||||
|
|
||||||
def _GetFilePaths(argv):
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
if FLAGS.check_html:
|
|
||||||
suffixes += ['.html', '.htm']
|
|
||||||
return fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
|
|
||||||
# Error printing functions
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintFileSummary(paths, records):
|
|
||||||
"""Print a detailed summary of the number of errors in each file."""
|
|
||||||
|
|
||||||
paths = list(paths)
|
|
||||||
paths.sort()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
path_errors = [e for e in records if e.path == path]
|
|
||||||
print '%s: %d' % (path, len(path_errors))
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintFileSeparator(path):
|
|
||||||
print '----- FILE : %s -----' % path
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintSummary(paths, error_records):
|
|
||||||
"""Print a summary of the number of errors and files."""
|
|
||||||
|
|
||||||
error_count = len(error_records)
|
|
||||||
all_paths = set(paths)
|
|
||||||
all_paths_count = len(all_paths)
|
|
||||||
|
|
||||||
if error_count is 0:
|
|
||||||
print '%d files checked, no errors found.' % all_paths_count
|
|
||||||
|
|
||||||
new_error_count = len([e for e in error_records if e.new_error])
|
|
||||||
|
|
||||||
error_paths = set([e.path for e in error_records])
|
|
||||||
error_paths_count = len(error_paths)
|
|
||||||
no_error_paths_count = all_paths_count - error_paths_count
|
|
||||||
|
|
||||||
if (error_count or new_error_count) and not FLAGS.quiet:
|
|
||||||
error_noun = 'error' if error_count == 1 else 'errors'
|
|
||||||
new_error_noun = 'error' if new_error_count == 1 else 'errors'
|
|
||||||
error_file_noun = 'file' if error_paths_count == 1 else 'files'
|
|
||||||
ok_file_noun = 'file' if no_error_paths_count == 1 else 'files'
|
|
||||||
print ('Found %d %s, including %d new %s, in %d %s (%d %s OK).' %
|
|
||||||
(error_count,
|
|
||||||
error_noun,
|
|
||||||
new_error_count,
|
|
||||||
new_error_noun,
|
|
||||||
error_paths_count,
|
|
||||||
error_file_noun,
|
|
||||||
no_error_paths_count,
|
|
||||||
ok_file_noun))
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintErrorRecords(error_records):
|
|
||||||
"""Print error records strings in the expected format."""
|
|
||||||
|
|
||||||
current_path = None
|
|
||||||
for record in error_records:
|
|
||||||
|
|
||||||
if current_path != record.path:
|
|
||||||
current_path = record.path
|
|
||||||
if not FLAGS.unix_mode:
|
|
||||||
_PrintFileSeparator(current_path)
|
|
||||||
|
|
||||||
print record.error_string
|
|
||||||
|
|
||||||
|
|
||||||
def _FormatTime(t):
|
|
||||||
"""Formats a duration as a human-readable string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
t: A duration in seconds.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A formatted duration string.
|
|
||||||
"""
|
|
||||||
if t < 1:
|
|
||||||
return '%dms' % round(t * 1000)
|
|
||||||
else:
|
|
||||||
return '%.2fs' % t
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
"""Main function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
"""
|
|
||||||
if argv is None:
|
|
||||||
argv = flags.FLAGS(sys.argv)
|
|
||||||
|
|
||||||
if FLAGS.time:
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
if FLAGS.check_html:
|
|
||||||
suffixes += ['.html', '.htm']
|
|
||||||
paths = fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
if FLAGS.multiprocess:
|
|
||||||
records_iter = _MultiprocessCheckPaths(paths)
|
|
||||||
else:
|
|
||||||
records_iter = _CheckPaths(paths)
|
|
||||||
|
|
||||||
records_iter, records_iter_copy = itertools.tee(records_iter, 2)
|
|
||||||
_PrintErrorRecords(records_iter_copy)
|
|
||||||
|
|
||||||
error_records = list(records_iter)
|
|
||||||
_PrintSummary(paths, error_records)
|
|
||||||
|
|
||||||
exit_code = 0
|
|
||||||
|
|
||||||
# If there are any errors
|
|
||||||
if error_records:
|
|
||||||
exit_code += 1
|
|
||||||
|
|
||||||
# If there are any new errors
|
|
||||||
if [r for r in error_records if r.new_error]:
|
|
||||||
exit_code += 2
|
|
||||||
|
|
||||||
if exit_code:
|
|
||||||
if FLAGS.summary:
|
|
||||||
_PrintFileSummary(paths, error_records)
|
|
||||||
|
|
||||||
if FLAGS.beep:
|
|
||||||
# Make a beep noise.
|
|
||||||
sys.stdout.write(chr(7))
|
|
||||||
|
|
||||||
# Write out instructions for using fixjsstyle script to fix some of the
|
|
||||||
# reported errors.
|
|
||||||
fix_args = []
|
|
||||||
for flag in sys.argv[1:]:
|
|
||||||
for f in GJSLINT_ONLY_FLAGS:
|
|
||||||
if flag.startswith(f):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
fix_args.append(flag)
|
|
||||||
|
|
||||||
if not FLAGS.quiet:
|
|
||||||
print """
|
|
||||||
Some of the errors reported by GJsLint may be auto-fixable using the script
|
|
||||||
fixjsstyle. Please double check any changes it makes and report any bugs. The
|
|
||||||
script can be run by executing:
|
|
||||||
|
|
||||||
fixjsstyle %s """ % ' '.join(fix_args)
|
|
||||||
|
|
||||||
if FLAGS.time:
|
|
||||||
print 'Done in %s.' % _FormatTime(time.time() - start_time)
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Methods for checking EcmaScript files for indentation issues."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)')
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import position
|
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_boolean('debug_indentation', False,
|
|
||||||
'Whether to print debugging information for indentation.')
|
|
||||||
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Context = ecmametadatapass.EcmaContext
|
|
||||||
Error = error.Error
|
|
||||||
Position = position.Position
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
# The general approach:
|
|
||||||
#
|
|
||||||
# 1. Build a stack of tokens that can affect indentation.
|
|
||||||
# For each token, we determine if it is a block or continuation token.
|
|
||||||
# Some tokens need to be temporarily overwritten in case they are removed
|
|
||||||
# before the end of the line.
|
|
||||||
# Much of the work here is determining which tokens to keep on the stack
|
|
||||||
# at each point. Operators, for example, should be removed once their
|
|
||||||
# expression or line is gone, while parentheses must stay until the matching
|
|
||||||
# end parentheses is found.
|
|
||||||
#
|
|
||||||
# 2. Given that stack, determine the allowable indentations.
|
|
||||||
# Due to flexible indentation rules in JavaScript, there may be many
|
|
||||||
# allowable indentations for each stack. We follows the general
|
|
||||||
# "no false positives" approach of GJsLint and build the most permissive
|
|
||||||
# set possible.
|
|
||||||
|
|
||||||
|
|
||||||
class TokenInfo(object):
|
|
||||||
"""Stores information about a token.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
token: The token
|
|
||||||
is_block: Whether the token represents a block indentation.
|
|
||||||
is_transient: Whether the token should be automatically removed without
|
|
||||||
finding a matching end token.
|
|
||||||
overridden_by: TokenInfo for a token that overrides the indentation that
|
|
||||||
this token would require.
|
|
||||||
is_permanent_override: Whether the override on this token should persist
|
|
||||||
even after the overriding token is removed from the stack. For example:
|
|
||||||
x([
|
|
||||||
1],
|
|
||||||
2);
|
|
||||||
needs this to be set so the last line is not required to be a continuation
|
|
||||||
indent.
|
|
||||||
line_number: The effective line number of this token. Will either be the
|
|
||||||
actual line number or the one before it in the case of a mis-wrapped
|
|
||||||
operator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, token, is_block=False):
|
|
||||||
"""Initializes a TokenInfo object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token
|
|
||||||
is_block: Whether the token represents a block indentation.
|
|
||||||
"""
|
|
||||||
self.token = token
|
|
||||||
self.overridden_by = None
|
|
||||||
self.is_permanent_override = False
|
|
||||||
self.is_block = is_block
|
|
||||||
self.is_transient = not is_block and token.type not in (
|
|
||||||
Type.START_PAREN, Type.START_PARAMETERS)
|
|
||||||
self.line_number = token.line_number
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
result = '\n %s' % self.token
|
|
||||||
if self.overridden_by:
|
|
||||||
result = '%s OVERRIDDEN [by "%s"]' % (
|
|
||||||
result, self.overridden_by.token.string)
|
|
||||||
result += ' {is_block: %s, is_transient: %s}' % (
|
|
||||||
self.is_block, self.is_transient)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class IndentationRules(object):
|
|
||||||
"""EmcaScript indentation rules.
|
|
||||||
|
|
||||||
Can be used to find common indentation errors in JavaScript, ActionScript and
|
|
||||||
other Ecma like scripting languages.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initializes the IndentationRules checker."""
|
|
||||||
self._stack = []
|
|
||||||
|
|
||||||
# Map from line number to number of characters it is off in indentation.
|
|
||||||
self._start_index_offset = {}
|
|
||||||
|
|
||||||
def Finalize(self):
|
|
||||||
if self._stack:
|
|
||||||
old_stack = self._stack
|
|
||||||
self._stack = []
|
|
||||||
raise Exception('INTERNAL ERROR: indentation stack is not empty: %r' %
|
|
||||||
old_stack)
|
|
||||||
|
|
||||||
def CheckToken(self, token, state):
|
|
||||||
"""Checks a token for indentation errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration
|
|
||||||
state: Additional information about the current tree state
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An error array [error code, error string, error token] if the token is
|
|
||||||
improperly indented, or None if indentation is correct.
|
|
||||||
"""
|
|
||||||
|
|
||||||
token_type = token.type
|
|
||||||
indentation_errors = []
|
|
||||||
stack = self._stack
|
|
||||||
is_first = self._IsFirstNonWhitespaceTokenInLine(token)
|
|
||||||
|
|
||||||
# Add tokens that could decrease indentation before checking.
|
|
||||||
if token_type == Type.END_PAREN:
|
|
||||||
self._PopTo(Type.START_PAREN)
|
|
||||||
|
|
||||||
elif token_type == Type.END_PARAMETERS:
|
|
||||||
self._PopTo(Type.START_PARAMETERS)
|
|
||||||
|
|
||||||
elif token_type == Type.END_BRACKET:
|
|
||||||
self._PopTo(Type.START_BRACKET)
|
|
||||||
|
|
||||||
elif token_type == Type.END_BLOCK:
|
|
||||||
start_token = self._PopTo(Type.START_BLOCK)
|
|
||||||
# Check for required goog.scope comment.
|
|
||||||
if start_token:
|
|
||||||
goog_scope = tokenutil.GoogScopeOrNoneFromStartBlock(start_token.token)
|
|
||||||
if goog_scope is not None:
|
|
||||||
if not token.line.endswith('; // goog.scope\n'):
|
|
||||||
if (token.line.find('//') > -1 and
|
|
||||||
token.line.find('goog.scope') >
|
|
||||||
token.line.find('//')):
|
|
||||||
indentation_errors.append([
|
|
||||||
errors.MALFORMED_END_OF_SCOPE_COMMENT,
|
|
||||||
('Malformed end of goog.scope comment. Please use the '
|
|
||||||
'exact following syntax to close the scope:\n'
|
|
||||||
'}); // goog.scope'),
|
|
||||||
token,
|
|
||||||
Position(token.start_index, token.length)])
|
|
||||||
else:
|
|
||||||
indentation_errors.append([
|
|
||||||
errors.MISSING_END_OF_SCOPE_COMMENT,
|
|
||||||
('Missing comment for end of goog.scope which opened at line '
|
|
||||||
'%d. End the scope with:\n'
|
|
||||||
'}); // goog.scope' %
|
|
||||||
(start_token.line_number)),
|
|
||||||
token,
|
|
||||||
Position(token.start_index, token.length)])
|
|
||||||
|
|
||||||
elif token_type == Type.KEYWORD and token.string in ('case', 'default'):
|
|
||||||
self._Add(self._PopTo(Type.START_BLOCK))
|
|
||||||
|
|
||||||
elif token_type == Type.SEMICOLON:
|
|
||||||
self._PopTransient()
|
|
||||||
|
|
||||||
if (is_first and
|
|
||||||
token_type not in (Type.COMMENT, Type.DOC_PREFIX, Type.STRING_TEXT)):
|
|
||||||
if flags.FLAGS.debug_indentation:
|
|
||||||
print 'Line #%d: stack %r' % (token.line_number, stack)
|
|
||||||
|
|
||||||
# Ignore lines that start in JsDoc since we don't check them properly yet.
|
|
||||||
# TODO(robbyw): Support checking JsDoc indentation.
|
|
||||||
# Ignore lines that start as multi-line strings since indentation is N/A.
|
|
||||||
# Ignore lines that start with operators since we report that already.
|
|
||||||
# Ignore lines with tabs since we report that already.
|
|
||||||
expected = self._GetAllowableIndentations()
|
|
||||||
actual = self._GetActualIndentation(token)
|
|
||||||
|
|
||||||
# Special case comments describing else, case, and default. Allow them
|
|
||||||
# to outdent to the parent block.
|
|
||||||
if token_type in Type.COMMENT_TYPES:
|
|
||||||
next_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
|
|
||||||
if next_code and next_code.type == Type.END_BLOCK:
|
|
||||||
next_code = tokenutil.SearchExcept(next_code, Type.NON_CODE_TYPES)
|
|
||||||
if next_code and next_code.string in ('else', 'case', 'default'):
|
|
||||||
# TODO(robbyw): This almost certainly introduces false negatives.
|
|
||||||
expected |= self._AddToEach(expected, -2)
|
|
||||||
|
|
||||||
if actual >= 0 and actual not in expected:
|
|
||||||
expected = sorted(expected)
|
|
||||||
indentation_errors.append([
|
|
||||||
errors.WRONG_INDENTATION,
|
|
||||||
'Wrong indentation: expected any of {%s} but got %d' % (
|
|
||||||
', '.join('%d' % x for x in expected if x < 80), actual),
|
|
||||||
token,
|
|
||||||
Position(actual, expected[0])])
|
|
||||||
self._start_index_offset[token.line_number] = expected[0] - actual
|
|
||||||
|
|
||||||
# Add tokens that could increase indentation.
|
|
||||||
if token_type == Type.START_BRACKET:
|
|
||||||
self._Add(TokenInfo(
|
|
||||||
token=token,
|
|
||||||
is_block=token.metadata.context.type == Context.ARRAY_LITERAL))
|
|
||||||
|
|
||||||
elif token_type == Type.START_BLOCK or token.metadata.is_implied_block:
|
|
||||||
self._Add(TokenInfo(token=token, is_block=True))
|
|
||||||
|
|
||||||
elif token_type in (Type.START_PAREN, Type.START_PARAMETERS):
|
|
||||||
self._Add(TokenInfo(token=token, is_block=False))
|
|
||||||
|
|
||||||
elif token_type == Type.KEYWORD and token.string == 'return':
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
|
|
||||||
elif not token.IsLastInLine() and (
|
|
||||||
token.IsAssignment() or token.IsOperator('?')):
|
|
||||||
self._Add(TokenInfo(token=token))
|
|
||||||
|
|
||||||
# Handle implied block closes.
|
|
||||||
if token.metadata.is_implied_block_close:
|
|
||||||
self._PopToImpliedBlock()
|
|
||||||
|
|
||||||
# Add some tokens only if they appear at the end of the line.
|
|
||||||
is_last = self._IsLastCodeInLine(token)
|
|
||||||
if is_last:
|
|
||||||
next_code_token = tokenutil.GetNextCodeToken(token)
|
|
||||||
# Increase required indentation if this is an overlong wrapped statement
|
|
||||||
# ending in an operator.
|
|
||||||
if token_type == Type.OPERATOR:
|
|
||||||
if token.string == ':':
|
|
||||||
if stack and stack[-1].token.string == '?':
|
|
||||||
# When a ternary : is on a different line than its '?', it doesn't
|
|
||||||
# add indentation.
|
|
||||||
if token.line_number == stack[-1].token.line_number:
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
elif token.metadata.context.type == Context.CASE_BLOCK:
|
|
||||||
# Pop transient tokens from say, line continuations, e.g.,
|
|
||||||
# case x.
|
|
||||||
# y:
|
|
||||||
# Want to pop the transient 4 space continuation indent.
|
|
||||||
self._PopTransient()
|
|
||||||
# Starting the body of the case statement, which is a type of
|
|
||||||
# block.
|
|
||||||
self._Add(TokenInfo(token=token, is_block=True))
|
|
||||||
elif token.metadata.context.type == Context.LITERAL_ELEMENT:
|
|
||||||
# When in an object literal, acts as operator indicating line
|
|
||||||
# continuations.
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
else:
|
|
||||||
# ':' might also be a statement label, no effect on indentation in
|
|
||||||
# this case.
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif token.string != ',':
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
else:
|
|
||||||
# The token is a comma.
|
|
||||||
if token.metadata.context.type == Context.VAR:
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
elif token.metadata.context.type != Context.PARAMETERS:
|
|
||||||
self._PopTransient()
|
|
||||||
# Increase required indentation if this is the end of a statement that's
|
|
||||||
# continued with an operator on the next line (e.g. the '.').
|
|
||||||
elif (next_code_token and next_code_token.type == Type.OPERATOR and
|
|
||||||
not next_code_token.metadata.IsUnaryOperator()):
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
elif token_type == Type.PARAMETERS and token.string.endswith(','):
|
|
||||||
# Parameter lists.
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
elif token.IsKeyword('var'):
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
elif token.metadata.is_implied_semicolon:
|
|
||||||
self._PopTransient()
|
|
||||||
elif token.IsAssignment():
|
|
||||||
self._Add(TokenInfo(token))
|
|
||||||
|
|
||||||
return indentation_errors
|
|
||||||
|
|
||||||
def _AddToEach(self, original, amount):
|
|
||||||
"""Returns a new set with the given amount added to each element.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
original: The original set of numbers
|
|
||||||
amount: The amount to add to each element
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A new set containing each element of the original set added to the amount.
|
|
||||||
"""
|
|
||||||
return set([x + amount for x in original])
|
|
||||||
|
|
||||||
_HARD_STOP_TYPES = (Type.START_PAREN, Type.START_PARAMETERS,
|
|
||||||
Type.START_BRACKET)
|
|
||||||
|
|
||||||
_HARD_STOP_STRINGS = ('return', '?')
|
|
||||||
|
|
||||||
def _IsHardStop(self, token):
|
|
||||||
"""Determines if the given token can have a hard stop after it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: token to examine
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the token can have a hard stop after it.
|
|
||||||
|
|
||||||
Hard stops are indentations defined by the position of another token as in
|
|
||||||
indentation lined up with return, (, [, and ?.
|
|
||||||
"""
|
|
||||||
return (token.type in self._HARD_STOP_TYPES or
|
|
||||||
token.string in self._HARD_STOP_STRINGS or
|
|
||||||
token.IsAssignment())
|
|
||||||
|
|
||||||
def _GetAllowableIndentations(self):
|
|
||||||
"""Computes the set of allowable indentations.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The set of allowable indentations, given the current stack.
|
|
||||||
"""
|
|
||||||
expected = set([0])
|
|
||||||
hard_stops = set([])
|
|
||||||
|
|
||||||
# Whether the tokens are still in the same continuation, meaning additional
|
|
||||||
# indentation is optional. As an example:
|
|
||||||
# x = 5 +
|
|
||||||
# 6 +
|
|
||||||
# 7;
|
|
||||||
# The second '+' does not add any required indentation.
|
|
||||||
in_same_continuation = False
|
|
||||||
|
|
||||||
for token_info in self._stack:
|
|
||||||
token = token_info.token
|
|
||||||
|
|
||||||
# Handle normal additive indentation tokens.
|
|
||||||
if not token_info.overridden_by and token.string != 'return':
|
|
||||||
if token_info.is_block:
|
|
||||||
expected = self._AddToEach(expected, 2)
|
|
||||||
hard_stops = self._AddToEach(hard_stops, 2)
|
|
||||||
in_same_continuation = False
|
|
||||||
elif in_same_continuation:
|
|
||||||
expected |= self._AddToEach(expected, 4)
|
|
||||||
hard_stops |= self._AddToEach(hard_stops, 4)
|
|
||||||
else:
|
|
||||||
expected = self._AddToEach(expected, 4)
|
|
||||||
hard_stops |= self._AddToEach(hard_stops, 4)
|
|
||||||
in_same_continuation = True
|
|
||||||
|
|
||||||
# Handle hard stops after (, [, return, =, and ?
|
|
||||||
if self._IsHardStop(token):
|
|
||||||
override_is_hard_stop = (token_info.overridden_by and
|
|
||||||
self._IsHardStop(
|
|
||||||
token_info.overridden_by.token))
|
|
||||||
if token.type == Type.START_PAREN and token.previous:
|
|
||||||
# For someFunction(...) we allow to indent at the beginning of the
|
|
||||||
# identifier +4
|
|
||||||
prev = token.previous
|
|
||||||
if (prev.type == Type.IDENTIFIER and
|
|
||||||
prev.line_number == token.line_number):
|
|
||||||
hard_stops.add(prev.start_index + 4)
|
|
||||||
if not override_is_hard_stop:
|
|
||||||
start_index = token.start_index
|
|
||||||
if token.line_number in self._start_index_offset:
|
|
||||||
start_index += self._start_index_offset[token.line_number]
|
|
||||||
if (token.type in (Type.START_PAREN, Type.START_PARAMETERS) and
|
|
||||||
not token_info.overridden_by):
|
|
||||||
hard_stops.add(start_index + 1)
|
|
||||||
|
|
||||||
elif token.string == 'return' and not token_info.overridden_by:
|
|
||||||
hard_stops.add(start_index + 7)
|
|
||||||
|
|
||||||
elif token.type == Type.START_BRACKET:
|
|
||||||
hard_stops.add(start_index + 1)
|
|
||||||
|
|
||||||
elif token.IsAssignment():
|
|
||||||
hard_stops.add(start_index + len(token.string) + 1)
|
|
||||||
|
|
||||||
elif token.IsOperator('?') and not token_info.overridden_by:
|
|
||||||
hard_stops.add(start_index + 2)
|
|
||||||
|
|
||||||
return (expected | hard_stops) or set([0])
|
|
||||||
|
|
||||||
def _GetActualIndentation(self, token):
|
|
||||||
"""Gets the actual indentation of the line containing the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Any token on the line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The actual indentation of the line containing the given token. Returns
|
|
||||||
-1 if this line should be ignored due to the presence of tabs.
|
|
||||||
"""
|
|
||||||
# Move to the first token in the line
|
|
||||||
token = tokenutil.GetFirstTokenInSameLine(token)
|
|
||||||
|
|
||||||
# If it is whitespace, it is the indentation.
|
|
||||||
if token.type == Type.WHITESPACE:
|
|
||||||
if token.string.find('\t') >= 0:
|
|
||||||
return -1
|
|
||||||
else:
|
|
||||||
return len(token.string)
|
|
||||||
elif token.type == Type.PARAMETERS:
|
|
||||||
return len(token.string) - len(token.string.lstrip())
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def _IsFirstNonWhitespaceTokenInLine(self, token):
|
|
||||||
"""Determines if the given token is the first non-space token on its line.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the token is the first non-whitespace token on its line.
|
|
||||||
"""
|
|
||||||
if token.type in (Type.WHITESPACE, Type.BLANK_LINE):
|
|
||||||
return False
|
|
||||||
if token.IsFirstInLine():
|
|
||||||
return True
|
|
||||||
return (token.previous and token.previous.IsFirstInLine() and
|
|
||||||
token.previous.type == Type.WHITESPACE)
|
|
||||||
|
|
||||||
def _IsLastCodeInLine(self, token):
|
|
||||||
"""Determines if the given token is the last code token on its line.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the token is the last code token on its line.
|
|
||||||
"""
|
|
||||||
if token.type in Type.NON_CODE_TYPES:
|
|
||||||
return False
|
|
||||||
start_token = token
|
|
||||||
while True:
|
|
||||||
token = token.next
|
|
||||||
if not token or token.line_number != start_token.line_number:
|
|
||||||
return True
|
|
||||||
if token.type not in Type.NON_CODE_TYPES:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _AllFunctionPropertyAssignTokens(self, start_token, end_token):
|
|
||||||
"""Checks if tokens are (likely) a valid function property assignment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: Start of the token range.
|
|
||||||
end_token: End of the token range.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if all tokens between start_token and end_token are legal tokens
|
|
||||||
within a function declaration and assignment into a property.
|
|
||||||
"""
|
|
||||||
for token in tokenutil.GetTokenRange(start_token, end_token):
|
|
||||||
fn_decl_tokens = (Type.FUNCTION_DECLARATION,
|
|
||||||
Type.PARAMETERS,
|
|
||||||
Type.START_PARAMETERS,
|
|
||||||
Type.END_PARAMETERS,
|
|
||||||
Type.END_PAREN)
|
|
||||||
if (token.type not in fn_decl_tokens and
|
|
||||||
token.IsCode() and
|
|
||||||
not tokenutil.IsIdentifierOrDot(token) and
|
|
||||||
not token.IsAssignment() and
|
|
||||||
not (token.type == Type.OPERATOR and token.string == ',')):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _Add(self, token_info):
|
|
||||||
"""Adds the given token info to the stack.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_info: The token information to add.
|
|
||||||
"""
|
|
||||||
if self._stack and self._stack[-1].token == token_info.token:
|
|
||||||
# Don't add the same token twice.
|
|
||||||
return
|
|
||||||
|
|
||||||
if token_info.is_block or token_info.token.type == Type.START_PAREN:
|
|
||||||
scope_token = tokenutil.GoogScopeOrNoneFromStartBlock(token_info.token)
|
|
||||||
token_info.overridden_by = TokenInfo(scope_token) if scope_token else None
|
|
||||||
|
|
||||||
if (token_info.token.type == Type.START_BLOCK and
|
|
||||||
token_info.token.metadata.context.type == Context.BLOCK):
|
|
||||||
# Handle function() {} assignments: their block contents get special
|
|
||||||
# treatment and are allowed to just indent by two whitespace.
|
|
||||||
# For example
|
|
||||||
# long.long.name = function(
|
|
||||||
# a) {
|
|
||||||
# In this case the { and the = are on different lines. But the
|
|
||||||
# override should still apply for all previous stack tokens that are
|
|
||||||
# part of an assignment of a block.
|
|
||||||
|
|
||||||
has_assignment = any(x for x in self._stack if x.token.IsAssignment())
|
|
||||||
if has_assignment:
|
|
||||||
last_token = token_info.token.previous
|
|
||||||
for stack_info in reversed(self._stack):
|
|
||||||
if (last_token and
|
|
||||||
not self._AllFunctionPropertyAssignTokens(stack_info.token,
|
|
||||||
last_token)):
|
|
||||||
break
|
|
||||||
stack_info.overridden_by = token_info
|
|
||||||
stack_info.is_permanent_override = True
|
|
||||||
last_token = stack_info.token
|
|
||||||
|
|
||||||
index = len(self._stack) - 1
|
|
||||||
while index >= 0:
|
|
||||||
stack_info = self._stack[index]
|
|
||||||
stack_token = stack_info.token
|
|
||||||
|
|
||||||
if stack_info.line_number == token_info.line_number:
|
|
||||||
# In general, tokens only override each other when they are on
|
|
||||||
# the same line.
|
|
||||||
stack_info.overridden_by = token_info
|
|
||||||
if (token_info.token.type == Type.START_BLOCK and
|
|
||||||
(stack_token.IsAssignment() or
|
|
||||||
stack_token.type in (Type.IDENTIFIER, Type.START_PAREN))):
|
|
||||||
# Multi-line blocks have lasting overrides, as in:
|
|
||||||
# callFn({
|
|
||||||
# a: 10
|
|
||||||
# },
|
|
||||||
# 30);
|
|
||||||
# b/11450054. If a string is not closed properly then close_block
|
|
||||||
# could be null.
|
|
||||||
close_block = token_info.token.metadata.context.end_token
|
|
||||||
stack_info.is_permanent_override = close_block and (
|
|
||||||
close_block.line_number != token_info.token.line_number)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
self._stack.append(token_info)
|
|
||||||
|
|
||||||
def _Pop(self):
|
|
||||||
"""Pops the top token from the stack.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The popped token info.
|
|
||||||
"""
|
|
||||||
token_info = self._stack.pop()
|
|
||||||
if token_info.token.type not in (Type.START_BLOCK, Type.START_BRACKET):
|
|
||||||
# Remove any temporary overrides.
|
|
||||||
self._RemoveOverrides(token_info)
|
|
||||||
else:
|
|
||||||
# For braces and brackets, which can be object and array literals, remove
|
|
||||||
# overrides when the literal is closed on the same line.
|
|
||||||
token_check = token_info.token
|
|
||||||
same_type = token_check.type
|
|
||||||
goal_type = None
|
|
||||||
if token_info.token.type == Type.START_BRACKET:
|
|
||||||
goal_type = Type.END_BRACKET
|
|
||||||
else:
|
|
||||||
goal_type = Type.END_BLOCK
|
|
||||||
line_number = token_info.token.line_number
|
|
||||||
count = 0
|
|
||||||
while token_check and token_check.line_number == line_number:
|
|
||||||
if token_check.type == goal_type:
|
|
||||||
count -= 1
|
|
||||||
if not count:
|
|
||||||
self._RemoveOverrides(token_info)
|
|
||||||
break
|
|
||||||
if token_check.type == same_type:
|
|
||||||
count += 1
|
|
||||||
token_check = token_check.next
|
|
||||||
return token_info
|
|
||||||
|
|
||||||
def _PopToImpliedBlock(self):
|
|
||||||
"""Pops the stack until an implied block token is found."""
|
|
||||||
while not self._Pop().token.metadata.is_implied_block:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _PopTo(self, stop_type):
|
|
||||||
"""Pops the stack until a token of the given type is popped.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
stop_type: The type of token to pop to.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The token info of the given type that was popped.
|
|
||||||
"""
|
|
||||||
last = None
|
|
||||||
while True:
|
|
||||||
last = self._Pop()
|
|
||||||
if last.token.type == stop_type:
|
|
||||||
break
|
|
||||||
return last
|
|
||||||
|
|
||||||
def _RemoveOverrides(self, token_info):
|
|
||||||
"""Marks any token that was overridden by this token as active again.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_info: The token that is being removed from the stack.
|
|
||||||
"""
|
|
||||||
for stack_token in self._stack:
|
|
||||||
if (stack_token.overridden_by == token_info and
|
|
||||||
not stack_token.is_permanent_override):
|
|
||||||
stack_token.overridden_by = None
|
|
||||||
|
|
||||||
def _PopTransient(self):
|
|
||||||
"""Pops all transient tokens - i.e. not blocks, literals, or parens."""
|
|
||||||
while self._stack and self._stack[-1].is_transient:
|
|
||||||
self._Pop()
|
|
||||||
@@ -1,754 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
"""Methods for checking JS files for common style guide violations.
|
|
||||||
|
|
||||||
These style guide violations should only apply to JavaScript and not an Ecma
|
|
||||||
scripting languages.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'jacobr@google.com (Jacob Richman)')
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from closure_linter import ecmalintrules
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascripttokenizer
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import requireprovidesorter
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import position
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Error = error.Error
|
|
||||||
Position = position.Position
|
|
||||||
Rule = error_check.Rule
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules):
|
|
||||||
"""JavaScript lint rules that catch JavaScript specific style errors."""
|
|
||||||
|
|
||||||
def __init__(self, namespaces_info):
|
|
||||||
"""Initializes a JavaScriptLintRules instance."""
|
|
||||||
ecmalintrules.EcmaScriptLintRules.__init__(self)
|
|
||||||
self._namespaces_info = namespaces_info
|
|
||||||
self._declared_private_member_tokens = {}
|
|
||||||
self._declared_private_members = set()
|
|
||||||
self._used_private_members = set()
|
|
||||||
# A stack of dictionaries, one for each function scope entered. Each
|
|
||||||
# dictionary is keyed by an identifier that defines a local variable and has
|
|
||||||
# a token as its value.
|
|
||||||
self._unused_local_variables_by_scope = []
|
|
||||||
|
|
||||||
def HandleMissingParameterDoc(self, token, param_name):
|
|
||||||
"""Handle errors associated with a parameter missing a param tag."""
|
|
||||||
self._HandleError(errors.MISSING_PARAMETER_DOCUMENTATION,
|
|
||||||
'Missing docs for parameter: "%s"' % param_name, token)
|
|
||||||
|
|
||||||
# pylint: disable=too-many-statements
|
|
||||||
def CheckToken(self, token, state):
|
|
||||||
"""Checks a token, given the current parser_state, for warnings and errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration
|
|
||||||
state: parser_state object that indicates the current state in the page
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Call the base class's CheckToken function.
|
|
||||||
super(JavaScriptLintRules, self).CheckToken(token, state)
|
|
||||||
|
|
||||||
# Store some convenience variables
|
|
||||||
namespaces_info = self._namespaces_info
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.UNUSED_LOCAL_VARIABLES):
|
|
||||||
self._CheckUnusedLocalVariables(token, state)
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS):
|
|
||||||
# Find all assignments to private members.
|
|
||||||
if token.type == Type.SIMPLE_LVALUE:
|
|
||||||
identifier = token.string
|
|
||||||
if identifier.endswith('_') and not identifier.endswith('__'):
|
|
||||||
doc_comment = state.GetDocComment()
|
|
||||||
suppressed = doc_comment and (
|
|
||||||
'underscore' in doc_comment.suppressions or
|
|
||||||
'unusedPrivateMembers' in doc_comment.suppressions)
|
|
||||||
if not suppressed:
|
|
||||||
# Look for static members defined on a provided namespace.
|
|
||||||
if namespaces_info:
|
|
||||||
namespace = namespaces_info.GetClosurizedNamespace(identifier)
|
|
||||||
provided_namespaces = namespaces_info.GetProvidedNamespaces()
|
|
||||||
else:
|
|
||||||
namespace = None
|
|
||||||
provided_namespaces = set()
|
|
||||||
|
|
||||||
# Skip cases of this.something_.somethingElse_.
|
|
||||||
regex = re.compile(r'^this\.[a-zA-Z_]+$')
|
|
||||||
if namespace in provided_namespaces or regex.match(identifier):
|
|
||||||
variable = identifier.split('.')[-1]
|
|
||||||
self._declared_private_member_tokens[variable] = token
|
|
||||||
self._declared_private_members.add(variable)
|
|
||||||
elif not identifier.endswith('__'):
|
|
||||||
# Consider setting public members of private members to be a usage.
|
|
||||||
for piece in identifier.split('.'):
|
|
||||||
if piece.endswith('_'):
|
|
||||||
self._used_private_members.add(piece)
|
|
||||||
|
|
||||||
# Find all usages of private members.
|
|
||||||
if token.type == Type.IDENTIFIER:
|
|
||||||
for piece in token.string.split('.'):
|
|
||||||
if piece.endswith('_'):
|
|
||||||
self._used_private_members.add(piece)
|
|
||||||
|
|
||||||
if token.type == Type.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
|
|
||||||
if flag.flag_type == 'param' and flag.name_token is not None:
|
|
||||||
self._CheckForMissingSpaceBeforeToken(
|
|
||||||
token.attached_object.name_token)
|
|
||||||
|
|
||||||
if flag.type is not None and flag.name is not None:
|
|
||||||
if error_check.ShouldCheck(Rule.VARIABLE_ARG_MARKER):
|
|
||||||
# Check for variable arguments marker in type.
|
|
||||||
if flag.jstype.IsVarArgsType() and flag.name != 'var_args':
|
|
||||||
self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_NAME,
|
|
||||||
'Variable length argument %s must be renamed '
|
|
||||||
'to var_args.' % flag.name,
|
|
||||||
token)
|
|
||||||
elif not flag.jstype.IsVarArgsType() and flag.name == 'var_args':
|
|
||||||
self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_TYPE,
|
|
||||||
'Variable length argument %s type must start '
|
|
||||||
'with \'...\'.' % flag.name,
|
|
||||||
token)
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.OPTIONAL_TYPE_MARKER):
|
|
||||||
# Check for optional marker in type.
|
|
||||||
if (flag.jstype.opt_arg and
|
|
||||||
not flag.name.startswith('opt_')):
|
|
||||||
self._HandleError(errors.JSDOC_MISSING_OPTIONAL_PREFIX,
|
|
||||||
'Optional parameter name %s must be prefixed '
|
|
||||||
'with opt_.' % flag.name,
|
|
||||||
token)
|
|
||||||
elif (not flag.jstype.opt_arg and
|
|
||||||
flag.name.startswith('opt_')):
|
|
||||||
self._HandleError(errors.JSDOC_MISSING_OPTIONAL_TYPE,
|
|
||||||
'Optional parameter %s type must end with =.' %
|
|
||||||
flag.name,
|
|
||||||
token)
|
|
||||||
|
|
||||||
if flag.flag_type in state.GetDocFlag().HAS_TYPE:
|
|
||||||
# Check for both missing type token and empty type braces '{}'
|
|
||||||
# Missing suppress types are reported separately and we allow enums,
|
|
||||||
# const, private, public and protected without types.
|
|
||||||
if (flag.flag_type not in state.GetDocFlag().CAN_OMIT_TYPE
|
|
||||||
and (not flag.jstype or flag.jstype.IsEmpty())):
|
|
||||||
self._HandleError(errors.MISSING_JSDOC_TAG_TYPE,
|
|
||||||
'Missing type in %s tag' % token.string, token)
|
|
||||||
|
|
||||||
elif flag.name_token and flag.type_end_token and tokenutil.Compare(
|
|
||||||
flag.type_end_token, flag.name_token) > 0:
|
|
||||||
self._HandleError(
|
|
||||||
errors.OUT_OF_ORDER_JSDOC_TAG_TYPE,
|
|
||||||
'Type should be immediately after %s tag' % token.string,
|
|
||||||
token)
|
|
||||||
|
|
||||||
elif token.type == Type.DOUBLE_QUOTE_STRING_START:
|
|
||||||
next_token = token.next
|
|
||||||
while next_token.type == Type.STRING_TEXT:
|
|
||||||
if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search(
|
|
||||||
next_token.string):
|
|
||||||
break
|
|
||||||
next_token = next_token.next
|
|
||||||
else:
|
|
||||||
self._HandleError(
|
|
||||||
errors.UNNECESSARY_DOUBLE_QUOTED_STRING,
|
|
||||||
'Single-quoted string preferred over double-quoted string.',
|
|
||||||
token,
|
|
||||||
position=Position.All(token.string))
|
|
||||||
|
|
||||||
elif token.type == Type.END_DOC_COMMENT:
|
|
||||||
doc_comment = state.GetDocComment()
|
|
||||||
|
|
||||||
# When @externs appears in a @fileoverview comment, it should trigger
|
|
||||||
# the same limited doc checks as a special filename like externs.js.
|
|
||||||
if doc_comment.HasFlag('fileoverview') and doc_comment.HasFlag('externs'):
|
|
||||||
self._SetLimitedDocChecks(True)
|
|
||||||
|
|
||||||
if (error_check.ShouldCheck(Rule.BLANK_LINES_AT_TOP_LEVEL) and
|
|
||||||
not self._is_html and
|
|
||||||
state.InTopLevel() and
|
|
||||||
not state.InNonScopeBlock()):
|
|
||||||
|
|
||||||
# Check if we're in a fileoverview or constructor JsDoc.
|
|
||||||
is_constructor = (
|
|
||||||
doc_comment.HasFlag('constructor') or
|
|
||||||
doc_comment.HasFlag('interface'))
|
|
||||||
# @fileoverview is an optional tag so if the dosctring is the first
|
|
||||||
# token in the file treat it as a file level docstring.
|
|
||||||
is_file_level_comment = (
|
|
||||||
doc_comment.HasFlag('fileoverview') or
|
|
||||||
not doc_comment.start_token.previous)
|
|
||||||
|
|
||||||
# If the comment is not a file overview, and it does not immediately
|
|
||||||
# precede some code, skip it.
|
|
||||||
# NOTE: The tokenutil methods are not used here because of their
|
|
||||||
# behavior at the top of a file.
|
|
||||||
next_token = token.next
|
|
||||||
if (not next_token or
|
|
||||||
(not is_file_level_comment and
|
|
||||||
next_token.type in Type.NON_CODE_TYPES)):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Don't require extra blank lines around suppression of extra
|
|
||||||
# goog.require errors.
|
|
||||||
if (doc_comment.SuppressionOnly() and
|
|
||||||
next_token.type == Type.IDENTIFIER and
|
|
||||||
next_token.string in ['goog.provide', 'goog.require']):
|
|
||||||
return
|
|
||||||
|
|
||||||
# Find the start of this block (include comments above the block, unless
|
|
||||||
# this is a file overview).
|
|
||||||
block_start = doc_comment.start_token
|
|
||||||
if not is_file_level_comment:
|
|
||||||
token = block_start.previous
|
|
||||||
while token and token.type in Type.COMMENT_TYPES:
|
|
||||||
block_start = token
|
|
||||||
token = token.previous
|
|
||||||
|
|
||||||
# Count the number of blank lines before this block.
|
|
||||||
blank_lines = 0
|
|
||||||
token = block_start.previous
|
|
||||||
while token and token.type in [Type.WHITESPACE, Type.BLANK_LINE]:
|
|
||||||
if token.type == Type.BLANK_LINE:
|
|
||||||
# A blank line.
|
|
||||||
blank_lines += 1
|
|
||||||
elif token.type == Type.WHITESPACE and not token.line.strip():
|
|
||||||
# A line with only whitespace on it.
|
|
||||||
blank_lines += 1
|
|
||||||
token = token.previous
|
|
||||||
|
|
||||||
# Log errors.
|
|
||||||
error_message = False
|
|
||||||
expected_blank_lines = 0
|
|
||||||
|
|
||||||
# Only need blank line before file overview if it is not the beginning
|
|
||||||
# of the file, e.g. copyright is first.
|
|
||||||
if is_file_level_comment and blank_lines == 0 and block_start.previous:
|
|
||||||
error_message = 'Should have a blank line before a file overview.'
|
|
||||||
expected_blank_lines = 1
|
|
||||||
elif is_constructor and blank_lines != 3:
|
|
||||||
error_message = (
|
|
||||||
'Should have 3 blank lines before a constructor/interface.')
|
|
||||||
expected_blank_lines = 3
|
|
||||||
elif (not is_file_level_comment and not is_constructor and
|
|
||||||
blank_lines != 2):
|
|
||||||
error_message = 'Should have 2 blank lines between top-level blocks.'
|
|
||||||
expected_blank_lines = 2
|
|
||||||
|
|
||||||
if error_message:
|
|
||||||
self._HandleError(
|
|
||||||
errors.WRONG_BLANK_LINE_COUNT, error_message,
|
|
||||||
block_start, position=Position.AtBeginning(),
|
|
||||||
fix_data=expected_blank_lines - blank_lines)
|
|
||||||
|
|
||||||
elif token.type == Type.END_BLOCK:
|
|
||||||
if state.InFunction() and state.IsFunctionClose():
|
|
||||||
is_immediately_called = (token.next and
|
|
||||||
token.next.type == Type.START_PAREN)
|
|
||||||
|
|
||||||
function = state.GetFunction()
|
|
||||||
if not self._limited_doc_checks:
|
|
||||||
if (function.has_return and function.doc and
|
|
||||||
not is_immediately_called and
|
|
||||||
not function.doc.HasFlag('return') and
|
|
||||||
not function.doc.InheritsDocumentation() and
|
|
||||||
not function.doc.HasFlag('constructor')):
|
|
||||||
# Check for proper documentation of return value.
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_RETURN_DOCUMENTATION,
|
|
||||||
'Missing @return JsDoc in function with non-trivial return',
|
|
||||||
function.doc.end_token, position=Position.AtBeginning())
|
|
||||||
elif (not function.has_return and
|
|
||||||
not function.has_throw and
|
|
||||||
function.doc and
|
|
||||||
function.doc.HasFlag('return') and
|
|
||||||
not state.InInterfaceMethod()):
|
|
||||||
flag = function.doc.GetFlag('return')
|
|
||||||
valid_no_return_names = ['undefined', 'void', '*']
|
|
||||||
invalid_return = flag.jstype is None or not any(
|
|
||||||
sub_type.identifier in valid_no_return_names
|
|
||||||
for sub_type in flag.jstype.IterTypeGroup())
|
|
||||||
|
|
||||||
if invalid_return:
|
|
||||||
self._HandleError(
|
|
||||||
errors.UNNECESSARY_RETURN_DOCUMENTATION,
|
|
||||||
'Found @return JsDoc on function that returns nothing',
|
|
||||||
flag.flag_token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
# b/4073735. Method in object literal definition of prototype can
|
|
||||||
# safely reference 'this'.
|
|
||||||
prototype_object_literal = False
|
|
||||||
block_start = None
|
|
||||||
previous_code = None
|
|
||||||
previous_previous_code = None
|
|
||||||
|
|
||||||
# Search for cases where prototype is defined as object literal.
|
|
||||||
# previous_previous_code
|
|
||||||
# | previous_code
|
|
||||||
# | | block_start
|
|
||||||
# | | |
|
|
||||||
# a.b.prototype = {
|
|
||||||
# c : function() {
|
|
||||||
# this.d = 1;
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
||||||
# If in object literal, find first token of block so to find previous
|
|
||||||
# tokens to check above condition.
|
|
||||||
if state.InObjectLiteral():
|
|
||||||
block_start = state.GetCurrentBlockStart()
|
|
||||||
|
|
||||||
# If an object literal then get previous token (code type). For above
|
|
||||||
# case it should be '='.
|
|
||||||
if block_start:
|
|
||||||
previous_code = tokenutil.SearchExcept(block_start,
|
|
||||||
Type.NON_CODE_TYPES,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
# If previous token to block is '=' then get its previous token.
|
|
||||||
if previous_code and previous_code.IsOperator('='):
|
|
||||||
previous_previous_code = tokenutil.SearchExcept(previous_code,
|
|
||||||
Type.NON_CODE_TYPES,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
# If variable/token before '=' ends with '.prototype' then its above
|
|
||||||
# case of prototype defined with object literal.
|
|
||||||
prototype_object_literal = (previous_previous_code and
|
|
||||||
previous_previous_code.string.endswith(
|
|
||||||
'.prototype'))
|
|
||||||
|
|
||||||
if (function.has_this and function.doc and
|
|
||||||
not function.doc.HasFlag('this') and
|
|
||||||
not function.is_constructor and
|
|
||||||
not function.is_interface and
|
|
||||||
'.prototype.' not in function.name and
|
|
||||||
not prototype_object_literal):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_JSDOC_TAG_THIS,
|
|
||||||
'Missing @this JsDoc in function referencing "this". ('
|
|
||||||
'this usually means you are trying to reference "this" in '
|
|
||||||
'a static function, or you have forgotten to mark a '
|
|
||||||
'constructor with @constructor)',
|
|
||||||
function.doc.end_token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
elif token.type == Type.IDENTIFIER:
|
|
||||||
if token.string == 'goog.inherits' and not state.InFunction():
|
|
||||||
if state.GetLastNonSpaceToken().line_number == token.line_number:
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_LINE,
|
|
||||||
'Missing newline between constructor and goog.inherits',
|
|
||||||
token,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
extra_space = state.GetLastNonSpaceToken().next
|
|
||||||
while extra_space != token:
|
|
||||||
if extra_space.type == Type.BLANK_LINE:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_LINE,
|
|
||||||
'Extra line between constructor and goog.inherits',
|
|
||||||
extra_space)
|
|
||||||
extra_space = extra_space.next
|
|
||||||
|
|
||||||
# TODO(robbyw): Test the last function was a constructor.
|
|
||||||
# TODO(robbyw): Test correct @extends and @implements documentation.
|
|
||||||
|
|
||||||
elif (token.string == 'goog.provide' and
|
|
||||||
not state.InFunction() and
|
|
||||||
namespaces_info is not None):
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
# Report extra goog.provide statement.
|
|
||||||
if not namespace or namespaces_info.IsExtraProvide(token):
|
|
||||||
if not namespace:
|
|
||||||
msg = 'Empty namespace in goog.provide'
|
|
||||||
else:
|
|
||||||
msg = 'Unnecessary goog.provide: ' + namespace
|
|
||||||
|
|
||||||
# Hint to user if this is a Test namespace.
|
|
||||||
if namespace.endswith('Test'):
|
|
||||||
msg += (' *Test namespaces must be mentioned in the '
|
|
||||||
'goog.setTestOnly() call')
|
|
||||||
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_GOOG_PROVIDE,
|
|
||||||
msg,
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
if namespaces_info.IsLastProvide(token):
|
|
||||||
# Report missing provide statements after the last existing provide.
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
if missing_provides:
|
|
||||||
self._ReportMissingProvides(
|
|
||||||
missing_provides,
|
|
||||||
tokenutil.GetLastTokenInSameLine(token).next,
|
|
||||||
False)
|
|
||||||
|
|
||||||
# If there are no require statements, missing requires should be
|
|
||||||
# reported after the last provide.
|
|
||||||
if not namespaces_info.GetRequiredNamespaces():
|
|
||||||
missing_requires, illegal_alias_statements = (
|
|
||||||
namespaces_info.GetMissingRequires())
|
|
||||||
if missing_requires:
|
|
||||||
self._ReportMissingRequires(
|
|
||||||
missing_requires,
|
|
||||||
tokenutil.GetLastTokenInSameLine(token).next,
|
|
||||||
True)
|
|
||||||
if illegal_alias_statements:
|
|
||||||
self._ReportIllegalAliasStatement(illegal_alias_statements)
|
|
||||||
|
|
||||||
elif (token.string == 'goog.require' and
|
|
||||||
not state.InFunction() and
|
|
||||||
namespaces_info is not None):
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
# If there are no provide statements, missing provides should be
|
|
||||||
# reported before the first require.
|
|
||||||
if (namespaces_info.IsFirstRequire(token) and
|
|
||||||
not namespaces_info.GetProvidedNamespaces()):
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
if missing_provides:
|
|
||||||
self._ReportMissingProvides(
|
|
||||||
missing_provides,
|
|
||||||
tokenutil.GetFirstTokenInSameLine(token),
|
|
||||||
True)
|
|
||||||
|
|
||||||
# Report extra goog.require statement.
|
|
||||||
if not namespace or namespaces_info.IsExtraRequire(token):
|
|
||||||
if not namespace:
|
|
||||||
msg = 'Empty namespace in goog.require'
|
|
||||||
else:
|
|
||||||
msg = 'Unnecessary goog.require: ' + namespace
|
|
||||||
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_GOOG_REQUIRE,
|
|
||||||
msg,
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
# Report missing goog.require statements.
|
|
||||||
if namespaces_info.IsLastRequire(token):
|
|
||||||
missing_requires, illegal_alias_statements = (
|
|
||||||
namespaces_info.GetMissingRequires())
|
|
||||||
if missing_requires:
|
|
||||||
self._ReportMissingRequires(
|
|
||||||
missing_requires,
|
|
||||||
tokenutil.GetLastTokenInSameLine(token).next,
|
|
||||||
False)
|
|
||||||
if illegal_alias_statements:
|
|
||||||
self._ReportIllegalAliasStatement(illegal_alias_statements)
|
|
||||||
|
|
||||||
elif token.type == Type.OPERATOR:
|
|
||||||
last_in_line = token.IsLastInLine()
|
|
||||||
# If the token is unary and appears to be used in a unary context
|
|
||||||
# it's ok. Otherwise, if it's at the end of the line or immediately
|
|
||||||
# before a comment, it's ok.
|
|
||||||
# Don't report an error before a start bracket - it will be reported
|
|
||||||
# by that token's space checks.
|
|
||||||
if (not token.metadata.IsUnaryOperator() and not last_in_line
|
|
||||||
and not token.next.IsComment()
|
|
||||||
and not token.next.IsOperator(',')
|
|
||||||
and not tokenutil.IsDot(token)
|
|
||||||
and token.next.type not in (Type.WHITESPACE, Type.END_PAREN,
|
|
||||||
Type.END_BRACKET, Type.SEMICOLON,
|
|
||||||
Type.START_BRACKET)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SPACE,
|
|
||||||
'Missing space after "%s"' % token.string,
|
|
||||||
token,
|
|
||||||
position=Position.AtEnd(token.string))
|
|
||||||
elif token.type == Type.WHITESPACE:
|
|
||||||
first_in_line = token.IsFirstInLine()
|
|
||||||
last_in_line = token.IsLastInLine()
|
|
||||||
# Check whitespace length if it's not the first token of the line and
|
|
||||||
# if it's not immediately before a comment.
|
|
||||||
if not last_in_line and not first_in_line and not token.next.IsComment():
|
|
||||||
# Ensure there is no space after opening parentheses.
|
|
||||||
if (token.previous.type in (Type.START_PAREN, Type.START_BRACKET,
|
|
||||||
Type.FUNCTION_NAME)
|
|
||||||
or token.next.type == Type.START_PARAMETERS):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE,
|
|
||||||
'Extra space after "%s"' % token.previous.string,
|
|
||||||
token,
|
|
||||||
position=Position.All(token.string))
|
|
||||||
elif token.type == Type.SEMICOLON:
|
|
||||||
previous_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES,
|
|
||||||
reverse=True)
|
|
||||||
if not previous_token:
|
|
||||||
self._HandleError(
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
'Semicolon without any statement',
|
|
||||||
token,
|
|
||||||
position=Position.AtEnd(token.string))
|
|
||||||
elif (previous_token.type == Type.KEYWORD and
|
|
||||||
previous_token.string not in ['break', 'continue', 'return']):
|
|
||||||
self._HandleError(
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
('Semicolon after \'%s\' without any statement.'
|
|
||||||
' Looks like an error.' % previous_token.string),
|
|
||||||
token,
|
|
||||||
position=Position.AtEnd(token.string))
|
|
||||||
|
|
||||||
def _CheckUnusedLocalVariables(self, token, state):
|
|
||||||
"""Checks for unused local variables in function blocks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to check.
|
|
||||||
state: The state tracker.
|
|
||||||
"""
|
|
||||||
# We don't use state.InFunction because that disregards scope functions.
|
|
||||||
in_function = state.FunctionDepth() > 0
|
|
||||||
if token.type == Type.SIMPLE_LVALUE or token.type == Type.IDENTIFIER:
|
|
||||||
if in_function:
|
|
||||||
identifier = token.string
|
|
||||||
# Check whether the previous token was var.
|
|
||||||
previous_code_token = tokenutil.CustomSearch(
|
|
||||||
token,
|
|
||||||
lambda t: t.type not in Type.NON_CODE_TYPES,
|
|
||||||
reverse=True)
|
|
||||||
if previous_code_token and previous_code_token.IsKeyword('var'):
|
|
||||||
# Add local variable declaration to the top of the unused locals
|
|
||||||
# stack.
|
|
||||||
self._unused_local_variables_by_scope[-1][identifier] = token
|
|
||||||
elif token.type == Type.IDENTIFIER:
|
|
||||||
# This covers most cases where the variable is used as an identifier.
|
|
||||||
self._MarkLocalVariableUsed(token.string)
|
|
||||||
elif token.type == Type.SIMPLE_LVALUE and '.' in identifier:
|
|
||||||
# This covers cases where a value is assigned to a property of the
|
|
||||||
# variable.
|
|
||||||
self._MarkLocalVariableUsed(token.string)
|
|
||||||
elif token.type == Type.START_BLOCK:
|
|
||||||
if in_function and state.IsFunctionOpen():
|
|
||||||
# Push a new map onto the stack
|
|
||||||
self._unused_local_variables_by_scope.append({})
|
|
||||||
elif token.type == Type.END_BLOCK:
|
|
||||||
if state.IsFunctionClose():
|
|
||||||
# Pop the stack and report any remaining locals as unused.
|
|
||||||
unused_local_variables = self._unused_local_variables_by_scope.pop()
|
|
||||||
for unused_token in unused_local_variables.values():
|
|
||||||
self._HandleError(
|
|
||||||
errors.UNUSED_LOCAL_VARIABLE,
|
|
||||||
'Unused local variable: %s.' % unused_token.string,
|
|
||||||
unused_token)
|
|
||||||
elif token.type == Type.DOC_FLAG:
|
|
||||||
# Flags that use aliased symbols should be counted.
|
|
||||||
flag = token.attached_object
|
|
||||||
js_type = flag and flag.jstype
|
|
||||||
if flag and flag.flag_type in state.GetDocFlag().HAS_TYPE and js_type:
|
|
||||||
self._MarkAliasUsed(js_type)
|
|
||||||
|
|
||||||
def _MarkAliasUsed(self, js_type):
|
|
||||||
"""Marks aliases in a type as used.
|
|
||||||
|
|
||||||
Recursively iterates over all subtypes in a jsdoc type annotation and
|
|
||||||
tracks usage of aliased symbols (which may be local variables).
|
|
||||||
Marks the local variable as used in the scope nearest to the current
|
|
||||||
scope that matches the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
js_type: The jsdoc type, a typeannotation.TypeAnnotation object.
|
|
||||||
"""
|
|
||||||
if js_type.alias:
|
|
||||||
self._MarkLocalVariableUsed(js_type.identifier)
|
|
||||||
for sub_type in js_type.IterTypes():
|
|
||||||
self._MarkAliasUsed(sub_type)
|
|
||||||
|
|
||||||
def _MarkLocalVariableUsed(self, identifier):
|
|
||||||
"""Marks the local variable as used in the relevant scope.
|
|
||||||
|
|
||||||
Marks the local variable in the scope nearest to the current scope that
|
|
||||||
matches the given identifier as used.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: The identifier representing the potential usage of a local
|
|
||||||
variable.
|
|
||||||
"""
|
|
||||||
identifier = identifier.split('.', 1)[0]
|
|
||||||
# Find the first instance of the identifier in the stack of function scopes
|
|
||||||
# and mark it used.
|
|
||||||
for unused_local_variables in reversed(
|
|
||||||
self._unused_local_variables_by_scope):
|
|
||||||
if identifier in unused_local_variables:
|
|
||||||
del unused_local_variables[identifier]
|
|
||||||
break
|
|
||||||
|
|
||||||
def _ReportMissingProvides(self, missing_provides, token, need_blank_line):
|
|
||||||
"""Reports missing provide statements to the error handler.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
missing_provides: A dictionary of string(key) and integer(value) where
|
|
||||||
each string(key) is a namespace that should be provided, but is not
|
|
||||||
and integer(value) is first line number where it's required.
|
|
||||||
token: The token where the error was detected (also where the new provides
|
|
||||||
will be inserted.
|
|
||||||
need_blank_line: Whether a blank line needs to be inserted after the new
|
|
||||||
provides are inserted. May be True, False, or None, where None
|
|
||||||
indicates that the insert location is unknown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
missing_provides_msg = 'Missing the following goog.provide statements:\n'
|
|
||||||
missing_provides_msg += '\n'.join(['goog.provide(\'%s\');' % x for x in
|
|
||||||
sorted(missing_provides)])
|
|
||||||
missing_provides_msg += '\n'
|
|
||||||
|
|
||||||
missing_provides_msg += '\nFirst line where provided: \n'
|
|
||||||
missing_provides_msg += '\n'.join(
|
|
||||||
[' %s : line %d' % (x, missing_provides[x]) for x in
|
|
||||||
sorted(missing_provides)])
|
|
||||||
missing_provides_msg += '\n'
|
|
||||||
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_GOOG_PROVIDE,
|
|
||||||
missing_provides_msg,
|
|
||||||
token, position=Position.AtBeginning(),
|
|
||||||
fix_data=(missing_provides.keys(), need_blank_line))
|
|
||||||
|
|
||||||
def _ReportMissingRequires(self, missing_requires, token, need_blank_line):
|
|
||||||
"""Reports missing require statements to the error handler.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
missing_requires: A dictionary of string(key) and integer(value) where
|
|
||||||
each string(key) is a namespace that should be required, but is not
|
|
||||||
and integer(value) is first line number where it's required.
|
|
||||||
token: The token where the error was detected (also where the new requires
|
|
||||||
will be inserted.
|
|
||||||
need_blank_line: Whether a blank line needs to be inserted before the new
|
|
||||||
requires are inserted. May be True, False, or None, where None
|
|
||||||
indicates that the insert location is unknown.
|
|
||||||
"""
|
|
||||||
|
|
||||||
missing_requires_msg = 'Missing the following goog.require statements:\n'
|
|
||||||
missing_requires_msg += '\n'.join(['goog.require(\'%s\');' % x for x in
|
|
||||||
sorted(missing_requires)])
|
|
||||||
missing_requires_msg += '\n'
|
|
||||||
|
|
||||||
missing_requires_msg += '\nFirst line where required: \n'
|
|
||||||
missing_requires_msg += '\n'.join(
|
|
||||||
[' %s : line %d' % (x, missing_requires[x]) for x in
|
|
||||||
sorted(missing_requires)])
|
|
||||||
missing_requires_msg += '\n'
|
|
||||||
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_GOOG_REQUIRE,
|
|
||||||
missing_requires_msg,
|
|
||||||
token, position=Position.AtBeginning(),
|
|
||||||
fix_data=(missing_requires.keys(), need_blank_line))
|
|
||||||
|
|
||||||
def _ReportIllegalAliasStatement(self, illegal_alias_statements):
|
|
||||||
"""Reports alias statements that would need a goog.require."""
|
|
||||||
for namespace, token in illegal_alias_statements.iteritems():
|
|
||||||
self._HandleError(
|
|
||||||
errors.ALIAS_STMT_NEEDS_GOOG_REQUIRE,
|
|
||||||
'The alias definition would need the namespace \'%s\' which is not '
|
|
||||||
'required through any other symbol.' % namespace,
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
def Finalize(self, state):
|
|
||||||
"""Perform all checks that need to occur after all lines are processed."""
|
|
||||||
# Call the base class's Finalize function.
|
|
||||||
super(JavaScriptLintRules, self).Finalize(state)
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS):
|
|
||||||
# Report an error for any declared private member that was never used.
|
|
||||||
unused_private_members = (self._declared_private_members -
|
|
||||||
self._used_private_members)
|
|
||||||
|
|
||||||
for variable in unused_private_members:
|
|
||||||
token = self._declared_private_member_tokens[variable]
|
|
||||||
self._HandleError(errors.UNUSED_PRIVATE_MEMBER,
|
|
||||||
'Unused private member: %s.' % token.string,
|
|
||||||
token)
|
|
||||||
|
|
||||||
# Clear state to prepare for the next file.
|
|
||||||
self._declared_private_member_tokens = {}
|
|
||||||
self._declared_private_members = set()
|
|
||||||
self._used_private_members = set()
|
|
||||||
|
|
||||||
namespaces_info = self._namespaces_info
|
|
||||||
if namespaces_info is not None:
|
|
||||||
# If there are no provide or require statements, missing provides and
|
|
||||||
# requires should be reported on line 1.
|
|
||||||
if (not namespaces_info.GetProvidedNamespaces() and
|
|
||||||
not namespaces_info.GetRequiredNamespaces()):
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
if missing_provides:
|
|
||||||
self._ReportMissingProvides(
|
|
||||||
missing_provides, state.GetFirstToken(), None)
|
|
||||||
|
|
||||||
missing_requires, illegal_alias = namespaces_info.GetMissingRequires()
|
|
||||||
if missing_requires:
|
|
||||||
self._ReportMissingRequires(
|
|
||||||
missing_requires, state.GetFirstToken(), None)
|
|
||||||
if illegal_alias:
|
|
||||||
self._ReportIllegalAliasStatement(illegal_alias)
|
|
||||||
|
|
||||||
self._CheckSortedRequiresProvides(state.GetFirstToken())
|
|
||||||
|
|
||||||
def _CheckSortedRequiresProvides(self, token):
|
|
||||||
"""Checks that all goog.require and goog.provide statements are sorted.
|
|
||||||
|
|
||||||
Note that this method needs to be run after missing statements are added to
|
|
||||||
preserve alphabetical order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
"""
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
first_provide_token = sorter.CheckProvides(token)
|
|
||||||
if first_provide_token:
|
|
||||||
new_order = sorter.GetFixedProvideString(first_provide_token)
|
|
||||||
self._HandleError(
|
|
||||||
errors.GOOG_PROVIDES_NOT_ALPHABETIZED,
|
|
||||||
'goog.provide classes must be alphabetized. The correct code is:\n' +
|
|
||||||
new_order,
|
|
||||||
first_provide_token,
|
|
||||||
position=Position.AtBeginning(),
|
|
||||||
fix_data=first_provide_token)
|
|
||||||
|
|
||||||
first_require_token = sorter.CheckRequires(token)
|
|
||||||
if first_require_token:
|
|
||||||
new_order = sorter.GetFixedRequireString(first_require_token)
|
|
||||||
self._HandleError(
|
|
||||||
errors.GOOG_REQUIRES_NOT_ALPHABETIZED,
|
|
||||||
'goog.require classes must be alphabetized. The correct code is:\n' +
|
|
||||||
new_order,
|
|
||||||
first_require_token,
|
|
||||||
position=Position.AtBeginning(),
|
|
||||||
fix_data=first_require_token)
|
|
||||||
|
|
||||||
def GetLongLineExceptions(self):
|
|
||||||
"""Gets a list of regexps for lines which can be longer than the limit.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of regexps, used as matches (rather than searches).
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
re.compile(r'(var .+\s*=\s*)?goog\.require\(.+\);?\s*$'),
|
|
||||||
re.compile(r'goog\.(provide|module|setTestOnly)\(.+\);?\s*$'),
|
|
||||||
re.compile(r'[\s/*]*@visibility\s*{.*}[\s*/]*$'),
|
|
||||||
]
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Parser for JavaScript files."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import statetracker
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class JsDocFlag(statetracker.DocFlag):
|
|
||||||
"""Javascript doc flag object.
|
|
||||||
|
|
||||||
Attribute:
|
|
||||||
flag_type: param, return, define, type, etc.
|
|
||||||
flag_token: The flag token.
|
|
||||||
type_start_token: The first token specifying the flag JS type,
|
|
||||||
including braces.
|
|
||||||
type_end_token: The last token specifying the flag JS type,
|
|
||||||
including braces.
|
|
||||||
type: The type spec string.
|
|
||||||
jstype: The type spec, a TypeAnnotation instance.
|
|
||||||
name_token: The token specifying the flag name.
|
|
||||||
name: The flag name
|
|
||||||
description_start_token: The first token in the description.
|
|
||||||
description_end_token: The end token in the description.
|
|
||||||
description: The description.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Please keep these lists alphabetized.
|
|
||||||
|
|
||||||
# Some projects use the following extensions to JsDoc.
|
|
||||||
# TODO(robbyw): determine which of these, if any, should be illegal.
|
|
||||||
EXTENDED_DOC = frozenset([
|
|
||||||
'class', 'code', 'desc', 'final', 'hidden', 'inheritDoc', 'link',
|
|
||||||
'meaning', 'provideGoog', 'throws'])
|
|
||||||
|
|
||||||
LEGAL_DOC = EXTENDED_DOC | statetracker.DocFlag.LEGAL_DOC
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptStateTracker(statetracker.StateTracker):
|
|
||||||
"""JavaScript state tracker.
|
|
||||||
|
|
||||||
Inherits from the core EcmaScript StateTracker adding extra state tracking
|
|
||||||
functionality needed for JavaScript.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initializes a JavaScript token stream state tracker."""
|
|
||||||
statetracker.StateTracker.__init__(self, JsDocFlag)
|
|
||||||
|
|
||||||
def Reset(self):
|
|
||||||
self._scope_depth = 0
|
|
||||||
self._block_stack = []
|
|
||||||
super(JavaScriptStateTracker, self).Reset()
|
|
||||||
|
|
||||||
def InTopLevel(self):
|
|
||||||
"""Compute whether we are at the top level in the class.
|
|
||||||
|
|
||||||
This function call is language specific. In some languages like
|
|
||||||
JavaScript, a function is top level if it is not inside any parenthesis.
|
|
||||||
In languages such as ActionScript, a function is top level if it is directly
|
|
||||||
within a class.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether we are at the top level in the class.
|
|
||||||
"""
|
|
||||||
return self._scope_depth == self.ParenthesesDepth()
|
|
||||||
|
|
||||||
def InFunction(self):
|
|
||||||
"""Returns true if the current token is within a function.
|
|
||||||
|
|
||||||
This js-specific override ignores goog.scope functions.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the current token is within a function.
|
|
||||||
"""
|
|
||||||
return self._scope_depth != self.FunctionDepth()
|
|
||||||
|
|
||||||
def InNonScopeBlock(self):
|
|
||||||
"""Compute whether we are nested within a non-goog.scope block.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the token is not enclosed in a block that does not originate from
|
|
||||||
a goog.scope statement. False otherwise.
|
|
||||||
"""
|
|
||||||
return self._scope_depth != self.BlockDepth()
|
|
||||||
|
|
||||||
def GetBlockType(self, token):
|
|
||||||
"""Determine the block type given a START_BLOCK token.
|
|
||||||
|
|
||||||
Code blocks come after parameters, keywords like else, and closing parens.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token. Can be assumed to be type START_BLOCK
|
|
||||||
Returns:
|
|
||||||
Code block type for current token.
|
|
||||||
"""
|
|
||||||
last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, reverse=True)
|
|
||||||
if last_code.type in (Type.END_PARAMETERS, Type.END_PAREN,
|
|
||||||
Type.KEYWORD) and not last_code.IsKeyword('return'):
|
|
||||||
return self.CODE
|
|
||||||
else:
|
|
||||||
return self.OBJECT_LITERAL
|
|
||||||
|
|
||||||
def GetCurrentBlockStart(self):
|
|
||||||
"""Gets the start token of current block.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Starting token of current block. None if not in block.
|
|
||||||
"""
|
|
||||||
if self._block_stack:
|
|
||||||
return self._block_stack[-1]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def HandleToken(self, token, last_non_space_token):
|
|
||||||
"""Handles the given token and updates state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to handle.
|
|
||||||
last_non_space_token: The last non space token encountered
|
|
||||||
"""
|
|
||||||
if token.type == Type.START_BLOCK:
|
|
||||||
self._block_stack.append(token)
|
|
||||||
if token.type == Type.IDENTIFIER and token.string == 'goog.scope':
|
|
||||||
self._scope_depth += 1
|
|
||||||
if token.type == Type.END_BLOCK:
|
|
||||||
start_token = self._block_stack.pop()
|
|
||||||
if tokenutil.GoogScopeOrNoneFromStartBlock(start_token):
|
|
||||||
self._scope_depth -= 1
|
|
||||||
super(JavaScriptStateTracker, self).HandleToken(token,
|
|
||||||
last_non_space_token)
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the javascriptstatetracker module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
|
|
||||||
_FUNCTION_SCRIPT = """\
|
|
||||||
var a = 3;
|
|
||||||
|
|
||||||
function foo(aaa, bbb, ccc) {
|
|
||||||
var b = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSDoc comment.
|
|
||||||
*/
|
|
||||||
var bar = function(ddd, eee, fff) {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that nested functions get their proper parameters recorded.
|
|
||||||
*/
|
|
||||||
var baz = function(ggg, hhh, iii) {
|
|
||||||
var qux = function(jjj, kkk, lll) {
|
|
||||||
};
|
|
||||||
// make sure that entering a new block does not change baz' parameters.
|
|
||||||
{};
|
|
||||||
};
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testFunctionParse(self):
|
|
||||||
functions, _ = testutil.ParseFunctionsAndComments(_FUNCTION_SCRIPT)
|
|
||||||
self.assertEquals(4, len(functions))
|
|
||||||
|
|
||||||
# First function
|
|
||||||
function = functions[0]
|
|
||||||
self.assertEquals(['aaa', 'bbb', 'ccc'], function.parameters)
|
|
||||||
|
|
||||||
start_token = function.start_token
|
|
||||||
end_token = function.end_token
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION,
|
|
||||||
function.start_token.type)
|
|
||||||
|
|
||||||
self.assertEquals('function', start_token.string)
|
|
||||||
self.assertEquals(3, start_token.line_number)
|
|
||||||
self.assertEquals(0, start_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('}', end_token.string)
|
|
||||||
self.assertEquals(5, end_token.line_number)
|
|
||||||
self.assertEquals(0, end_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('foo', function.name)
|
|
||||||
|
|
||||||
self.assertIsNone(function.doc)
|
|
||||||
|
|
||||||
# Second function
|
|
||||||
function = functions[1]
|
|
||||||
self.assertEquals(['ddd', 'eee', 'fff'], function.parameters)
|
|
||||||
|
|
||||||
start_token = function.start_token
|
|
||||||
end_token = function.end_token
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION,
|
|
||||||
function.start_token.type)
|
|
||||||
|
|
||||||
self.assertEquals('function', start_token.string)
|
|
||||||
self.assertEquals(11, start_token.line_number)
|
|
||||||
self.assertEquals(10, start_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('}', end_token.string)
|
|
||||||
self.assertEquals(13, end_token.line_number)
|
|
||||||
self.assertEquals(0, end_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('bar', function.name)
|
|
||||||
|
|
||||||
self.assertIsNotNone(function.doc)
|
|
||||||
|
|
||||||
# Check function JSDoc
|
|
||||||
doc = function.doc
|
|
||||||
doc_tokens = tokenutil.GetTokenRange(doc.start_token, doc.end_token)
|
|
||||||
|
|
||||||
comment_type = javascripttokens.JavaScriptTokenType.COMMENT
|
|
||||||
comment_tokens = filter(lambda t: t.type is comment_type, doc_tokens)
|
|
||||||
|
|
||||||
self.assertEquals('JSDoc comment.',
|
|
||||||
tokenutil.TokensToString(comment_tokens).strip())
|
|
||||||
|
|
||||||
# Third function
|
|
||||||
function = functions[2]
|
|
||||||
self.assertEquals(['ggg', 'hhh', 'iii'], function.parameters)
|
|
||||||
|
|
||||||
start_token = function.start_token
|
|
||||||
end_token = function.end_token
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION,
|
|
||||||
function.start_token.type)
|
|
||||||
|
|
||||||
self.assertEquals('function', start_token.string)
|
|
||||||
self.assertEquals(19, start_token.line_number)
|
|
||||||
self.assertEquals(10, start_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('}', end_token.string)
|
|
||||||
self.assertEquals(24, end_token.line_number)
|
|
||||||
self.assertEquals(0, end_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('baz', function.name)
|
|
||||||
self.assertIsNotNone(function.doc)
|
|
||||||
|
|
||||||
# Fourth function (inside third function)
|
|
||||||
function = functions[3]
|
|
||||||
self.assertEquals(['jjj', 'kkk', 'lll'], function.parameters)
|
|
||||||
|
|
||||||
start_token = function.start_token
|
|
||||||
end_token = function.end_token
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION,
|
|
||||||
function.start_token.type)
|
|
||||||
|
|
||||||
self.assertEquals('function', start_token.string)
|
|
||||||
self.assertEquals(20, start_token.line_number)
|
|
||||||
self.assertEquals(12, start_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('}', end_token.string)
|
|
||||||
self.assertEquals(21, end_token.line_number)
|
|
||||||
self.assertEquals(2, end_token.start_index)
|
|
||||||
|
|
||||||
self.assertEquals('qux', function.name)
|
|
||||||
self.assertIsNone(function.doc)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CommentTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testGetDescription(self):
|
|
||||||
comment = self._ParseComment("""
|
|
||||||
/**
|
|
||||||
* Comment targeting goog.foo.
|
|
||||||
*
|
|
||||||
* This is the second line.
|
|
||||||
* @param {number} foo The count of foo.
|
|
||||||
*/
|
|
||||||
target;""")
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
'Comment targeting goog.foo.\n\nThis is the second line.',
|
|
||||||
comment.description)
|
|
||||||
|
|
||||||
def testCommentGetTarget(self):
|
|
||||||
self.assertCommentTarget('goog.foo', """
|
|
||||||
/**
|
|
||||||
* Comment targeting goog.foo.
|
|
||||||
*/
|
|
||||||
goog.foo = 6;
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('bar', """
|
|
||||||
/**
|
|
||||||
* Comment targeting bar.
|
|
||||||
*/
|
|
||||||
var bar = "Karate!";
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('doThing', """
|
|
||||||
/**
|
|
||||||
* Comment targeting doThing.
|
|
||||||
*/
|
|
||||||
function doThing() {};
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('this.targetProperty', """
|
|
||||||
goog.bar.Baz = function() {
|
|
||||||
/**
|
|
||||||
* Comment targeting targetProperty.
|
|
||||||
*/
|
|
||||||
this.targetProperty = 3;
|
|
||||||
};
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('goog.bar.prop', """
|
|
||||||
/**
|
|
||||||
* Comment targeting goog.bar.prop.
|
|
||||||
*/
|
|
||||||
goog.bar.prop;
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('goog.aaa.bbb', """
|
|
||||||
/**
|
|
||||||
* Comment targeting goog.aaa.bbb.
|
|
||||||
*/
|
|
||||||
(goog.aaa.bbb)
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('theTarget', """
|
|
||||||
/**
|
|
||||||
* Comment targeting symbol preceded by newlines, whitespace,
|
|
||||||
* and parens -- things we ignore.
|
|
||||||
*/
|
|
||||||
(theTarget)
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget(None, """
|
|
||||||
/**
|
|
||||||
* @fileoverview File overview.
|
|
||||||
*/
|
|
||||||
(notATarget)
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget(None, """
|
|
||||||
/**
|
|
||||||
* Comment that doesn't find a target.
|
|
||||||
*/
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('theTarget.is.split.across.lines', """
|
|
||||||
/**
|
|
||||||
* Comment that addresses a symbol split across lines.
|
|
||||||
*/
|
|
||||||
(theTarget.is.split
|
|
||||||
.across.lines)
|
|
||||||
""")
|
|
||||||
|
|
||||||
self.assertCommentTarget('theTarget.is.split.across.lines', """
|
|
||||||
/**
|
|
||||||
* Comment that addresses a symbol split across lines.
|
|
||||||
*/
|
|
||||||
(theTarget.is.split.
|
|
||||||
across.lines)
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _ParseComment(self, script):
|
|
||||||
"""Parse a script that contains one comment and return it."""
|
|
||||||
_, comments = testutil.ParseFunctionsAndComments(script)
|
|
||||||
self.assertEquals(1, len(comments))
|
|
||||||
return comments[0]
|
|
||||||
|
|
||||||
def assertCommentTarget(self, target, script):
|
|
||||||
comment = self._ParseComment(script)
|
|
||||||
self.assertEquals(target, comment.GetTargetIdentifier())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Regular expression based JavaScript parsing classes."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import re
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter.common import matcher
|
|
||||||
from closure_linter.common import tokenizer
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
Matcher = matcher.Matcher
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptModes(object):
|
|
||||||
"""Enumeration of the different matcher modes used for JavaScript."""
|
|
||||||
TEXT_MODE = 'text'
|
|
||||||
SINGLE_QUOTE_STRING_MODE = 'single_quote_string'
|
|
||||||
DOUBLE_QUOTE_STRING_MODE = 'double_quote_string'
|
|
||||||
BLOCK_COMMENT_MODE = 'block_comment'
|
|
||||||
DOC_COMMENT_MODE = 'doc_comment'
|
|
||||||
DOC_COMMENT_LEX_SPACES_MODE = 'doc_comment_spaces'
|
|
||||||
LINE_COMMENT_MODE = 'line_comment'
|
|
||||||
PARAMETER_MODE = 'parameter'
|
|
||||||
FUNCTION_MODE = 'function'
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptTokenizer(tokenizer.Tokenizer):
|
|
||||||
"""JavaScript tokenizer.
|
|
||||||
|
|
||||||
Convert JavaScript code in to an array of tokens.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Useful patterns for JavaScript parsing.
|
|
||||||
IDENTIFIER_CHAR = r'A-Za-z0-9_$'
|
|
||||||
|
|
||||||
# Number patterns based on:
|
|
||||||
# http://www.mozilla.org/js/language/js20-2000-07/formal/lexer-grammar.html
|
|
||||||
MANTISSA = r"""
|
|
||||||
(\d+(?!\.)) | # Matches '10'
|
|
||||||
(\d+\.(?!\d)) | # Matches '10.'
|
|
||||||
(\d*\.\d+) # Matches '.5' or '10.5'
|
|
||||||
"""
|
|
||||||
DECIMAL_LITERAL = r'(%s)([eE][-+]?\d+)?' % MANTISSA
|
|
||||||
HEX_LITERAL = r'0[xX][0-9a-fA-F]+'
|
|
||||||
NUMBER = re.compile(r"""
|
|
||||||
((%s)|(%s))
|
|
||||||
""" % (HEX_LITERAL, DECIMAL_LITERAL), re.VERBOSE)
|
|
||||||
|
|
||||||
# Strings come in three parts - first we match the start of the string, then
|
|
||||||
# the contents, then the end. The contents consist of any character except a
|
|
||||||
# backslash or end of string, or a backslash followed by any character, or a
|
|
||||||
# backslash followed by end of line to support correct parsing of multi-line
|
|
||||||
# strings.
|
|
||||||
SINGLE_QUOTE = re.compile(r"'")
|
|
||||||
SINGLE_QUOTE_TEXT = re.compile(r"([^'\\]|\\(.|$))+")
|
|
||||||
DOUBLE_QUOTE = re.compile(r'"')
|
|
||||||
DOUBLE_QUOTE_TEXT = re.compile(r'([^"\\]|\\(.|$))+')
|
|
||||||
|
|
||||||
START_SINGLE_LINE_COMMENT = re.compile(r'//')
|
|
||||||
END_OF_LINE_SINGLE_LINE_COMMENT = re.compile(r'//$')
|
|
||||||
|
|
||||||
START_DOC_COMMENT = re.compile(r'/\*\*')
|
|
||||||
START_BLOCK_COMMENT = re.compile(r'/\*')
|
|
||||||
END_BLOCK_COMMENT = re.compile(r'\*/')
|
|
||||||
BLOCK_COMMENT_TEXT = re.compile(r'([^*]|\*(?!/))+')
|
|
||||||
|
|
||||||
# Comment text is anything that we are not going to parse into another special
|
|
||||||
# token like (inline) flags or end comments. Complicated regex to match
|
|
||||||
# most normal characters, and '*', '{', '}', and '@' when we are sure that
|
|
||||||
# it is safe. Expression [^*{\s]@ must come first, or the other options will
|
|
||||||
# match everything before @, and we won't match @'s that aren't part of flags
|
|
||||||
# like in email addresses in the @author tag.
|
|
||||||
DOC_COMMENT_TEXT = re.compile(r'([^*{}\s]@|[^*{}@]|\*(?!/))+')
|
|
||||||
DOC_COMMENT_NO_SPACES_TEXT = re.compile(r'([^*{}\s]@|[^*{}@\s]|\*(?!/))+')
|
|
||||||
# Match anything that is allowed in a type definition, except for tokens
|
|
||||||
# needed to parse it (and the lookahead assertion for "*/").
|
|
||||||
DOC_COMMENT_TYPE_TEXT = re.compile(r'([^*|!?=<>(){}:,\s]|\*(?!/))+')
|
|
||||||
|
|
||||||
# Match the prefix ' * ' that starts every line of jsdoc. Want to include
|
|
||||||
# spaces after the '*', but nothing else that occurs after a '*', and don't
|
|
||||||
# want to match the '*' in '*/'.
|
|
||||||
DOC_PREFIX = re.compile(r'\s*\*(\s+|(?!/))')
|
|
||||||
|
|
||||||
START_BLOCK = re.compile('{')
|
|
||||||
END_BLOCK = re.compile('}')
|
|
||||||
|
|
||||||
REGEX_CHARACTER_CLASS = r"""
|
|
||||||
\[ # Opening bracket
|
|
||||||
([^\]\\]|\\.)* # Anything but a ] or \,
|
|
||||||
# or a backslash followed by anything
|
|
||||||
\] # Closing bracket
|
|
||||||
"""
|
|
||||||
# We ensure the regex is followed by one of the above tokens to avoid
|
|
||||||
# incorrectly parsing something like x / y / z as x REGEX(/ y /) z
|
|
||||||
POST_REGEX_LIST = [
|
|
||||||
';', ',', r'\.', r'\)', r'\]', '$', r'\/\/', r'\/\*', ':', '}']
|
|
||||||
|
|
||||||
REGEX = re.compile(r"""
|
|
||||||
/ # opening slash
|
|
||||||
(?!\*) # not the start of a comment
|
|
||||||
(\\.|[^\[\/\\]|(%s))* # a backslash followed by anything,
|
|
||||||
# or anything but a / or [ or \,
|
|
||||||
# or a character class
|
|
||||||
/ # closing slash
|
|
||||||
[gimsx]* # optional modifiers
|
|
||||||
(?=\s*(%s))
|
|
||||||
""" % (REGEX_CHARACTER_CLASS, '|'.join(POST_REGEX_LIST)),
|
|
||||||
re.VERBOSE)
|
|
||||||
|
|
||||||
ANYTHING = re.compile(r'.*')
|
|
||||||
PARAMETERS = re.compile(r'[^\)]+')
|
|
||||||
CLOSING_PAREN_WITH_SPACE = re.compile(r'\)\s*')
|
|
||||||
|
|
||||||
FUNCTION_DECLARATION = re.compile(r'\bfunction\b')
|
|
||||||
|
|
||||||
OPENING_PAREN = re.compile(r'\(')
|
|
||||||
CLOSING_PAREN = re.compile(r'\)')
|
|
||||||
|
|
||||||
OPENING_BRACKET = re.compile(r'\[')
|
|
||||||
CLOSING_BRACKET = re.compile(r'\]')
|
|
||||||
|
|
||||||
# We omit these JS keywords from the list:
|
|
||||||
# function - covered by FUNCTION_DECLARATION.
|
|
||||||
# delete, in, instanceof, new, typeof - included as operators.
|
|
||||||
# this - included in identifiers.
|
|
||||||
# null, undefined - not included, should go in some "special constant" list.
|
|
||||||
KEYWORD_LIST = [
|
|
||||||
'break',
|
|
||||||
'case',
|
|
||||||
'catch',
|
|
||||||
'continue',
|
|
||||||
'default',
|
|
||||||
'do',
|
|
||||||
'else',
|
|
||||||
'finally',
|
|
||||||
'for',
|
|
||||||
'if',
|
|
||||||
'return',
|
|
||||||
'switch',
|
|
||||||
'throw',
|
|
||||||
'try',
|
|
||||||
'var',
|
|
||||||
'while',
|
|
||||||
'with',
|
|
||||||
]
|
|
||||||
|
|
||||||
# List of regular expressions to match as operators. Some notes: for our
|
|
||||||
# purposes, the comma behaves similarly enough to a normal operator that we
|
|
||||||
# include it here. r'\bin\b' actually matches 'in' surrounded by boundary
|
|
||||||
# characters - this may not match some very esoteric uses of the in operator.
|
|
||||||
# Operators that are subsets of larger operators must come later in this list
|
|
||||||
# for proper matching, e.g., '>>' must come AFTER '>>>'.
|
|
||||||
OPERATOR_LIST = [
|
|
||||||
',',
|
|
||||||
r'\+\+',
|
|
||||||
'===',
|
|
||||||
'!==',
|
|
||||||
'>>>=',
|
|
||||||
'>>>',
|
|
||||||
'==',
|
|
||||||
'>=',
|
|
||||||
'<=',
|
|
||||||
'!=',
|
|
||||||
'<<=',
|
|
||||||
'>>=',
|
|
||||||
'<<',
|
|
||||||
'>>',
|
|
||||||
'=>',
|
|
||||||
'>',
|
|
||||||
'<',
|
|
||||||
r'\+=',
|
|
||||||
r'\+',
|
|
||||||
'--',
|
|
||||||
r'\^=',
|
|
||||||
'-=',
|
|
||||||
'-',
|
|
||||||
'/=',
|
|
||||||
'/',
|
|
||||||
r'\*=',
|
|
||||||
r'\*',
|
|
||||||
'%=',
|
|
||||||
'%',
|
|
||||||
'&&',
|
|
||||||
r'\|\|',
|
|
||||||
'&=',
|
|
||||||
'&',
|
|
||||||
r'\|=',
|
|
||||||
r'\|',
|
|
||||||
'=',
|
|
||||||
'!',
|
|
||||||
':',
|
|
||||||
r'\?',
|
|
||||||
r'\^',
|
|
||||||
r'\bdelete\b',
|
|
||||||
r'\bin\b',
|
|
||||||
r'\binstanceof\b',
|
|
||||||
r'\bnew\b',
|
|
||||||
r'\btypeof\b',
|
|
||||||
r'\bvoid\b',
|
|
||||||
r'\.',
|
|
||||||
]
|
|
||||||
OPERATOR = re.compile('|'.join(OPERATOR_LIST))
|
|
||||||
|
|
||||||
WHITESPACE = re.compile(r'\s+')
|
|
||||||
SEMICOLON = re.compile(r';')
|
|
||||||
# Technically JavaScript identifiers can't contain '.', but we treat a set of
|
|
||||||
# nested identifiers as a single identifier, except for trailing dots.
|
|
||||||
NESTED_IDENTIFIER = r'[a-zA-Z_$]([%s]|\.[a-zA-Z_$])*' % IDENTIFIER_CHAR
|
|
||||||
IDENTIFIER = re.compile(NESTED_IDENTIFIER)
|
|
||||||
|
|
||||||
SIMPLE_LVALUE = re.compile(r"""
|
|
||||||
(?P<identifier>%s) # a valid identifier
|
|
||||||
(?=\s* # optional whitespace
|
|
||||||
\= # look ahead to equal sign
|
|
||||||
(?!=)) # not follwed by equal
|
|
||||||
""" % NESTED_IDENTIFIER, re.VERBOSE)
|
|
||||||
|
|
||||||
# A doc flag is a @ sign followed by non-space characters that appears at the
|
|
||||||
# beginning of the line, after whitespace, or after a '{'. The look-behind
|
|
||||||
# check is necessary to not match someone@google.com as a flag.
|
|
||||||
DOC_FLAG = re.compile(r'(^|(?<=\s))@(?P<name>[a-zA-Z]+)')
|
|
||||||
# To properly parse parameter names and complex doctypes containing
|
|
||||||
# whitespace, we need to tokenize whitespace into a token after certain
|
|
||||||
# doctags. All statetracker.HAS_TYPE that are not listed here must not contain
|
|
||||||
# any whitespace in their types.
|
|
||||||
DOC_FLAG_LEX_SPACES = re.compile(
|
|
||||||
r'(^|(?<=\s))@(?P<name>%s)\b' %
|
|
||||||
'|'.join([
|
|
||||||
'const',
|
|
||||||
'enum',
|
|
||||||
'extends',
|
|
||||||
'final',
|
|
||||||
'implements',
|
|
||||||
'param',
|
|
||||||
'private',
|
|
||||||
'protected',
|
|
||||||
'public',
|
|
||||||
'return',
|
|
||||||
'type',
|
|
||||||
'typedef'
|
|
||||||
]))
|
|
||||||
|
|
||||||
DOC_INLINE_FLAG = re.compile(r'(?<={)@(?P<name>[a-zA-Z]+)')
|
|
||||||
|
|
||||||
DOC_TYPE_BLOCK_START = re.compile(r'[<(]')
|
|
||||||
DOC_TYPE_BLOCK_END = re.compile(r'[>)]')
|
|
||||||
DOC_TYPE_MODIFIERS = re.compile(r'[!?|,:=]')
|
|
||||||
|
|
||||||
# Star followed by non-slash, i.e a star that does not end a comment.
|
|
||||||
# This is used for TYPE_GROUP below.
|
|
||||||
SAFE_STAR = r'(\*(?!/))'
|
|
||||||
|
|
||||||
COMMON_DOC_MATCHERS = [
|
|
||||||
# Find the end of the comment.
|
|
||||||
Matcher(END_BLOCK_COMMENT, Type.END_DOC_COMMENT,
|
|
||||||
JavaScriptModes.TEXT_MODE),
|
|
||||||
|
|
||||||
# Tokenize documented flags like @private.
|
|
||||||
Matcher(DOC_INLINE_FLAG, Type.DOC_INLINE_FLAG),
|
|
||||||
Matcher(DOC_FLAG_LEX_SPACES, Type.DOC_FLAG,
|
|
||||||
JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE),
|
|
||||||
|
|
||||||
# Encountering a doc flag should leave lex spaces mode.
|
|
||||||
Matcher(DOC_FLAG, Type.DOC_FLAG, JavaScriptModes.DOC_COMMENT_MODE),
|
|
||||||
|
|
||||||
# Tokenize braces so we can find types.
|
|
||||||
Matcher(START_BLOCK, Type.DOC_START_BRACE),
|
|
||||||
Matcher(END_BLOCK, Type.DOC_END_BRACE),
|
|
||||||
|
|
||||||
# And some more to parse types.
|
|
||||||
Matcher(DOC_TYPE_BLOCK_START, Type.DOC_TYPE_START_BLOCK),
|
|
||||||
Matcher(DOC_TYPE_BLOCK_END, Type.DOC_TYPE_END_BLOCK),
|
|
||||||
|
|
||||||
Matcher(DOC_TYPE_MODIFIERS, Type.DOC_TYPE_MODIFIER),
|
|
||||||
Matcher(DOC_COMMENT_TYPE_TEXT, Type.COMMENT),
|
|
||||||
|
|
||||||
Matcher(DOC_PREFIX, Type.DOC_PREFIX, None, True)]
|
|
||||||
|
|
||||||
# When text is not matched, it is given this default type based on mode.
|
|
||||||
# If unspecified in this map, the default default is Type.NORMAL.
|
|
||||||
JAVASCRIPT_DEFAULT_TYPES = {
|
|
||||||
JavaScriptModes.DOC_COMMENT_MODE: Type.COMMENT,
|
|
||||||
JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: Type.COMMENT
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def BuildMatchers(cls):
|
|
||||||
"""Builds the token matcher group.
|
|
||||||
|
|
||||||
The token matcher groups work as follows: it is a list of Matcher objects.
|
|
||||||
The matchers will be tried in this order, and the first to match will be
|
|
||||||
returned. Hence the order is important because the matchers that come first
|
|
||||||
overrule the matchers that come later.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The completed token matcher group.
|
|
||||||
"""
|
|
||||||
# Match a keyword string followed by a non-identifier character in order to
|
|
||||||
# not match something like doSomething as do + Something.
|
|
||||||
keyword = re.compile('(%s)((?=[^%s])|$)' % (
|
|
||||||
'|'.join(cls.KEYWORD_LIST), cls.IDENTIFIER_CHAR))
|
|
||||||
return {
|
|
||||||
|
|
||||||
# Matchers for basic text mode.
|
|
||||||
JavaScriptModes.TEXT_MODE: [
|
|
||||||
# Check a big group - strings, starting comments, and regexes - all
|
|
||||||
# of which could be intertwined. 'string with /regex/',
|
|
||||||
# /regex with 'string'/, /* comment with /regex/ and string */ (and
|
|
||||||
# so on)
|
|
||||||
Matcher(cls.START_DOC_COMMENT, Type.START_DOC_COMMENT,
|
|
||||||
JavaScriptModes.DOC_COMMENT_MODE),
|
|
||||||
Matcher(cls.START_BLOCK_COMMENT, Type.START_BLOCK_COMMENT,
|
|
||||||
JavaScriptModes.BLOCK_COMMENT_MODE),
|
|
||||||
Matcher(cls.END_OF_LINE_SINGLE_LINE_COMMENT,
|
|
||||||
Type.START_SINGLE_LINE_COMMENT),
|
|
||||||
Matcher(cls.START_SINGLE_LINE_COMMENT,
|
|
||||||
Type.START_SINGLE_LINE_COMMENT,
|
|
||||||
JavaScriptModes.LINE_COMMENT_MODE),
|
|
||||||
Matcher(cls.SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_START,
|
|
||||||
JavaScriptModes.SINGLE_QUOTE_STRING_MODE),
|
|
||||||
Matcher(cls.DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_START,
|
|
||||||
JavaScriptModes.DOUBLE_QUOTE_STRING_MODE),
|
|
||||||
Matcher(cls.REGEX, Type.REGEX),
|
|
||||||
|
|
||||||
# Next we check for start blocks appearing outside any of the items
|
|
||||||
# above.
|
|
||||||
Matcher(cls.START_BLOCK, Type.START_BLOCK),
|
|
||||||
Matcher(cls.END_BLOCK, Type.END_BLOCK),
|
|
||||||
|
|
||||||
# Then we search for function declarations.
|
|
||||||
Matcher(cls.FUNCTION_DECLARATION, Type.FUNCTION_DECLARATION,
|
|
||||||
JavaScriptModes.FUNCTION_MODE),
|
|
||||||
|
|
||||||
# Next, we convert non-function related parens to tokens.
|
|
||||||
Matcher(cls.OPENING_PAREN, Type.START_PAREN),
|
|
||||||
Matcher(cls.CLOSING_PAREN, Type.END_PAREN),
|
|
||||||
|
|
||||||
# Next, we convert brackets to tokens.
|
|
||||||
Matcher(cls.OPENING_BRACKET, Type.START_BRACKET),
|
|
||||||
Matcher(cls.CLOSING_BRACKET, Type.END_BRACKET),
|
|
||||||
|
|
||||||
# Find numbers. This has to happen before operators because
|
|
||||||
# scientific notation numbers can have + and - in them.
|
|
||||||
Matcher(cls.NUMBER, Type.NUMBER),
|
|
||||||
|
|
||||||
# Find operators and simple assignments
|
|
||||||
Matcher(cls.SIMPLE_LVALUE, Type.SIMPLE_LVALUE),
|
|
||||||
Matcher(cls.OPERATOR, Type.OPERATOR),
|
|
||||||
|
|
||||||
# Find key words and whitespace.
|
|
||||||
Matcher(keyword, Type.KEYWORD),
|
|
||||||
Matcher(cls.WHITESPACE, Type.WHITESPACE),
|
|
||||||
|
|
||||||
# Find identifiers.
|
|
||||||
Matcher(cls.IDENTIFIER, Type.IDENTIFIER),
|
|
||||||
|
|
||||||
# Finally, we convert semicolons to tokens.
|
|
||||||
Matcher(cls.SEMICOLON, Type.SEMICOLON)],
|
|
||||||
|
|
||||||
# Matchers for single quote strings.
|
|
||||||
JavaScriptModes.SINGLE_QUOTE_STRING_MODE: [
|
|
||||||
Matcher(cls.SINGLE_QUOTE_TEXT, Type.STRING_TEXT),
|
|
||||||
Matcher(cls.SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_END,
|
|
||||||
JavaScriptModes.TEXT_MODE)],
|
|
||||||
|
|
||||||
# Matchers for double quote strings.
|
|
||||||
JavaScriptModes.DOUBLE_QUOTE_STRING_MODE: [
|
|
||||||
Matcher(cls.DOUBLE_QUOTE_TEXT, Type.STRING_TEXT),
|
|
||||||
Matcher(cls.DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_END,
|
|
||||||
JavaScriptModes.TEXT_MODE)],
|
|
||||||
|
|
||||||
# Matchers for block comments.
|
|
||||||
JavaScriptModes.BLOCK_COMMENT_MODE: [
|
|
||||||
# First we check for exiting a block comment.
|
|
||||||
Matcher(cls.END_BLOCK_COMMENT, Type.END_BLOCK_COMMENT,
|
|
||||||
JavaScriptModes.TEXT_MODE),
|
|
||||||
|
|
||||||
# Match non-comment-ending text..
|
|
||||||
Matcher(cls.BLOCK_COMMENT_TEXT, Type.COMMENT)],
|
|
||||||
|
|
||||||
# Matchers for doc comments.
|
|
||||||
JavaScriptModes.DOC_COMMENT_MODE: cls.COMMON_DOC_MATCHERS + [
|
|
||||||
Matcher(cls.DOC_COMMENT_TEXT, Type.COMMENT)],
|
|
||||||
|
|
||||||
JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: cls.COMMON_DOC_MATCHERS + [
|
|
||||||
Matcher(cls.WHITESPACE, Type.COMMENT),
|
|
||||||
Matcher(cls.DOC_COMMENT_NO_SPACES_TEXT, Type.COMMENT)],
|
|
||||||
|
|
||||||
# Matchers for single line comments.
|
|
||||||
JavaScriptModes.LINE_COMMENT_MODE: [
|
|
||||||
# We greedy match until the end of the line in line comment mode.
|
|
||||||
Matcher(cls.ANYTHING, Type.COMMENT, JavaScriptModes.TEXT_MODE)],
|
|
||||||
|
|
||||||
# Matchers for code after the function keyword.
|
|
||||||
JavaScriptModes.FUNCTION_MODE: [
|
|
||||||
# Must match open paren before anything else and move into parameter
|
|
||||||
# mode, otherwise everything inside the parameter list is parsed
|
|
||||||
# incorrectly.
|
|
||||||
Matcher(cls.OPENING_PAREN, Type.START_PARAMETERS,
|
|
||||||
JavaScriptModes.PARAMETER_MODE),
|
|
||||||
Matcher(cls.WHITESPACE, Type.WHITESPACE),
|
|
||||||
Matcher(cls.IDENTIFIER, Type.FUNCTION_NAME)],
|
|
||||||
|
|
||||||
# Matchers for function parameters
|
|
||||||
JavaScriptModes.PARAMETER_MODE: [
|
|
||||||
# When in function parameter mode, a closing paren is treated
|
|
||||||
# specially. Everything else is treated as lines of parameters.
|
|
||||||
Matcher(cls.CLOSING_PAREN_WITH_SPACE, Type.END_PARAMETERS,
|
|
||||||
JavaScriptModes.TEXT_MODE),
|
|
||||||
Matcher(cls.PARAMETERS, Type.PARAMETERS,
|
|
||||||
JavaScriptModes.PARAMETER_MODE)]}
|
|
||||||
|
|
||||||
def __init__(self, parse_js_doc=True):
|
|
||||||
"""Create a tokenizer object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parse_js_doc: Whether to do detailed parsing of javascript doc comments,
|
|
||||||
or simply treat them as normal comments. Defaults to parsing JsDoc.
|
|
||||||
"""
|
|
||||||
matchers = self.BuildMatchers()
|
|
||||||
if not parse_js_doc:
|
|
||||||
# Make a copy so the original doesn't get modified.
|
|
||||||
matchers = copy.deepcopy(matchers)
|
|
||||||
matchers[JavaScriptModes.DOC_COMMENT_MODE] = matchers[
|
|
||||||
JavaScriptModes.BLOCK_COMMENT_MODE]
|
|
||||||
|
|
||||||
tokenizer.Tokenizer.__init__(self, JavaScriptModes.TEXT_MODE, matchers,
|
|
||||||
self.JAVASCRIPT_DEFAULT_TYPES)
|
|
||||||
|
|
||||||
def _CreateToken(self, string, token_type, line, line_number, values=None):
|
|
||||||
"""Creates a new JavaScriptToken object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string of input the token contains.
|
|
||||||
token_type: The type of token.
|
|
||||||
line: The text of the line this token is in.
|
|
||||||
line_number: The line number of the token.
|
|
||||||
values: A dict of named values within the token. For instance, a
|
|
||||||
function declaration may have a value called 'name' which captures the
|
|
||||||
name of the function.
|
|
||||||
"""
|
|
||||||
return javascripttokens.JavaScriptToken(string, token_type, line,
|
|
||||||
line_number, values, line_number)
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Classes to represent JavaScript tokens."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
class JavaScriptTokenType(tokens.TokenType):
|
|
||||||
"""Enumeration of JavaScript token types, and useful sets of token types."""
|
|
||||||
NUMBER = 'number'
|
|
||||||
START_SINGLE_LINE_COMMENT = '//'
|
|
||||||
START_BLOCK_COMMENT = '/*'
|
|
||||||
START_DOC_COMMENT = '/**'
|
|
||||||
END_BLOCK_COMMENT = '*/'
|
|
||||||
END_DOC_COMMENT = 'doc */'
|
|
||||||
COMMENT = 'comment'
|
|
||||||
SINGLE_QUOTE_STRING_START = "'string"
|
|
||||||
SINGLE_QUOTE_STRING_END = "string'"
|
|
||||||
DOUBLE_QUOTE_STRING_START = '"string'
|
|
||||||
DOUBLE_QUOTE_STRING_END = 'string"'
|
|
||||||
STRING_TEXT = 'string'
|
|
||||||
START_BLOCK = '{'
|
|
||||||
END_BLOCK = '}'
|
|
||||||
START_PAREN = '('
|
|
||||||
END_PAREN = ')'
|
|
||||||
START_BRACKET = '['
|
|
||||||
END_BRACKET = ']'
|
|
||||||
REGEX = '/regex/'
|
|
||||||
FUNCTION_DECLARATION = 'function(...)'
|
|
||||||
FUNCTION_NAME = 'function functionName(...)'
|
|
||||||
START_PARAMETERS = 'startparams('
|
|
||||||
PARAMETERS = 'pa,ra,ms'
|
|
||||||
END_PARAMETERS = ')endparams'
|
|
||||||
SEMICOLON = ';'
|
|
||||||
DOC_FLAG = '@flag'
|
|
||||||
DOC_INLINE_FLAG = '{@flag ...}'
|
|
||||||
DOC_START_BRACE = 'doc {'
|
|
||||||
DOC_END_BRACE = 'doc }'
|
|
||||||
DOC_PREFIX = 'comment prefix: * '
|
|
||||||
DOC_TYPE_START_BLOCK = 'Type <'
|
|
||||||
DOC_TYPE_END_BLOCK = 'Type >'
|
|
||||||
DOC_TYPE_MODIFIER = 'modifier'
|
|
||||||
SIMPLE_LVALUE = 'lvalue='
|
|
||||||
KEYWORD = 'keyword'
|
|
||||||
OPERATOR = 'operator'
|
|
||||||
IDENTIFIER = 'identifier'
|
|
||||||
|
|
||||||
STRING_TYPES = frozenset([
|
|
||||||
SINGLE_QUOTE_STRING_START, SINGLE_QUOTE_STRING_END,
|
|
||||||
DOUBLE_QUOTE_STRING_START, DOUBLE_QUOTE_STRING_END, STRING_TEXT])
|
|
||||||
|
|
||||||
COMMENT_TYPES = frozenset([
|
|
||||||
START_SINGLE_LINE_COMMENT, COMMENT,
|
|
||||||
START_BLOCK_COMMENT, START_DOC_COMMENT,
|
|
||||||
END_BLOCK_COMMENT, END_DOC_COMMENT,
|
|
||||||
DOC_START_BRACE, DOC_END_BRACE,
|
|
||||||
DOC_FLAG, DOC_INLINE_FLAG, DOC_PREFIX,
|
|
||||||
DOC_TYPE_START_BLOCK, DOC_TYPE_END_BLOCK, DOC_TYPE_MODIFIER])
|
|
||||||
|
|
||||||
FLAG_DESCRIPTION_TYPES = frozenset([
|
|
||||||
DOC_INLINE_FLAG, COMMENT, DOC_START_BRACE, DOC_END_BRACE,
|
|
||||||
DOC_TYPE_START_BLOCK, DOC_TYPE_END_BLOCK, DOC_TYPE_MODIFIER])
|
|
||||||
|
|
||||||
FLAG_ENDING_TYPES = frozenset([DOC_FLAG, END_DOC_COMMENT])
|
|
||||||
|
|
||||||
NON_CODE_TYPES = COMMENT_TYPES | frozenset([
|
|
||||||
tokens.TokenType.WHITESPACE, tokens.TokenType.BLANK_LINE])
|
|
||||||
|
|
||||||
UNARY_OPERATORS = ['!', 'new', 'delete', 'typeof', 'void']
|
|
||||||
|
|
||||||
UNARY_OK_OPERATORS = ['--', '++', '-', '+'] + UNARY_OPERATORS
|
|
||||||
|
|
||||||
UNARY_POST_OPERATORS = ['--', '++']
|
|
||||||
|
|
||||||
# An expression ender is any token that can end an object - i.e. we could have
|
|
||||||
# x.y or [1, 2], or (10 + 9) or {a: 10}.
|
|
||||||
EXPRESSION_ENDER_TYPES = [tokens.TokenType.NORMAL, IDENTIFIER, NUMBER,
|
|
||||||
SIMPLE_LVALUE, END_BRACKET, END_PAREN, END_BLOCK,
|
|
||||||
SINGLE_QUOTE_STRING_END, DOUBLE_QUOTE_STRING_END]
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptToken(tokens.Token):
|
|
||||||
"""JavaScript token subclass of Token, provides extra instance checks.
|
|
||||||
|
|
||||||
The following token types have data in attached_object:
|
|
||||||
- All JsDoc flags: a parser.JsDocFlag object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def IsKeyword(self, keyword):
|
|
||||||
"""Tests if this token is the given keyword.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keyword: The keyword to compare to.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if this token is a keyword token with the given name.
|
|
||||||
"""
|
|
||||||
return self.type == JavaScriptTokenType.KEYWORD and self.string == keyword
|
|
||||||
|
|
||||||
def IsOperator(self, operator):
|
|
||||||
"""Tests if this token is the given operator.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
operator: The operator to compare to.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if this token is a operator token with the given name.
|
|
||||||
"""
|
|
||||||
return self.type == JavaScriptTokenType.OPERATOR and self.string == operator
|
|
||||||
|
|
||||||
def IsAssignment(self):
|
|
||||||
"""Tests if this token is an assignment operator.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if this token is an assignment operator.
|
|
||||||
"""
|
|
||||||
return (self.type == JavaScriptTokenType.OPERATOR and
|
|
||||||
self.string.endswith('=') and
|
|
||||||
self.string not in ('==', '!=', '>=', '<=', '===', '!=='))
|
|
||||||
|
|
||||||
def IsComment(self):
|
|
||||||
"""Tests if this token is any part of a comment.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if this token is any part of a comment.
|
|
||||||
"""
|
|
||||||
return self.type in JavaScriptTokenType.COMMENT_TYPES
|
|
||||||
|
|
||||||
def IsCode(self):
|
|
||||||
"""Tests if this token is code, as opposed to a comment or whitespace."""
|
|
||||||
return self.type not in JavaScriptTokenType.NON_CODE_TYPES
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<JavaScriptToken: %d, %s, "%s", %r, %r>' % (self.line_number,
|
|
||||||
self.type, self.string,
|
|
||||||
self.values,
|
|
||||||
self.metadata)
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
"""Tests for gjslint --nostrict.
|
|
||||||
|
|
||||||
Tests errors that can be thrown by gjslint when not in strict mode.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import filetestcase
|
|
||||||
|
|
||||||
_RESOURCE_PREFIX = 'closure_linter/testdata'
|
|
||||||
|
|
||||||
flags.FLAGS.strict = False
|
|
||||||
flags.FLAGS.custom_jsdoc_tags = ('customtag', 'requires')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
flags.FLAGS.limited_doc_files = ('externs.js', 'dummy.js',
|
|
||||||
'limited_doc_checks.js')
|
|
||||||
|
|
||||||
|
|
||||||
# List of files under testdata to test.
|
|
||||||
# We need to list files explicitly since pyglib can't list directories.
|
|
||||||
_TEST_FILES = [
|
|
||||||
'not_strict.js'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class GJsLintTestSuite(unittest.TestSuite):
|
|
||||||
"""Test suite to run a GJsLintTest for each of several files.
|
|
||||||
|
|
||||||
If sys.argv[1:] is non-empty, it is interpreted as a list of filenames in
|
|
||||||
testdata to test. Otherwise, _TEST_FILES is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tests=()):
|
|
||||||
unittest.TestSuite.__init__(self, tests)
|
|
||||||
|
|
||||||
argv = sys.argv and sys.argv[1:] or []
|
|
||||||
if argv:
|
|
||||||
test_files = argv
|
|
||||||
else:
|
|
||||||
test_files = _TEST_FILES
|
|
||||||
for test_file in test_files:
|
|
||||||
resource_path = os.path.join(_RESOURCE_PREFIX, test_file)
|
|
||||||
self.addTest(filetestcase.AnnotatedFileTestCase(resource_path,
|
|
||||||
runner.Run,
|
|
||||||
errors.ByName))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Don't let main parse args; it happens in the TestSuite.
|
|
||||||
googletest.main(argv=sys.argv[0:1], defaultTest='GJsLintTestSuite')
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
"""Contains logic for sorting goog.provide and goog.require statements.
|
|
||||||
|
|
||||||
Closurized JavaScript files use goog.provide and goog.require statements at the
|
|
||||||
top of the file to manage dependencies. These statements should be sorted
|
|
||||||
alphabetically, however, it is common for them to be accompanied by inline
|
|
||||||
comments or suppression annotations. In order to sort these statements without
|
|
||||||
disrupting their comments and annotations, the association between statements
|
|
||||||
and comments/annotations must be maintained while sorting.
|
|
||||||
|
|
||||||
RequireProvideSorter: Handles checking/fixing of provide/require statements.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class RequireProvideSorter(object):
|
|
||||||
"""Checks for and fixes alphabetization of provide and require statements.
|
|
||||||
|
|
||||||
When alphabetizing, comments on the same line or comments directly above a
|
|
||||||
goog.provide or goog.require statement are associated with that statement and
|
|
||||||
stay with the statement as it gets sorted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def CheckProvides(self, token):
|
|
||||||
"""Checks alphabetization of goog.provide statements.
|
|
||||||
|
|
||||||
Iterates over tokens in given token stream, identifies goog.provide tokens,
|
|
||||||
and checks that they occur in alphabetical order by the object being
|
|
||||||
provided.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token in the token stream before any goog.provide tokens.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first provide token in the token stream.
|
|
||||||
|
|
||||||
None is returned if all goog.provide statements are already sorted.
|
|
||||||
"""
|
|
||||||
provide_tokens = self._GetRequireOrProvideTokens(token, 'goog.provide')
|
|
||||||
provide_strings = self._GetRequireOrProvideTokenStrings(provide_tokens)
|
|
||||||
sorted_provide_strings = sorted(provide_strings)
|
|
||||||
if provide_strings != sorted_provide_strings:
|
|
||||||
return provide_tokens[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def CheckRequires(self, token):
|
|
||||||
"""Checks alphabetization of goog.require statements.
|
|
||||||
|
|
||||||
Iterates over tokens in given token stream, identifies goog.require tokens,
|
|
||||||
and checks that they occur in alphabetical order by the dependency being
|
|
||||||
required.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token in the token stream before any goog.require tokens.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first require token in the token stream.
|
|
||||||
|
|
||||||
None is returned if all goog.require statements are already sorted.
|
|
||||||
"""
|
|
||||||
require_tokens = self._GetRequireOrProvideTokens(token, 'goog.require')
|
|
||||||
require_strings = self._GetRequireOrProvideTokenStrings(require_tokens)
|
|
||||||
sorted_require_strings = sorted(require_strings)
|
|
||||||
if require_strings != sorted_require_strings:
|
|
||||||
return require_tokens[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def FixProvides(self, token):
|
|
||||||
"""Sorts goog.provide statements in the given token stream alphabetically.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
"""
|
|
||||||
self._FixProvidesOrRequires(
|
|
||||||
self._GetRequireOrProvideTokens(token, 'goog.provide'))
|
|
||||||
|
|
||||||
def FixRequires(self, token):
|
|
||||||
"""Sorts goog.require statements in the given token stream alphabetically.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
"""
|
|
||||||
self._FixProvidesOrRequires(
|
|
||||||
self._GetRequireOrProvideTokens(token, 'goog.require'))
|
|
||||||
|
|
||||||
def _FixProvidesOrRequires(self, tokens):
|
|
||||||
"""Sorts goog.provide or goog.require statements.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: A list of goog.provide or goog.require tokens in the order they
|
|
||||||
appear in the token stream. i.e. the first token in this list must
|
|
||||||
be the first goog.provide or goog.require token.
|
|
||||||
"""
|
|
||||||
strings = self._GetRequireOrProvideTokenStrings(tokens)
|
|
||||||
sorted_strings = sorted(strings)
|
|
||||||
|
|
||||||
# Make a separate pass to remove any blank lines between goog.require/
|
|
||||||
# goog.provide tokens.
|
|
||||||
first_token = tokens[0]
|
|
||||||
last_token = tokens[-1]
|
|
||||||
i = last_token
|
|
||||||
while i != first_token and i is not None:
|
|
||||||
if i.type is Type.BLANK_LINE:
|
|
||||||
tokenutil.DeleteToken(i)
|
|
||||||
i = i.previous
|
|
||||||
|
|
||||||
# A map from required/provided object name to tokens that make up the line
|
|
||||||
# it was on, including any comments immediately before it or after it on the
|
|
||||||
# same line.
|
|
||||||
tokens_map = self._GetTokensMap(tokens)
|
|
||||||
|
|
||||||
# Iterate over the map removing all tokens.
|
|
||||||
for name in tokens_map:
|
|
||||||
tokens_to_delete = tokens_map[name]
|
|
||||||
for i in tokens_to_delete:
|
|
||||||
tokenutil.DeleteToken(i)
|
|
||||||
|
|
||||||
# Save token to rest of file. Sorted token will be inserted before this.
|
|
||||||
rest_of_file = tokens_map[strings[-1]][-1].next
|
|
||||||
|
|
||||||
# Re-add all tokens in the map in alphabetical order.
|
|
||||||
insert_after = tokens[0].previous
|
|
||||||
for string in sorted_strings:
|
|
||||||
for i in tokens_map[string]:
|
|
||||||
if rest_of_file:
|
|
||||||
tokenutil.InsertTokenBefore(i, rest_of_file)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertTokenAfter(i, insert_after)
|
|
||||||
insert_after = i
|
|
||||||
|
|
||||||
def _GetRequireOrProvideTokens(self, token, token_string):
|
|
||||||
"""Gets all goog.provide or goog.require tokens in the given token stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
token_string: One of 'goog.provide' or 'goog.require' to indicate which
|
|
||||||
tokens to find.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of goog.provide or goog.require tokens in the order they appear in
|
|
||||||
the token stream.
|
|
||||||
"""
|
|
||||||
tokens = []
|
|
||||||
while token:
|
|
||||||
if token.type == Type.IDENTIFIER:
|
|
||||||
if token.string == token_string:
|
|
||||||
tokens.append(token)
|
|
||||||
elif token.string not in [
|
|
||||||
'goog.provide', 'goog.require', 'goog.setTestOnly']:
|
|
||||||
# These 3 identifiers are at the top of the file. So if any other
|
|
||||||
# identifier is encountered, return.
|
|
||||||
# TODO(user): Once it's decided what ordering goog.require
|
|
||||||
# should use, add 'goog.module' to the list above and implement the
|
|
||||||
# decision.
|
|
||||||
break
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
return tokens
|
|
||||||
|
|
||||||
def _GetRequireOrProvideTokenStrings(self, tokens):
|
|
||||||
"""Gets a list of strings corresponding to the given list of tokens.
|
|
||||||
|
|
||||||
The string will be the next string in the token stream after each token in
|
|
||||||
tokens. This is used to find the object being provided/required by a given
|
|
||||||
goog.provide or goog.require token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: A list of goog.provide or goog.require tokens.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of object names that are being provided or required by the given
|
|
||||||
list of tokens. For example:
|
|
||||||
|
|
||||||
['object.a', 'object.c', 'object.b']
|
|
||||||
"""
|
|
||||||
token_strings = []
|
|
||||||
for token in tokens:
|
|
||||||
if not token.is_deleted:
|
|
||||||
name = tokenutil.GetStringAfterToken(token)
|
|
||||||
token_strings.append(name)
|
|
||||||
return token_strings
|
|
||||||
|
|
||||||
def _GetTokensMap(self, tokens):
|
|
||||||
"""Gets a map from object name to tokens associated with that object.
|
|
||||||
|
|
||||||
Starting from the goog.provide/goog.require token, searches backwards in the
|
|
||||||
token stream for any lines that start with a comment. These lines are
|
|
||||||
associated with the goog.provide/goog.require token. Also associates any
|
|
||||||
tokens on the same line as the goog.provide/goog.require token with that
|
|
||||||
token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: A list of goog.provide or goog.require tokens.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A dictionary that maps object names to the tokens associated with the
|
|
||||||
goog.provide or goog.require of that object name. For example:
|
|
||||||
|
|
||||||
{
|
|
||||||
'object.a': [JavaScriptToken, JavaScriptToken, ...],
|
|
||||||
'object.b': [...]
|
|
||||||
}
|
|
||||||
|
|
||||||
The list of tokens includes any comment lines above the goog.provide or
|
|
||||||
goog.require statement and everything after the statement on the same
|
|
||||||
line. For example, all of the following would be associated with
|
|
||||||
'object.a':
|
|
||||||
|
|
||||||
/** @suppress {extraRequire} */
|
|
||||||
goog.require('object.a'); // Some comment.
|
|
||||||
"""
|
|
||||||
tokens_map = {}
|
|
||||||
for token in tokens:
|
|
||||||
object_name = tokenutil.GetStringAfterToken(token)
|
|
||||||
# If the previous line starts with a comment, presume that the comment
|
|
||||||
# relates to the goog.require or goog.provide and keep them together when
|
|
||||||
# sorting.
|
|
||||||
first_token = token
|
|
||||||
previous_first_token = tokenutil.GetFirstTokenInPreviousLine(first_token)
|
|
||||||
while (previous_first_token and
|
|
||||||
previous_first_token.IsAnyType(Type.COMMENT_TYPES)):
|
|
||||||
first_token = previous_first_token
|
|
||||||
previous_first_token = tokenutil.GetFirstTokenInPreviousLine(
|
|
||||||
first_token)
|
|
||||||
|
|
||||||
# Find the last token on the line.
|
|
||||||
last_token = tokenutil.GetLastTokenInSameLine(token)
|
|
||||||
|
|
||||||
all_tokens = self._GetTokenList(first_token, last_token)
|
|
||||||
tokens_map[object_name] = all_tokens
|
|
||||||
return tokens_map
|
|
||||||
|
|
||||||
def _GetTokenList(self, first_token, last_token):
|
|
||||||
"""Gets a list of all tokens from first_token to last_token, inclusive.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
first_token: The first token to get.
|
|
||||||
last_token: The last token to get.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of all tokens between first_token and last_token, including both
|
|
||||||
first_token and last_token.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If the token stream ends before last_token is reached.
|
|
||||||
"""
|
|
||||||
token_list = []
|
|
||||||
token = first_token
|
|
||||||
while token != last_token:
|
|
||||||
if not token:
|
|
||||||
raise Exception('ran out of tokens')
|
|
||||||
token_list.append(token)
|
|
||||||
token = token.next
|
|
||||||
token_list.append(last_token)
|
|
||||||
|
|
||||||
return token_list
|
|
||||||
|
|
||||||
def GetFixedRequireString(self, token):
|
|
||||||
"""Get fixed/sorted order of goog.require statements.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string for correct sorted order of goog.require.
|
|
||||||
"""
|
|
||||||
return self._GetFixedRequireOrProvideString(
|
|
||||||
self._GetRequireOrProvideTokens(token, 'goog.require'))
|
|
||||||
|
|
||||||
def GetFixedProvideString(self, token):
|
|
||||||
"""Get fixed/sorted order of goog.provide statements.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string for correct sorted order of goog.provide.
|
|
||||||
"""
|
|
||||||
return self._GetFixedRequireOrProvideString(
|
|
||||||
self._GetRequireOrProvideTokens(token, 'goog.provide'))
|
|
||||||
|
|
||||||
def _GetFixedRequireOrProvideString(self, tokens):
|
|
||||||
"""Sorts goog.provide or goog.require statements.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: A list of goog.provide or goog.require tokens in the order they
|
|
||||||
appear in the token stream. i.e. the first token in this list must
|
|
||||||
be the first goog.provide or goog.require token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string for sorted goog.require or goog.provide statements
|
|
||||||
"""
|
|
||||||
|
|
||||||
# A map from required/provided object name to tokens that make up the line
|
|
||||||
# it was on, including any comments immediately before it or after it on the
|
|
||||||
# same line.
|
|
||||||
tokens_map = self._GetTokensMap(tokens)
|
|
||||||
sorted_strings = sorted(tokens_map.keys())
|
|
||||||
|
|
||||||
new_order = ''
|
|
||||||
for string in sorted_strings:
|
|
||||||
for i in tokens_map[string]:
|
|
||||||
new_order += i.string
|
|
||||||
if i.IsLastInLine():
|
|
||||||
new_order += '\n'
|
|
||||||
|
|
||||||
return new_order
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for RequireProvideSorter."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import requireprovidesorter
|
|
||||||
from closure_linter import testutil
|
|
||||||
|
|
||||||
# pylint: disable=g-bad-name
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class RequireProvideSorterTest(googletest.TestCase):
|
|
||||||
"""Tests for RequireProvideSorter."""
|
|
||||||
|
|
||||||
def testGetFixedProvideString(self):
|
|
||||||
"""Tests that fixed string constains proper comments also."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.xyz\');',
|
|
||||||
'/** @suppress {extraprovide} **/',
|
|
||||||
'goog.provide(\'package.abcd\');'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected_lines = [
|
|
||||||
'/** @suppress {extraprovide} **/',
|
|
||||||
'goog.provide(\'package.abcd\');',
|
|
||||||
'goog.provide(\'package.xyz\');'
|
|
||||||
]
|
|
||||||
|
|
||||||
token = testutil.TokenizeSourceAndRunEcmaPass(input_lines)
|
|
||||||
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
fixed_provide_string = sorter.GetFixedProvideString(token)
|
|
||||||
|
|
||||||
self.assertEquals(expected_lines, fixed_provide_string.splitlines())
|
|
||||||
|
|
||||||
def testGetFixedRequireString(self):
|
|
||||||
"""Tests that fixed string constains proper comments also."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.xyz\');',
|
|
||||||
'/** This is needed for scope. **/',
|
|
||||||
'goog.require(\'package.abcd\');'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected_lines = [
|
|
||||||
'/** This is needed for scope. **/',
|
|
||||||
'goog.require(\'package.abcd\');',
|
|
||||||
'goog.require(\'package.xyz\');'
|
|
||||||
]
|
|
||||||
|
|
||||||
token = testutil.TokenizeSourceAndRunEcmaPass(input_lines)
|
|
||||||
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
fixed_require_string = sorter.GetFixedRequireString(token)
|
|
||||||
|
|
||||||
self.assertEquals(expected_lines, fixed_require_string.splitlines())
|
|
||||||
|
|
||||||
def testFixRequires_removeBlankLines(self):
|
|
||||||
"""Tests that blank lines are omitted in sorted goog.require statements."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.subpackage.Whatever\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'package.subpackage.ClassB\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'package.subpackage.ClassA\');'
|
|
||||||
]
|
|
||||||
expected_lines = [
|
|
||||||
'goog.provide(\'package.subpackage.Whatever\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'package.subpackage.ClassA\');',
|
|
||||||
'goog.require(\'package.subpackage.ClassB\');'
|
|
||||||
]
|
|
||||||
token = testutil.TokenizeSourceAndRunEcmaPass(input_lines)
|
|
||||||
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixRequires(token)
|
|
||||||
|
|
||||||
self.assertEquals(expected_lines, self._GetLines(token))
|
|
||||||
|
|
||||||
def fixRequiresTest_withTestOnly(self, position):
|
|
||||||
"""Regression-tests sorting even with a goog.setTestOnly statement.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
position: The position in the list where to insert the goog.setTestOnly
|
|
||||||
statement. Will be used to test all possible combinations for
|
|
||||||
this test.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.subpackage.Whatever\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'package.subpackage.ClassB\');',
|
|
||||||
'goog.require(\'package.subpackage.ClassA\');'
|
|
||||||
]
|
|
||||||
expected_lines = [
|
|
||||||
'goog.provide(\'package.subpackage.Whatever\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'package.subpackage.ClassA\');',
|
|
||||||
'goog.require(\'package.subpackage.ClassB\');'
|
|
||||||
]
|
|
||||||
input_lines.insert(position, 'goog.setTestOnly();')
|
|
||||||
expected_lines.insert(position, 'goog.setTestOnly();')
|
|
||||||
|
|
||||||
token = testutil.TokenizeSourceAndRunEcmaPass(input_lines)
|
|
||||||
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixRequires(token)
|
|
||||||
|
|
||||||
self.assertEquals(expected_lines, self._GetLines(token))
|
|
||||||
|
|
||||||
def testFixRequires_withTestOnly(self):
|
|
||||||
"""Regression-tests sorting even after a goog.setTestOnly statement."""
|
|
||||||
|
|
||||||
# goog.setTestOnly at first line.
|
|
||||||
self.fixRequiresTest_withTestOnly(position=0)
|
|
||||||
|
|
||||||
# goog.setTestOnly after goog.provide.
|
|
||||||
self.fixRequiresTest_withTestOnly(position=1)
|
|
||||||
|
|
||||||
# goog.setTestOnly before goog.require.
|
|
||||||
self.fixRequiresTest_withTestOnly(position=2)
|
|
||||||
|
|
||||||
# goog.setTestOnly after goog.require.
|
|
||||||
self.fixRequiresTest_withTestOnly(position=4)
|
|
||||||
|
|
||||||
def _GetLines(self, token):
|
|
||||||
"""Returns an array of lines based on the specified token stream."""
|
|
||||||
lines = []
|
|
||||||
line = ''
|
|
||||||
while token:
|
|
||||||
line += token.string
|
|
||||||
if token.IsLastInLine():
|
|
||||||
lines.append(line)
|
|
||||||
line = ''
|
|
||||||
token = token.next
|
|
||||||
return lines
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Main lint function. Tokenizes file, runs passes, and feeds to checker."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = 'nnaze@google.com (Nathan Naze)'
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import checker
|
|
||||||
from closure_linter import ecmalintrules
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokenizer
|
|
||||||
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import htmlutil
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
flags.DEFINE_list('limited_doc_files', ['dummy.js', 'externs.js'],
|
|
||||||
'List of files with relaxed documentation checks. Will not '
|
|
||||||
'report errors for missing documentation, some missing '
|
|
||||||
'descriptions, or methods whose @return tags don\'t have a '
|
|
||||||
'matching return statement.')
|
|
||||||
flags.DEFINE_boolean('error_trace', False,
|
|
||||||
'Whether to show error exceptions.')
|
|
||||||
flags.ADOPT_module_key_flags(checker)
|
|
||||||
flags.ADOPT_module_key_flags(ecmalintrules)
|
|
||||||
flags.ADOPT_module_key_flags(error_check)
|
|
||||||
|
|
||||||
|
|
||||||
def _GetLastNonWhiteSpaceToken(start_token):
|
|
||||||
"""Get the last non-whitespace token in a token stream."""
|
|
||||||
ret_token = None
|
|
||||||
|
|
||||||
whitespace_tokens = frozenset([
|
|
||||||
tokens.TokenType.WHITESPACE, tokens.TokenType.BLANK_LINE])
|
|
||||||
for t in start_token:
|
|
||||||
if t.type not in whitespace_tokens:
|
|
||||||
ret_token = t
|
|
||||||
|
|
||||||
return ret_token
|
|
||||||
|
|
||||||
|
|
||||||
def _IsHtml(filename):
|
|
||||||
return filename.endswith('.html') or filename.endswith('.htm')
|
|
||||||
|
|
||||||
|
|
||||||
def _Tokenize(fileobj):
|
|
||||||
"""Tokenize a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fileobj: file-like object (or iterable lines) with the source.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token in the token stream and the ending mode of the tokenizer.
|
|
||||||
"""
|
|
||||||
tokenizer = javascripttokenizer.JavaScriptTokenizer()
|
|
||||||
start_token = tokenizer.TokenizeFile(fileobj)
|
|
||||||
return start_token, tokenizer.mode
|
|
||||||
|
|
||||||
|
|
||||||
def _IsLimitedDocCheck(filename, limited_doc_files):
|
|
||||||
"""Whether this this a limited-doc file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The filename.
|
|
||||||
limited_doc_files: Iterable of strings. Suffixes of filenames that should
|
|
||||||
be limited doc check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the file should be limited check.
|
|
||||||
"""
|
|
||||||
for limited_doc_filename in limited_doc_files:
|
|
||||||
if filename.endswith(limited_doc_filename):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def Run(filename, error_handler, source=None):
|
|
||||||
"""Tokenize, run passes, and check the given file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The path of the file to check
|
|
||||||
error_handler: The error handler to report errors to.
|
|
||||||
source: A file-like object with the file source. If omitted, the file will
|
|
||||||
be read from the filename path.
|
|
||||||
"""
|
|
||||||
if not source:
|
|
||||||
try:
|
|
||||||
source = open(filename)
|
|
||||||
except IOError:
|
|
||||||
error_handler.HandleFile(filename, None)
|
|
||||||
error_handler.HandleError(
|
|
||||||
error.Error(errors.FILE_NOT_FOUND, 'File not found'))
|
|
||||||
error_handler.FinishFile()
|
|
||||||
return
|
|
||||||
|
|
||||||
if _IsHtml(filename):
|
|
||||||
source_file = htmlutil.GetScriptLines(source)
|
|
||||||
else:
|
|
||||||
source_file = source
|
|
||||||
|
|
||||||
token, tokenizer_mode = _Tokenize(source_file)
|
|
||||||
|
|
||||||
error_handler.HandleFile(filename, token)
|
|
||||||
|
|
||||||
# If we did not end in the basic mode, this a failed parse.
|
|
||||||
if tokenizer_mode is not javascripttokenizer.JavaScriptModes.TEXT_MODE:
|
|
||||||
error_handler.HandleError(
|
|
||||||
error.Error(errors.FILE_IN_BLOCK,
|
|
||||||
'File ended in mode "%s".' % tokenizer_mode,
|
|
||||||
_GetLastNonWhiteSpaceToken(token)))
|
|
||||||
|
|
||||||
# Run the ECMA pass
|
|
||||||
error_token = None
|
|
||||||
|
|
||||||
ecma_pass = ecmametadatapass.EcmaMetaDataPass()
|
|
||||||
error_token = RunMetaDataPass(token, ecma_pass, error_handler, filename)
|
|
||||||
|
|
||||||
is_limited_doc_check = (
|
|
||||||
_IsLimitedDocCheck(filename, flags.FLAGS.limited_doc_files))
|
|
||||||
|
|
||||||
_RunChecker(token, error_handler,
|
|
||||||
is_limited_doc_check,
|
|
||||||
is_html=_IsHtml(filename),
|
|
||||||
stop_token=error_token)
|
|
||||||
|
|
||||||
error_handler.FinishFile()
|
|
||||||
|
|
||||||
|
|
||||||
def RunMetaDataPass(start_token, metadata_pass, error_handler, filename=''):
|
|
||||||
"""Run a metadata pass over a token stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The first token in a token stream.
|
|
||||||
metadata_pass: Metadata pass to run.
|
|
||||||
error_handler: The error handler to report errors to.
|
|
||||||
filename: Filename of the source.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The token where the error occurred (if any).
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
|
||||||
metadata_pass.Process(start_token)
|
|
||||||
except ecmametadatapass.ParseError, parse_err:
|
|
||||||
if flags.FLAGS.error_trace:
|
|
||||||
traceback.print_exc()
|
|
||||||
error_token = parse_err.token
|
|
||||||
error_msg = str(parse_err)
|
|
||||||
error_handler.HandleError(
|
|
||||||
error.Error(errors.FILE_DOES_NOT_PARSE,
|
|
||||||
('Error parsing file at token "%s". Unable to '
|
|
||||||
'check the rest of file.'
|
|
||||||
'\nError "%s"' % (error_token, error_msg)), error_token))
|
|
||||||
return error_token
|
|
||||||
except Exception: # pylint: disable=broad-except
|
|
||||||
traceback.print_exc()
|
|
||||||
error_handler.HandleError(
|
|
||||||
error.Error(
|
|
||||||
errors.FILE_DOES_NOT_PARSE,
|
|
||||||
'Internal error in %s' % filename))
|
|
||||||
|
|
||||||
|
|
||||||
def _RunChecker(start_token, error_handler,
|
|
||||||
limited_doc_checks, is_html,
|
|
||||||
stop_token=None):
|
|
||||||
|
|
||||||
state_tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
|
|
||||||
style_checker = checker.JavaScriptStyleChecker(
|
|
||||||
state_tracker=state_tracker,
|
|
||||||
error_handler=error_handler)
|
|
||||||
|
|
||||||
style_checker.Check(start_token,
|
|
||||||
is_html=is_html,
|
|
||||||
limited_doc_checks=limited_doc_checks,
|
|
||||||
stop_token=stop_token)
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Unit tests for the runner module."""
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
|
|
||||||
import mox
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import errorhandler
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
|
|
||||||
class LimitedDocTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testIsLimitedDocCheck(self):
|
|
||||||
self.assertTrue(runner._IsLimitedDocCheck('foo_test.js', ['_test.js']))
|
|
||||||
self.assertFalse(runner._IsLimitedDocCheck('foo_bar.js', ['_test.js']))
|
|
||||||
|
|
||||||
self.assertTrue(runner._IsLimitedDocCheck(
|
|
||||||
'foo_moo.js', ['moo.js', 'quack.js']))
|
|
||||||
self.assertFalse(runner._IsLimitedDocCheck(
|
|
||||||
'foo_moo.js', ['woof.js', 'quack.js']))
|
|
||||||
|
|
||||||
|
|
||||||
class RunnerTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.mox = mox.Mox()
|
|
||||||
|
|
||||||
def testRunOnMissingFile(self):
|
|
||||||
mock_error_handler = self.mox.CreateMock(errorhandler.ErrorHandler)
|
|
||||||
|
|
||||||
def ValidateError(err):
|
|
||||||
return (isinstance(err, error.Error) and
|
|
||||||
err.code is errors.FILE_NOT_FOUND and
|
|
||||||
err.token is None)
|
|
||||||
|
|
||||||
mock_error_handler.HandleFile('does_not_exist.js', None)
|
|
||||||
mock_error_handler.HandleError(mox.Func(ValidateError))
|
|
||||||
mock_error_handler.FinishFile()
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
runner.Run('does_not_exist.js', mock_error_handler)
|
|
||||||
|
|
||||||
self.mox.VerifyAll()
|
|
||||||
|
|
||||||
def testBadTokenization(self):
|
|
||||||
mock_error_handler = self.mox.CreateMock(errorhandler.ErrorHandler)
|
|
||||||
|
|
||||||
def ValidateError(err):
|
|
||||||
return (isinstance(err, error.Error) and
|
|
||||||
err.code is errors.FILE_IN_BLOCK and
|
|
||||||
err.token.string == '}')
|
|
||||||
|
|
||||||
mock_error_handler.HandleFile('foo.js', mox.IsA(tokens.Token))
|
|
||||||
mock_error_handler.HandleError(mox.Func(ValidateError))
|
|
||||||
mock_error_handler.HandleError(mox.IsA(error.Error))
|
|
||||||
mock_error_handler.FinishFile()
|
|
||||||
|
|
||||||
self.mox.ReplayAll()
|
|
||||||
|
|
||||||
source = StringIO.StringIO(_BAD_TOKENIZATION_SCRIPT)
|
|
||||||
runner.Run('foo.js', mock_error_handler, source)
|
|
||||||
|
|
||||||
self.mox.VerifyAll()
|
|
||||||
|
|
||||||
|
|
||||||
_BAD_TOKENIZATION_SCRIPT = """
|
|
||||||
function foo () {
|
|
||||||
var a = 3;
|
|
||||||
var b = 2;
|
|
||||||
return b + a; /* Comment not closed
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Tools to match goog.scope alias statements."""
|
|
||||||
|
|
||||||
# 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 tokenutil
|
|
||||||
from closure_linter.javascripttokens import JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def IsGoogScopeBlock(context):
|
|
||||||
"""Whether the given context is a goog.scope block.
|
|
||||||
|
|
||||||
This function only checks that the block is a function block inside
|
|
||||||
a goog.scope() call.
|
|
||||||
|
|
||||||
TODO(nnaze): Implement goog.scope checks that verify the call is
|
|
||||||
in the root context and contains only a single function literal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: An EcmaContext of type block.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the context is a goog.scope block.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if context.type != ecmametadatapass.EcmaContext.BLOCK:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not _IsFunctionLiteralBlock(context):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check that this function is contained by a group
|
|
||||||
# of form "goog.scope(...)".
|
|
||||||
parent = context.parent
|
|
||||||
if parent and parent.type is ecmametadatapass.EcmaContext.GROUP:
|
|
||||||
|
|
||||||
last_code_token = parent.start_token.metadata.last_code
|
|
||||||
|
|
||||||
if (last_code_token and
|
|
||||||
last_code_token.type is JavaScriptTokenType.IDENTIFIER and
|
|
||||||
last_code_token.string == 'goog.scope'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _IsFunctionLiteralBlock(block_context):
|
|
||||||
"""Check if a context is a function literal block (without parameters).
|
|
||||||
|
|
||||||
Example function literal block: 'function() {}'
|
|
||||||
|
|
||||||
Args:
|
|
||||||
block_context: An EcmaContext of type block.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether this context is a function literal block.
|
|
||||||
"""
|
|
||||||
|
|
||||||
previous_code_tokens_iter = itertools.ifilter(
|
|
||||||
lambda token: token not in JavaScriptTokenType.NON_CODE_TYPES,
|
|
||||||
reversed(block_context.start_token))
|
|
||||||
|
|
||||||
# Ignore the current token
|
|
||||||
next(previous_code_tokens_iter, None)
|
|
||||||
|
|
||||||
# Grab the previous three tokens and put them in correct order.
|
|
||||||
previous_code_tokens = list(itertools.islice(previous_code_tokens_iter, 3))
|
|
||||||
previous_code_tokens.reverse()
|
|
||||||
|
|
||||||
# There aren't three previous tokens.
|
|
||||||
if len(previous_code_tokens) is not 3:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check that the previous three code tokens are "function ()"
|
|
||||||
previous_code_token_types = [token.type for token in previous_code_tokens]
|
|
||||||
if (previous_code_token_types == [
|
|
||||||
JavaScriptTokenType.FUNCTION_DECLARATION,
|
|
||||||
JavaScriptTokenType.START_PARAMETERS,
|
|
||||||
JavaScriptTokenType.END_PARAMETERS]):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def IsInClosurizedNamespace(symbol, closurized_namespaces):
|
|
||||||
"""Match a goog.scope alias.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
symbol: An identifier like 'goog.events.Event'.
|
|
||||||
closurized_namespaces: Iterable of valid Closurized namespaces (strings).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if symbol is an identifier in a Closurized namespace, otherwise False.
|
|
||||||
"""
|
|
||||||
for ns in closurized_namespaces:
|
|
||||||
if symbol.startswith(ns + '.'):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _GetVarAssignmentTokens(context):
|
|
||||||
"""Returns the tokens from context if it is a var assignment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: An EcmaContext.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
If a var assignment, the tokens contained within it w/o the trailing
|
|
||||||
semicolon.
|
|
||||||
"""
|
|
||||||
if context.type != ecmametadatapass.EcmaContext.VAR:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Get the tokens in this statement.
|
|
||||||
if context.start_token and context.end_token:
|
|
||||||
statement_tokens = tokenutil.GetTokenRange(context.start_token,
|
|
||||||
context.end_token)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
# And now just those tokens that are actually code.
|
|
||||||
is_non_code_type = lambda t: t.type not in JavaScriptTokenType.NON_CODE_TYPES
|
|
||||||
code_tokens = filter(is_non_code_type, statement_tokens)
|
|
||||||
|
|
||||||
# Pop off the semicolon if present.
|
|
||||||
if code_tokens and code_tokens[-1].IsType(JavaScriptTokenType.SEMICOLON):
|
|
||||||
code_tokens.pop()
|
|
||||||
|
|
||||||
if len(code_tokens) < 4:
|
|
||||||
return
|
|
||||||
|
|
||||||
if (code_tokens[0].IsKeyword('var') and
|
|
||||||
code_tokens[1].IsType(JavaScriptTokenType.SIMPLE_LVALUE) and
|
|
||||||
code_tokens[2].IsOperator('=')):
|
|
||||||
return code_tokens
|
|
||||||
|
|
||||||
|
|
||||||
def MatchAlias(context):
|
|
||||||
"""Match an alias statement (some identifier assigned to a variable).
|
|
||||||
|
|
||||||
Example alias: var MyClass = proj.longNamespace.MyClass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: An EcmaContext of type EcmaContext.VAR.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
If a valid alias, returns a tuple of alias and symbol, otherwise None.
|
|
||||||
"""
|
|
||||||
code_tokens = _GetVarAssignmentTokens(context)
|
|
||||||
if code_tokens is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if all(tokenutil.IsIdentifierOrDot(t) for t in code_tokens[3:]):
|
|
||||||
# var Foo = bar.Foo;
|
|
||||||
alias, symbol = code_tokens[1], code_tokens[3]
|
|
||||||
# Mark both tokens as an alias definition to not count them as usages.
|
|
||||||
alias.metadata.is_alias_definition = True
|
|
||||||
symbol.metadata.is_alias_definition = True
|
|
||||||
return alias.string, tokenutil.GetIdentifierForToken(symbol)
|
|
||||||
|
|
||||||
|
|
||||||
def MatchModuleAlias(context):
|
|
||||||
"""Match an alias statement in a goog.module style import.
|
|
||||||
|
|
||||||
Example alias: var MyClass = goog.require('proj.longNamespace.MyClass').
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context: An EcmaContext.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
If a valid alias, returns a tuple of alias and symbol, otherwise None.
|
|
||||||
"""
|
|
||||||
code_tokens = _GetVarAssignmentTokens(context)
|
|
||||||
if code_tokens is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if(code_tokens[3].IsType(JavaScriptTokenType.IDENTIFIER) and
|
|
||||||
code_tokens[3].string == 'goog.require'):
|
|
||||||
# var Foo = goog.require('bar.Foo');
|
|
||||||
alias = code_tokens[1]
|
|
||||||
symbol = tokenutil.GetStringAfterToken(code_tokens[3])
|
|
||||||
if symbol:
|
|
||||||
alias.metadata.is_alias_definition = True
|
|
||||||
return alias.string, symbol
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the scopeutil module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import scopeutil
|
|
||||||
from closure_linter import testutil
|
|
||||||
|
|
||||||
|
|
||||||
def _FindContexts(start_token):
|
|
||||||
"""Depth first search of all contexts referenced by a token stream.
|
|
||||||
|
|
||||||
Includes contexts' parents, which might not be directly referenced
|
|
||||||
by any token in the stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: First token in the token stream.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
All contexts referenced by this token stream.
|
|
||||||
"""
|
|
||||||
|
|
||||||
seen_contexts = set()
|
|
||||||
|
|
||||||
# For each token, yield the context if we haven't seen it before.
|
|
||||||
for token in start_token:
|
|
||||||
|
|
||||||
token_context = token.metadata.context
|
|
||||||
contexts = [token_context]
|
|
||||||
|
|
||||||
# Also grab all the context's ancestors.
|
|
||||||
parent = token_context.parent
|
|
||||||
while parent:
|
|
||||||
contexts.append(parent)
|
|
||||||
parent = parent.parent
|
|
||||||
|
|
||||||
# Yield each of these contexts if we've not seen them.
|
|
||||||
for context in contexts:
|
|
||||||
if context not in seen_contexts:
|
|
||||||
yield context
|
|
||||||
|
|
||||||
seen_contexts.add(context)
|
|
||||||
|
|
||||||
|
|
||||||
def _FindFirstContextOfType(token, context_type):
|
|
||||||
"""Returns the first statement context."""
|
|
||||||
for context in _FindContexts(token):
|
|
||||||
if context.type == context_type:
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def _ParseAssignment(script):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(script)
|
|
||||||
statement = _FindFirstContextOfType(
|
|
||||||
start_token, ecmametadatapass.EcmaContext.VAR)
|
|
||||||
return statement
|
|
||||||
|
|
||||||
|
|
||||||
class StatementTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def assertAlias(self, expected_match, script):
|
|
||||||
statement = _ParseAssignment(script)
|
|
||||||
match = scopeutil.MatchAlias(statement)
|
|
||||||
self.assertEquals(expected_match, match)
|
|
||||||
|
|
||||||
def assertModuleAlias(self, expected_match, script):
|
|
||||||
statement = _ParseAssignment(script)
|
|
||||||
match = scopeutil.MatchModuleAlias(statement)
|
|
||||||
self.assertEquals(expected_match, match)
|
|
||||||
|
|
||||||
def testSimpleAliases(self):
|
|
||||||
self.assertAlias(
|
|
||||||
('foo', 'goog.foo'),
|
|
||||||
'var foo = goog.foo;')
|
|
||||||
|
|
||||||
self.assertAlias(
|
|
||||||
('foo', 'goog.foo'),
|
|
||||||
'var foo = goog.foo') # No semicolon
|
|
||||||
|
|
||||||
def testAliasWithComment(self):
|
|
||||||
self.assertAlias(
|
|
||||||
('Component', 'goog.ui.Component'),
|
|
||||||
'var Component = /* comment */ goog.ui.Component;')
|
|
||||||
|
|
||||||
def testMultilineAlias(self):
|
|
||||||
self.assertAlias(
|
|
||||||
('Component', 'goog.ui.Component'),
|
|
||||||
'var Component = \n goog.ui.\n Component;')
|
|
||||||
|
|
||||||
def testNonSymbolAliasVarStatements(self):
|
|
||||||
self.assertAlias(None, 'var foo = 3;')
|
|
||||||
self.assertAlias(None, 'var foo = function() {};')
|
|
||||||
self.assertAlias(None, 'var foo = bar ? baz : qux;')
|
|
||||||
|
|
||||||
def testModuleAlias(self):
|
|
||||||
self.assertModuleAlias(
|
|
||||||
('foo', 'goog.foo'),
|
|
||||||
'var foo = goog.require("goog.foo");')
|
|
||||||
self.assertModuleAlias(
|
|
||||||
None,
|
|
||||||
'var foo = goog.require(notastring);')
|
|
||||||
|
|
||||||
|
|
||||||
class ScopeBlockTest(googletest.TestCase):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _GetBlocks(source):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(source)
|
|
||||||
for context in _FindContexts(start_token):
|
|
||||||
if context.type is ecmametadatapass.EcmaContext.BLOCK:
|
|
||||||
yield context
|
|
||||||
|
|
||||||
def assertNoBlocks(self, script):
|
|
||||||
blocks = list(self._GetBlocks(script))
|
|
||||||
self.assertEquals([], blocks)
|
|
||||||
|
|
||||||
def testNotBlocks(self):
|
|
||||||
# Ensure these are not considered blocks.
|
|
||||||
self.assertNoBlocks('goog.scope(if{});')
|
|
||||||
self.assertNoBlocks('goog.scope(for{});')
|
|
||||||
self.assertNoBlocks('goog.scope(switch{});')
|
|
||||||
self.assertNoBlocks('goog.scope(function foo{});')
|
|
||||||
|
|
||||||
def testNonScopeBlocks(self):
|
|
||||||
|
|
||||||
blocks = list(self._GetBlocks('goog.scope(try{});'))
|
|
||||||
self.assertEquals(1, len(blocks))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
|
|
||||||
blocks = list(self._GetBlocks('goog.scope(function(a,b){});'))
|
|
||||||
self.assertEquals(1, len(blocks))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
|
|
||||||
blocks = list(self._GetBlocks('goog.scope(try{} catch(){});'))
|
|
||||||
# Two blocks: try and catch.
|
|
||||||
self.assertEquals(2, len(blocks))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
|
|
||||||
blocks = list(self._GetBlocks('goog.scope(try{} catch(){} finally {});'))
|
|
||||||
self.assertEquals(3, len(blocks))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop()))
|
|
||||||
|
|
||||||
|
|
||||||
class AliasTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)
|
|
||||||
|
|
||||||
def testMatchAliasStatement(self):
|
|
||||||
matches = set()
|
|
||||||
for context in _FindContexts(self.start_token):
|
|
||||||
match = scopeutil.MatchAlias(context)
|
|
||||||
if match:
|
|
||||||
matches.add(match)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
set([('bar', 'baz'),
|
|
||||||
('foo', 'this.foo_'),
|
|
||||||
('Component', 'goog.ui.Component'),
|
|
||||||
('MyClass', 'myproject.foo.MyClass'),
|
|
||||||
('NonClosurizedClass', 'aaa.bbb.NonClosurizedClass')]),
|
|
||||||
matches)
|
|
||||||
|
|
||||||
def testMatchAliasStatement_withClosurizedNamespaces(self):
|
|
||||||
|
|
||||||
closurized_namepaces = frozenset(['goog', 'myproject'])
|
|
||||||
|
|
||||||
matches = set()
|
|
||||||
for context in _FindContexts(self.start_token):
|
|
||||||
match = scopeutil.MatchAlias(context)
|
|
||||||
if match:
|
|
||||||
unused_alias, symbol = match
|
|
||||||
if scopeutil.IsInClosurizedNamespace(symbol, closurized_namepaces):
|
|
||||||
matches.add(match)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
set([('MyClass', 'myproject.foo.MyClass'),
|
|
||||||
('Component', 'goog.ui.Component')]),
|
|
||||||
matches)
|
|
||||||
|
|
||||||
_TEST_SCRIPT = """
|
|
||||||
goog.scope(function() {
|
|
||||||
var Component = goog.ui.Component; // scope alias
|
|
||||||
var MyClass = myproject.foo.MyClass; // scope alias
|
|
||||||
|
|
||||||
// Scope alias of non-Closurized namespace.
|
|
||||||
var NonClosurizedClass = aaa.bbb.NonClosurizedClass;
|
|
||||||
|
|
||||||
var foo = this.foo_; // non-scope object property alias
|
|
||||||
var bar = baz; // variable alias
|
|
||||||
|
|
||||||
var component = new Component();
|
|
||||||
});
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,123 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the statetracker module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import statetracker
|
|
||||||
from closure_linter import testutil
|
|
||||||
|
|
||||||
|
|
||||||
class _FakeDocFlag(object):
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '@%s %s' % (self.flag_type, self.name)
|
|
||||||
|
|
||||||
|
|
||||||
class IdentifierTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testJustIdentifier(self):
|
|
||||||
a = javascripttokens.JavaScriptToken(
|
|
||||||
'abc', javascripttokens.JavaScriptTokenType.IDENTIFIER, 'abc', 1)
|
|
||||||
|
|
||||||
st = statetracker.StateTracker()
|
|
||||||
st.HandleToken(a, None)
|
|
||||||
|
|
||||||
|
|
||||||
class DocCommentTest(googletest.TestCase):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _MakeDocFlagFake(flag_type, name=None):
|
|
||||||
flag = _FakeDocFlag()
|
|
||||||
flag.flag_type = flag_type
|
|
||||||
flag.name = name
|
|
||||||
return flag
|
|
||||||
|
|
||||||
def testDocFlags(self):
|
|
||||||
comment = statetracker.DocComment(None)
|
|
||||||
|
|
||||||
a = self._MakeDocFlagFake('param', 'foo')
|
|
||||||
comment.AddFlag(a)
|
|
||||||
|
|
||||||
b = self._MakeDocFlagFake('param', '')
|
|
||||||
comment.AddFlag(b)
|
|
||||||
|
|
||||||
c = self._MakeDocFlagFake('param', 'bar')
|
|
||||||
comment.AddFlag(c)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
['foo', 'bar'],
|
|
||||||
comment.ordered_params)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
[a, b, c],
|
|
||||||
comment.GetDocFlags())
|
|
||||||
|
|
||||||
def testInvalidate(self):
|
|
||||||
comment = statetracker.DocComment(None)
|
|
||||||
|
|
||||||
self.assertFalse(comment.invalidated)
|
|
||||||
self.assertFalse(comment.IsInvalidated())
|
|
||||||
|
|
||||||
comment.Invalidate()
|
|
||||||
|
|
||||||
self.assertTrue(comment.invalidated)
|
|
||||||
self.assertTrue(comment.IsInvalidated())
|
|
||||||
|
|
||||||
def testSuppressionOnly(self):
|
|
||||||
comment = statetracker.DocComment(None)
|
|
||||||
|
|
||||||
self.assertFalse(comment.SuppressionOnly())
|
|
||||||
comment.AddFlag(self._MakeDocFlagFake('suppress'))
|
|
||||||
self.assertTrue(comment.SuppressionOnly())
|
|
||||||
comment.AddFlag(self._MakeDocFlagFake('foo'))
|
|
||||||
self.assertFalse(comment.SuppressionOnly())
|
|
||||||
|
|
||||||
def testRepr(self):
|
|
||||||
comment = statetracker.DocComment(None)
|
|
||||||
comment.AddFlag(self._MakeDocFlagFake('param', 'foo'))
|
|
||||||
comment.AddFlag(self._MakeDocFlagFake('param', 'bar'))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'<DocComment: [\'foo\', \'bar\'], [@param foo, @param bar]>',
|
|
||||||
repr(comment))
|
|
||||||
|
|
||||||
def testDocFlagParam(self):
|
|
||||||
comment = self._ParseComment("""
|
|
||||||
/**
|
|
||||||
* @param {string} [name] Name of customer.
|
|
||||||
*/""")
|
|
||||||
flag = comment.GetFlag('param')
|
|
||||||
self.assertEquals('string', flag.type)
|
|
||||||
self.assertEquals('string', flag.jstype.ToString())
|
|
||||||
self.assertEquals('[name]', flag.name)
|
|
||||||
|
|
||||||
def _ParseComment(self, script):
|
|
||||||
"""Parse a script that contains one comment and return it."""
|
|
||||||
_, comments = testutil.ParseFunctionsAndComments(script)
|
|
||||||
self.assertEquals(1, len(comments))
|
|
||||||
return comments[0]
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2013 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.
|
|
||||||
|
|
||||||
"""Tests for gjslint --strict.
|
|
||||||
|
|
||||||
Tests errors that can be thrown by gjslint when in strict mode.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
|
|
||||||
|
|
||||||
class StrictTest(unittest.TestCase):
|
|
||||||
"""Tests scenarios where strict generates warnings."""
|
|
||||||
|
|
||||||
def testUnclosedString(self):
|
|
||||||
"""Tests warnings are reported when nothing is disabled.
|
|
||||||
|
|
||||||
b/11450054.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'bug = function() {',
|
|
||||||
' (\'foo\'\');',
|
|
||||||
'};',
|
|
||||||
'',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [errors.FILE_DOES_NOT_PARSE, errors.MULTI_LINE_STRING,
|
|
||||||
errors.FILE_IN_BLOCK]
|
|
||||||
self._AssertErrors(original, expected)
|
|
||||||
|
|
||||||
def _AssertErrors(self, original, expected_errors):
|
|
||||||
"""Asserts that the error fixer corrects original to expected."""
|
|
||||||
|
|
||||||
# Trap gjslint's output parse it to get messages added.
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
runner.Run('testing.js', error_accumulator, source=original)
|
|
||||||
error_nums = [e.code for e in error_accumulator.GetErrors()]
|
|
||||||
|
|
||||||
error_nums.sort()
|
|
||||||
expected_errors.sort()
|
|
||||||
self.assertListEqual(error_nums, expected_errors)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Utility functions for testing gjslint components."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokenizer
|
|
||||||
|
|
||||||
|
|
||||||
def TokenizeSource(source):
|
|
||||||
"""Convert a source into a string of tokens.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: A source file as a string or file-like object (iterates lines).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token of the resulting token stream.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(source, basestring):
|
|
||||||
source = StringIO.StringIO(source)
|
|
||||||
|
|
||||||
tokenizer = javascripttokenizer.JavaScriptTokenizer()
|
|
||||||
return tokenizer.TokenizeFile(source)
|
|
||||||
|
|
||||||
|
|
||||||
def TokenizeSourceAndRunEcmaPass(source):
|
|
||||||
"""Tokenize a source and run the EcmaMetaDataPass on it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: A source file as a string or file-like object (iterates lines).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token of the resulting token stream.
|
|
||||||
"""
|
|
||||||
start_token = TokenizeSource(source)
|
|
||||||
ecma_pass = ecmametadatapass.EcmaMetaDataPass()
|
|
||||||
ecma_pass.Process(start_token)
|
|
||||||
return start_token
|
|
||||||
|
|
||||||
|
|
||||||
def ParseFunctionsAndComments(source, error_handler=None):
|
|
||||||
"""Run the tokenizer and tracker and return comments and functions found.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: A source file as a string or file-like object (iterates lines).
|
|
||||||
error_handler: An error handler.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The functions and comments as a tuple.
|
|
||||||
"""
|
|
||||||
start_token = TokenizeSourceAndRunEcmaPass(source)
|
|
||||||
|
|
||||||
tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
if error_handler is not None:
|
|
||||||
tracker.DocFlagPass(start_token, error_handler)
|
|
||||||
|
|
||||||
functions = []
|
|
||||||
comments = []
|
|
||||||
for token in start_token:
|
|
||||||
tracker.HandleToken(token, tracker.GetLastNonSpaceToken())
|
|
||||||
|
|
||||||
function = tracker.GetFunction()
|
|
||||||
if function and function not in functions:
|
|
||||||
functions.append(function)
|
|
||||||
|
|
||||||
comment = tracker.GetDocComment()
|
|
||||||
if comment and comment not in comments:
|
|
||||||
comments.append(comment)
|
|
||||||
|
|
||||||
tracker.HandleAfterToken(token)
|
|
||||||
|
|
||||||
return functions, comments
|
|
||||||
@@ -1,697 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Token utility functions."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
from closure_linter.javascripttokens import JavaScriptToken
|
|
||||||
from closure_linter.javascripttokens import JavaScriptTokenType
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = tokens.TokenType
|
|
||||||
|
|
||||||
|
|
||||||
def GetFirstTokenInSameLine(token):
|
|
||||||
"""Returns the first token in the same line as token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Any token in the line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token in the same line as token.
|
|
||||||
"""
|
|
||||||
while not token.IsFirstInLine():
|
|
||||||
token = token.previous
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def GetFirstTokenInPreviousLine(token):
|
|
||||||
"""Returns the first token in the previous line as token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Any token in the line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token in the previous line as token, or None if token is on the
|
|
||||||
first line.
|
|
||||||
"""
|
|
||||||
first_in_line = GetFirstTokenInSameLine(token)
|
|
||||||
if first_in_line.previous:
|
|
||||||
return GetFirstTokenInSameLine(first_in_line.previous)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def GetLastTokenInSameLine(token):
|
|
||||||
"""Returns the last token in the same line as token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Any token in the line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The last token in the same line as token.
|
|
||||||
"""
|
|
||||||
while not token.IsLastInLine():
|
|
||||||
token = token.next
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
def GetAllTokensInSameLine(token):
|
|
||||||
"""Returns all tokens in the same line as the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Any token in the line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
All tokens on the same line as the given token.
|
|
||||||
"""
|
|
||||||
first_token = GetFirstTokenInSameLine(token)
|
|
||||||
last_token = GetLastTokenInSameLine(token)
|
|
||||||
|
|
||||||
tokens_in_line = []
|
|
||||||
while first_token != last_token:
|
|
||||||
tokens_in_line.append(first_token)
|
|
||||||
first_token = first_token.next
|
|
||||||
tokens_in_line.append(last_token)
|
|
||||||
|
|
||||||
return tokens_in_line
|
|
||||||
|
|
||||||
|
|
||||||
def CustomSearch(start_token, func, end_func=None, distance=None,
|
|
||||||
reverse=False):
|
|
||||||
"""Returns the first token where func is True within distance of this token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The token to start searching from
|
|
||||||
func: The function to call to test a token for applicability
|
|
||||||
end_func: The function to call to test a token to determine whether to abort
|
|
||||||
the search.
|
|
||||||
distance: The number of tokens to look through before failing search. Must
|
|
||||||
be positive. If unspecified, will search until the end of the token
|
|
||||||
chain
|
|
||||||
reverse: When true, search the tokens before this one instead of the tokens
|
|
||||||
after it
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token matching func within distance of this token, or None if no
|
|
||||||
such token is found.
|
|
||||||
"""
|
|
||||||
token = start_token
|
|
||||||
if reverse:
|
|
||||||
while token and (distance is None or distance > 0):
|
|
||||||
previous = token.previous
|
|
||||||
if previous:
|
|
||||||
if func(previous):
|
|
||||||
return previous
|
|
||||||
if end_func and end_func(previous):
|
|
||||||
return None
|
|
||||||
|
|
||||||
token = previous
|
|
||||||
if distance is not None:
|
|
||||||
distance -= 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
while token and (distance is None or distance > 0):
|
|
||||||
next_token = token.next
|
|
||||||
if next_token:
|
|
||||||
if func(next_token):
|
|
||||||
return next_token
|
|
||||||
if end_func and end_func(next_token):
|
|
||||||
return None
|
|
||||||
|
|
||||||
token = next_token
|
|
||||||
if distance is not None:
|
|
||||||
distance -= 1
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def Search(start_token, token_types, distance=None, reverse=False):
|
|
||||||
"""Returns the first token of type in token_types within distance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The token to start searching from
|
|
||||||
token_types: The allowable types of the token being searched for
|
|
||||||
distance: The number of tokens to look through before failing search. Must
|
|
||||||
be positive. If unspecified, will search until the end of the token
|
|
||||||
chain
|
|
||||||
reverse: When true, search the tokens before this one instead of the tokens
|
|
||||||
after it
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token of any type in token_types within distance of this token, or
|
|
||||||
None if no such token is found.
|
|
||||||
"""
|
|
||||||
return CustomSearch(start_token, lambda token: token.IsAnyType(token_types),
|
|
||||||
None, distance, reverse)
|
|
||||||
|
|
||||||
|
|
||||||
def SearchExcept(start_token, token_types, distance=None, reverse=False):
|
|
||||||
"""Returns the first token not of any type in token_types within distance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The token to start searching from
|
|
||||||
token_types: The unallowable types of the token being searched for
|
|
||||||
distance: The number of tokens to look through before failing search. Must
|
|
||||||
be positive. If unspecified, will search until the end of the token
|
|
||||||
chain
|
|
||||||
reverse: When true, search the tokens before this one instead of the tokens
|
|
||||||
after it
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token of any type in token_types within distance of this token, or
|
|
||||||
None if no such token is found.
|
|
||||||
"""
|
|
||||||
return CustomSearch(start_token,
|
|
||||||
lambda token: not token.IsAnyType(token_types),
|
|
||||||
None, distance, reverse)
|
|
||||||
|
|
||||||
|
|
||||||
def SearchUntil(start_token, token_types, end_types, distance=None,
|
|
||||||
reverse=False):
|
|
||||||
"""Returns the first token of type in token_types before a token of end_type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The token to start searching from.
|
|
||||||
token_types: The allowable types of the token being searched for.
|
|
||||||
end_types: Types of tokens to abort search if we find.
|
|
||||||
distance: The number of tokens to look through before failing search. Must
|
|
||||||
be positive. If unspecified, will search until the end of the token
|
|
||||||
chain
|
|
||||||
reverse: When true, search the tokens before this one instead of the tokens
|
|
||||||
after it
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token of any type in token_types within distance of this token
|
|
||||||
before any tokens of type in end_type, or None if no such token is found.
|
|
||||||
"""
|
|
||||||
return CustomSearch(start_token, lambda token: token.IsAnyType(token_types),
|
|
||||||
lambda token: token.IsAnyType(end_types),
|
|
||||||
distance, reverse)
|
|
||||||
|
|
||||||
|
|
||||||
def DeleteToken(token):
|
|
||||||
"""Deletes the given token from the linked list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to delete
|
|
||||||
"""
|
|
||||||
# When deleting a token, we do not update the deleted token itself to make
|
|
||||||
# sure the previous and next pointers are still pointing to tokens which are
|
|
||||||
# not deleted. Also it is very hard to keep track of all previously deleted
|
|
||||||
# tokens to update them when their pointers become invalid. So we add this
|
|
||||||
# flag that any token linked list iteration logic can skip deleted node safely
|
|
||||||
# when its current token is deleted.
|
|
||||||
token.is_deleted = True
|
|
||||||
if token.previous:
|
|
||||||
token.previous.next = token.next
|
|
||||||
|
|
||||||
if token.next:
|
|
||||||
token.next.previous = token.previous
|
|
||||||
|
|
||||||
following_token = token.next
|
|
||||||
while following_token and following_token.metadata.last_code == token:
|
|
||||||
following_token.metadata.last_code = token.metadata.last_code
|
|
||||||
following_token = following_token.next
|
|
||||||
|
|
||||||
|
|
||||||
def DeleteTokens(token, token_count):
|
|
||||||
"""Deletes the given number of tokens starting with the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to start deleting at.
|
|
||||||
token_count: The total number of tokens to delete.
|
|
||||||
"""
|
|
||||||
for i in xrange(1, token_count):
|
|
||||||
DeleteToken(token.next)
|
|
||||||
DeleteToken(token)
|
|
||||||
|
|
||||||
|
|
||||||
def InsertTokenBefore(new_token, token):
|
|
||||||
"""Insert new_token before token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_token: A token to be added to the stream
|
|
||||||
token: A token already in the stream
|
|
||||||
"""
|
|
||||||
new_token.next = token
|
|
||||||
new_token.previous = token.previous
|
|
||||||
|
|
||||||
new_token.metadata = copy.copy(token.metadata)
|
|
||||||
|
|
||||||
if new_token.IsCode():
|
|
||||||
old_last_code = token.metadata.last_code
|
|
||||||
following_token = token
|
|
||||||
while (following_token and
|
|
||||||
following_token.metadata.last_code == old_last_code):
|
|
||||||
following_token.metadata.last_code = new_token
|
|
||||||
following_token = following_token.next
|
|
||||||
|
|
||||||
token.previous = new_token
|
|
||||||
if new_token.previous:
|
|
||||||
new_token.previous.next = new_token
|
|
||||||
|
|
||||||
if new_token.start_index is None:
|
|
||||||
if new_token.line_number == token.line_number:
|
|
||||||
new_token.start_index = token.start_index
|
|
||||||
else:
|
|
||||||
previous_token = new_token.previous
|
|
||||||
if previous_token:
|
|
||||||
new_token.start_index = (previous_token.start_index +
|
|
||||||
len(previous_token.string))
|
|
||||||
else:
|
|
||||||
new_token.start_index = 0
|
|
||||||
|
|
||||||
iterator = new_token.next
|
|
||||||
while iterator and iterator.line_number == new_token.line_number:
|
|
||||||
iterator.start_index += len(new_token.string)
|
|
||||||
iterator = iterator.next
|
|
||||||
|
|
||||||
|
|
||||||
def InsertTokenAfter(new_token, token):
|
|
||||||
"""Insert new_token after token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_token: A token to be added to the stream
|
|
||||||
token: A token already in the stream
|
|
||||||
"""
|
|
||||||
new_token.previous = token
|
|
||||||
new_token.next = token.next
|
|
||||||
|
|
||||||
new_token.metadata = copy.copy(token.metadata)
|
|
||||||
|
|
||||||
if token.IsCode():
|
|
||||||
new_token.metadata.last_code = token
|
|
||||||
|
|
||||||
if new_token.IsCode():
|
|
||||||
following_token = token.next
|
|
||||||
while following_token and following_token.metadata.last_code == token:
|
|
||||||
following_token.metadata.last_code = new_token
|
|
||||||
following_token = following_token.next
|
|
||||||
|
|
||||||
token.next = new_token
|
|
||||||
if new_token.next:
|
|
||||||
new_token.next.previous = new_token
|
|
||||||
|
|
||||||
if new_token.start_index is None:
|
|
||||||
if new_token.line_number == token.line_number:
|
|
||||||
new_token.start_index = token.start_index + len(token.string)
|
|
||||||
else:
|
|
||||||
new_token.start_index = 0
|
|
||||||
|
|
||||||
iterator = new_token.next
|
|
||||||
while iterator and iterator.line_number == new_token.line_number:
|
|
||||||
iterator.start_index += len(new_token.string)
|
|
||||||
iterator = iterator.next
|
|
||||||
|
|
||||||
|
|
||||||
def InsertTokensAfter(new_tokens, token):
|
|
||||||
"""Insert multiple tokens after token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
new_tokens: An array of tokens to be added to the stream
|
|
||||||
token: A token already in the stream
|
|
||||||
"""
|
|
||||||
# TODO(user): It would be nicer to have InsertTokenAfter defer to here
|
|
||||||
# instead of vice-versa.
|
|
||||||
current_token = token
|
|
||||||
for new_token in new_tokens:
|
|
||||||
InsertTokenAfter(new_token, current_token)
|
|
||||||
current_token = new_token
|
|
||||||
|
|
||||||
|
|
||||||
def InsertSpaceTokenAfter(token):
|
|
||||||
"""Inserts a space token after the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to insert a space token after
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A single space token
|
|
||||||
"""
|
|
||||||
space_token = JavaScriptToken(' ', Type.WHITESPACE, token.line,
|
|
||||||
token.line_number)
|
|
||||||
InsertTokenAfter(space_token, token)
|
|
||||||
|
|
||||||
|
|
||||||
def InsertBlankLineAfter(token):
|
|
||||||
"""Inserts a blank line after the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to insert a blank line after
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A single space token
|
|
||||||
"""
|
|
||||||
blank_token = JavaScriptToken('', Type.BLANK_LINE, '',
|
|
||||||
token.line_number + 1)
|
|
||||||
InsertLineAfter(token, [blank_token])
|
|
||||||
|
|
||||||
|
|
||||||
def InsertLineAfter(token, new_tokens):
|
|
||||||
"""Inserts a new line consisting of new_tokens after the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to insert after.
|
|
||||||
new_tokens: The tokens that will make up the new line.
|
|
||||||
"""
|
|
||||||
insert_location = token
|
|
||||||
for new_token in new_tokens:
|
|
||||||
InsertTokenAfter(new_token, insert_location)
|
|
||||||
insert_location = new_token
|
|
||||||
|
|
||||||
# Update all subsequent line numbers.
|
|
||||||
next_token = new_tokens[-1].next
|
|
||||||
while next_token:
|
|
||||||
next_token.line_number += 1
|
|
||||||
next_token = next_token.next
|
|
||||||
|
|
||||||
|
|
||||||
def SplitToken(token, position):
|
|
||||||
"""Splits the token into two tokens at position.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to split
|
|
||||||
position: The position to split at. Will be the beginning of second token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The new second token.
|
|
||||||
"""
|
|
||||||
new_string = token.string[position:]
|
|
||||||
token.string = token.string[:position]
|
|
||||||
|
|
||||||
new_token = JavaScriptToken(new_string, token.type, token.line,
|
|
||||||
token.line_number)
|
|
||||||
InsertTokenAfter(new_token, token)
|
|
||||||
|
|
||||||
return new_token
|
|
||||||
|
|
||||||
|
|
||||||
def Compare(token1, token2):
|
|
||||||
"""Compares two tokens and determines their relative order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token1: The first token to compare.
|
|
||||||
token2: The second token to compare.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A negative integer, zero, or a positive integer as the first token is
|
|
||||||
before, equal, or after the second in the token stream.
|
|
||||||
"""
|
|
||||||
if token2.line_number != token1.line_number:
|
|
||||||
return token1.line_number - token2.line_number
|
|
||||||
else:
|
|
||||||
return token1.start_index - token2.start_index
|
|
||||||
|
|
||||||
|
|
||||||
def GoogScopeOrNoneFromStartBlock(token):
|
|
||||||
"""Determines if the given START_BLOCK is part of a goog.scope statement.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token of type START_BLOCK.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The goog.scope function call token, or None if such call doesn't exist.
|
|
||||||
"""
|
|
||||||
if token.type != JavaScriptTokenType.START_BLOCK:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Search for a goog.scope statement, which will be 5 tokens before the
|
|
||||||
# block. Illustration of the tokens found prior to the start block:
|
|
||||||
# goog.scope(function() {
|
|
||||||
# 5 4 3 21 ^
|
|
||||||
|
|
||||||
maybe_goog_scope = token
|
|
||||||
for unused_i in xrange(5):
|
|
||||||
maybe_goog_scope = (maybe_goog_scope.previous if maybe_goog_scope and
|
|
||||||
maybe_goog_scope.previous else None)
|
|
||||||
if maybe_goog_scope and maybe_goog_scope.string == 'goog.scope':
|
|
||||||
return maybe_goog_scope
|
|
||||||
|
|
||||||
|
|
||||||
def GetTokenRange(start_token, end_token):
|
|
||||||
"""Returns a list of tokens between the two given, inclusive.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: Start token in the range.
|
|
||||||
end_token: End token in the range.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of tokens, in order, from start_token to end_token (including start
|
|
||||||
and end). Returns none if the tokens do not describe a valid range.
|
|
||||||
"""
|
|
||||||
|
|
||||||
token_range = []
|
|
||||||
token = start_token
|
|
||||||
|
|
||||||
while token:
|
|
||||||
token_range.append(token)
|
|
||||||
|
|
||||||
if token == end_token:
|
|
||||||
return token_range
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
|
|
||||||
def TokensToString(token_iterable):
|
|
||||||
"""Convert a number of tokens into a string.
|
|
||||||
|
|
||||||
Newlines will be inserted whenever the line_number of two neighboring
|
|
||||||
strings differ.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_iterable: The tokens to turn to a string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A string representation of the given tokens.
|
|
||||||
"""
|
|
||||||
|
|
||||||
buf = StringIO.StringIO()
|
|
||||||
token_list = list(token_iterable)
|
|
||||||
if not token_list:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
line_number = token_list[0].line_number
|
|
||||||
|
|
||||||
for token in token_list:
|
|
||||||
|
|
||||||
while line_number < token.line_number:
|
|
||||||
line_number += 1
|
|
||||||
buf.write('\n')
|
|
||||||
|
|
||||||
if line_number > token.line_number:
|
|
||||||
line_number = token.line_number
|
|
||||||
buf.write('\n')
|
|
||||||
|
|
||||||
buf.write(token.string)
|
|
||||||
|
|
||||||
return buf.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
def GetPreviousCodeToken(token):
|
|
||||||
"""Returns the code token before the specified token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The code token before the specified token or None if no such token
|
|
||||||
exists.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return CustomSearch(
|
|
||||||
token,
|
|
||||||
lambda t: t and t.type not in JavaScriptTokenType.NON_CODE_TYPES,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
|
|
||||||
def GetNextCodeToken(token):
|
|
||||||
"""Returns the next code token after the specified token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The next code token after the specified token or None if no such token
|
|
||||||
exists.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return CustomSearch(
|
|
||||||
token,
|
|
||||||
lambda t: t and t.type not in JavaScriptTokenType.NON_CODE_TYPES,
|
|
||||||
reverse=False)
|
|
||||||
|
|
||||||
|
|
||||||
def GetIdentifierStart(token):
|
|
||||||
"""Returns the first token in an identifier.
|
|
||||||
|
|
||||||
Given a token which is part of an identifier, returns the token at the start
|
|
||||||
of the identifier.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A token which is part of an identifier.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The token at the start of the identifier or None if the identifier was not
|
|
||||||
of the form 'a.b.c' (e.g. "['a']['b'].c").
|
|
||||||
"""
|
|
||||||
|
|
||||||
start_token = token
|
|
||||||
previous_code_token = GetPreviousCodeToken(token)
|
|
||||||
|
|
||||||
while (previous_code_token and (
|
|
||||||
previous_code_token.IsType(JavaScriptTokenType.IDENTIFIER) or
|
|
||||||
IsDot(previous_code_token))):
|
|
||||||
start_token = previous_code_token
|
|
||||||
previous_code_token = GetPreviousCodeToken(previous_code_token)
|
|
||||||
|
|
||||||
if IsDot(start_token):
|
|
||||||
return None
|
|
||||||
|
|
||||||
return start_token
|
|
||||||
|
|
||||||
|
|
||||||
def GetIdentifierForToken(token):
|
|
||||||
"""Get the symbol specified by a token.
|
|
||||||
|
|
||||||
Given a token, this function additionally concatenates any parts of an
|
|
||||||
identifying symbol being identified that are split by whitespace or a
|
|
||||||
newline.
|
|
||||||
|
|
||||||
The function will return None if the token is not the first token of an
|
|
||||||
identifier.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token of a symbol.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The whole symbol, as a string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# 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(JavaScriptTokenType.IDENTIFIER) or
|
|
||||||
IsDot(prev_token)):
|
|
||||||
return None
|
|
||||||
|
|
||||||
if (prev_token.IsType(tokens.TokenType.WHITESPACE) or
|
|
||||||
prev_token.IsAnyType(JavaScriptTokenType.COMMENT_TYPES)):
|
|
||||||
prev_token = prev_token.previous
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# A "function foo()" declaration.
|
|
||||||
if token.type is JavaScriptTokenType.FUNCTION_NAME:
|
|
||||||
return token.string
|
|
||||||
|
|
||||||
# A "var foo" declaration (if the previous token is 'var')
|
|
||||||
previous_code_token = GetPreviousCodeToken(token)
|
|
||||||
|
|
||||||
if previous_code_token and previous_code_token.IsKeyword('var'):
|
|
||||||
return token.string
|
|
||||||
|
|
||||||
# Otherwise, this is potentially a namespaced (goog.foo.bar) identifier that
|
|
||||||
# could span multiple lines or be broken up by whitespace. We need
|
|
||||||
# to concatenate.
|
|
||||||
identifier_types = set([
|
|
||||||
JavaScriptTokenType.IDENTIFIER,
|
|
||||||
JavaScriptTokenType.SIMPLE_LVALUE
|
|
||||||
])
|
|
||||||
|
|
||||||
assert token.type in identifier_types
|
|
||||||
|
|
||||||
# Start with the first token
|
|
||||||
symbol_tokens = [token]
|
|
||||||
|
|
||||||
if token.next:
|
|
||||||
for t in token.next:
|
|
||||||
last_symbol_token = symbol_tokens[-1]
|
|
||||||
|
|
||||||
# A dot is part of the previous symbol.
|
|
||||||
if IsDot(t):
|
|
||||||
symbol_tokens.append(t)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# An identifier is part of the previous symbol if the previous one was a
|
|
||||||
# dot.
|
|
||||||
if t.type in identifier_types:
|
|
||||||
if IsDot(last_symbol_token):
|
|
||||||
symbol_tokens.append(t)
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Skip any whitespace
|
|
||||||
if t.type in JavaScriptTokenType.NON_CODE_TYPES:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# This is the end of the identifier. Stop iterating.
|
|
||||||
break
|
|
||||||
|
|
||||||
if symbol_tokens:
|
|
||||||
return ''.join([t.string for t in symbol_tokens])
|
|
||||||
|
|
||||||
|
|
||||||
def GetStringAfterToken(token):
|
|
||||||
"""Get string after token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: Search will be done after this token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
String if found after token else None (empty string will also
|
|
||||||
return None).
|
|
||||||
|
|
||||||
Search until end of string as in case of empty string Type.STRING_TEXT is not
|
|
||||||
present/found and don't want to return next string.
|
|
||||||
E.g.
|
|
||||||
a = '';
|
|
||||||
b = 'test';
|
|
||||||
When searching for string after 'a' if search is not limited by end of string
|
|
||||||
then it will return 'test' which is not desirable as there is a empty string
|
|
||||||
before that.
|
|
||||||
|
|
||||||
This will return None for cases where string is empty or no string found
|
|
||||||
as in both cases there is no Type.STRING_TEXT.
|
|
||||||
"""
|
|
||||||
string_token = SearchUntil(token, JavaScriptTokenType.STRING_TEXT,
|
|
||||||
[JavaScriptTokenType.SINGLE_QUOTE_STRING_END,
|
|
||||||
JavaScriptTokenType.DOUBLE_QUOTE_STRING_END])
|
|
||||||
if string_token:
|
|
||||||
return string_token.string
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def IsDot(token):
|
|
||||||
"""Whether the token represents a "dot" operator (foo.bar)."""
|
|
||||||
return token.type is JavaScriptTokenType.OPERATOR and token.string == '.'
|
|
||||||
|
|
||||||
|
|
||||||
def IsIdentifierOrDot(token):
|
|
||||||
"""Whether the token is either an identifier or a '.'."""
|
|
||||||
return (token.type in [JavaScriptTokenType.IDENTIFIER,
|
|
||||||
JavaScriptTokenType.SIMPLE_LVALUE] or
|
|
||||||
IsDot(token))
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the scopeutil module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
|
|
||||||
class FakeToken(object):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TokenUtilTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testGetTokenRange(self):
|
|
||||||
|
|
||||||
a = FakeToken()
|
|
||||||
b = FakeToken()
|
|
||||||
c = FakeToken()
|
|
||||||
d = FakeToken()
|
|
||||||
e = FakeToken()
|
|
||||||
|
|
||||||
a.next = b
|
|
||||||
b.next = c
|
|
||||||
c.next = d
|
|
||||||
|
|
||||||
self.assertEquals([a, b, c, d], tokenutil.GetTokenRange(a, d))
|
|
||||||
|
|
||||||
# This is an error as e does not come after a in the token chain.
|
|
||||||
self.assertRaises(Exception, lambda: tokenutil.GetTokenRange(a, e))
|
|
||||||
|
|
||||||
def testTokensToString(self):
|
|
||||||
|
|
||||||
a = FakeToken()
|
|
||||||
b = FakeToken()
|
|
||||||
c = FakeToken()
|
|
||||||
d = FakeToken()
|
|
||||||
e = FakeToken()
|
|
||||||
|
|
||||||
a.string = 'aaa'
|
|
||||||
b.string = 'bbb'
|
|
||||||
c.string = 'ccc'
|
|
||||||
d.string = 'ddd'
|
|
||||||
e.string = 'eee'
|
|
||||||
|
|
||||||
a.line_number = 5
|
|
||||||
b.line_number = 6
|
|
||||||
c.line_number = 6
|
|
||||||
d.line_number = 10
|
|
||||||
e.line_number = 11
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'aaa\nbbbccc\n\n\n\nddd\neee',
|
|
||||||
tokenutil.TokensToString([a, b, c, d, e]))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'ddd\neee\naaa\nbbbccc',
|
|
||||||
tokenutil.TokensToString([d, e, a, b, c]),
|
|
||||||
'Neighboring tokens not in line_number order should have a newline '
|
|
||||||
'between them.')
|
|
||||||
|
|
||||||
def testGetPreviousCodeToken(self):
|
|
||||||
|
|
||||||
tokens = testutil.TokenizeSource("""
|
|
||||||
start1. // comment
|
|
||||||
/* another comment */
|
|
||||||
end1
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _GetTokenStartingWith(token_starts_with):
|
|
||||||
for t in tokens:
|
|
||||||
if t.string.startswith(token_starts_with):
|
|
||||||
return t
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
None,
|
|
||||||
tokenutil.GetPreviousCodeToken(_GetTokenStartingWith('start1')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'.',
|
|
||||||
tokenutil.GetPreviousCodeToken(_GetTokenStartingWith('end1')).string)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start1',
|
|
||||||
tokenutil.GetPreviousCodeToken(_GetTokenStartingWith('.')).string)
|
|
||||||
|
|
||||||
def testGetNextCodeToken(self):
|
|
||||||
|
|
||||||
tokens = testutil.TokenizeSource("""
|
|
||||||
start1. // comment
|
|
||||||
/* another comment */
|
|
||||||
end1
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _GetTokenStartingWith(token_starts_with):
|
|
||||||
for t in tokens:
|
|
||||||
if t.string.startswith(token_starts_with):
|
|
||||||
return t
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'.',
|
|
||||||
tokenutil.GetNextCodeToken(_GetTokenStartingWith('start1')).string)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'end1',
|
|
||||||
tokenutil.GetNextCodeToken(_GetTokenStartingWith('.')).string)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
None,
|
|
||||||
tokenutil.GetNextCodeToken(_GetTokenStartingWith('end1')))
|
|
||||||
|
|
||||||
def testGetIdentifierStart(self):
|
|
||||||
|
|
||||||
tokens = testutil.TokenizeSource("""
|
|
||||||
start1 . // comment
|
|
||||||
prototype. /* another comment */
|
|
||||||
end1
|
|
||||||
|
|
||||||
['edge'][case].prototype.
|
|
||||||
end2 = function() {}
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _GetTokenStartingWith(token_starts_with):
|
|
||||||
for t in tokens:
|
|
||||||
if t.string.startswith(token_starts_with):
|
|
||||||
return t
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start1',
|
|
||||||
tokenutil.GetIdentifierStart(_GetTokenStartingWith('end1')).string)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start1',
|
|
||||||
tokenutil.GetIdentifierStart(_GetTokenStartingWith('start1')).string)
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
None,
|
|
||||||
tokenutil.GetIdentifierStart(_GetTokenStartingWith('end2')))
|
|
||||||
|
|
||||||
def testInsertTokenBefore(self):
|
|
||||||
|
|
||||||
self.AssertInsertTokenAfterBefore(False)
|
|
||||||
|
|
||||||
def testInsertTokenAfter(self):
|
|
||||||
|
|
||||||
self.AssertInsertTokenAfterBefore(True)
|
|
||||||
|
|
||||||
def AssertInsertTokenAfterBefore(self, after):
|
|
||||||
|
|
||||||
new_token = javascripttokens.JavaScriptToken(
|
|
||||||
'a', javascripttokens.JavaScriptTokenType.IDENTIFIER, 1, 1)
|
|
||||||
|
|
||||||
existing_token1 = javascripttokens.JavaScriptToken(
|
|
||||||
'var', javascripttokens.JavaScriptTokenType.KEYWORD, 1, 1)
|
|
||||||
existing_token1.start_index = 0
|
|
||||||
existing_token1.metadata = ecmametadatapass.EcmaMetaData()
|
|
||||||
|
|
||||||
existing_token2 = javascripttokens.JavaScriptToken(
|
|
||||||
' ', javascripttokens.JavaScriptTokenType.WHITESPACE, 1, 1)
|
|
||||||
existing_token2.start_index = 3
|
|
||||||
existing_token2.metadata = ecmametadatapass.EcmaMetaData()
|
|
||||||
existing_token2.metadata.last_code = existing_token1
|
|
||||||
|
|
||||||
existing_token1.next = existing_token2
|
|
||||||
existing_token2.previous = existing_token1
|
|
||||||
|
|
||||||
if after:
|
|
||||||
tokenutil.InsertTokenAfter(new_token, existing_token1)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertTokenBefore(new_token, existing_token2)
|
|
||||||
|
|
||||||
self.assertEquals(existing_token1, new_token.previous)
|
|
||||||
self.assertEquals(existing_token2, new_token.next)
|
|
||||||
|
|
||||||
self.assertEquals(new_token, existing_token1.next)
|
|
||||||
self.assertEquals(new_token, existing_token2.previous)
|
|
||||||
|
|
||||||
self.assertEquals(existing_token1, new_token.metadata.last_code)
|
|
||||||
self.assertEquals(new_token, existing_token2.metadata.last_code)
|
|
||||||
|
|
||||||
self.assertEquals(0, existing_token1.start_index)
|
|
||||||
self.assertEquals(3, new_token.start_index)
|
|
||||||
self.assertEquals(4, existing_token2.start_index)
|
|
||||||
|
|
||||||
def testGetIdentifierForToken(self):
|
|
||||||
|
|
||||||
tokens = testutil.TokenizeSource("""
|
|
||||||
start1.abc.def.prototype.
|
|
||||||
onContinuedLine
|
|
||||||
|
|
||||||
(start2.abc.def
|
|
||||||
.hij.klm
|
|
||||||
.nop)
|
|
||||||
|
|
||||||
start3.abc.def
|
|
||||||
.hij = function() {};
|
|
||||||
|
|
||||||
// An absurd multi-liner.
|
|
||||||
start4.abc.def.
|
|
||||||
hij.
|
|
||||||
klm = function() {};
|
|
||||||
|
|
||||||
start5 . aaa . bbb . ccc
|
|
||||||
shouldntBePartOfThePreviousSymbol
|
|
||||||
|
|
||||||
start6.abc.def ghi.shouldntBePartOfThePreviousSymbol
|
|
||||||
|
|
||||||
var start7 = 42;
|
|
||||||
|
|
||||||
function start8() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
start9.abc. // why is there a comment here?
|
|
||||||
def /* another comment */
|
|
||||||
shouldntBePart
|
|
||||||
|
|
||||||
start10.abc // why is there a comment here?
|
|
||||||
.def /* another comment */
|
|
||||||
shouldntBePart
|
|
||||||
|
|
||||||
start11.abc. middle1.shouldNotBeIdentifier
|
|
||||||
""")
|
|
||||||
|
|
||||||
def _GetTokenStartingWith(token_starts_with):
|
|
||||||
for t in tokens:
|
|
||||||
if t.string.startswith(token_starts_with):
|
|
||||||
return t
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start1.abc.def.prototype.onContinuedLine',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start1')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start2.abc.def.hij.klm.nop',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start2')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start3.abc.def.hij',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start3')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start4.abc.def.hij.klm',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start4')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start5.aaa.bbb.ccc',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start5')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start6.abc.def',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start6')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start7',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start7')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start8',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start8')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start9.abc.def',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start9')))
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
'start10.abc.def',
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start10')))
|
|
||||||
|
|
||||||
self.assertIsNone(
|
|
||||||
tokenutil.GetIdentifierForToken(_GetTokenStartingWith('middle1')))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#*-* coding: utf-8
|
|
||||||
"""Closure typeannotation parsing and utilities."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter.common import error
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
TYPE = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class TypeAnnotation(object):
|
|
||||||
"""Represents a structured view of a closure type annotation.
|
|
||||||
|
|
||||||
Attribute:
|
|
||||||
identifier: The name of the type.
|
|
||||||
key_type: The name part before a colon.
|
|
||||||
sub_types: The list of sub_types used e.g. for Array.<…>
|
|
||||||
or_null: The '?' annotation
|
|
||||||
not_null: The '!' annotation
|
|
||||||
type_group: If this a a grouping (a|b), but does not include function(a).
|
|
||||||
return_type: The return type of a function definition.
|
|
||||||
alias: The actual type set by closurizednamespaceinfo if the identifier uses
|
|
||||||
an alias to shorten the name.
|
|
||||||
tokens: An ordered list of tokens used for this type. May contain
|
|
||||||
TypeAnnotation instances for sub_types, key_type or return_type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
IMPLICIT_TYPE_GROUP = 2
|
|
||||||
|
|
||||||
NULLABILITY_UNKNOWN = 2
|
|
||||||
|
|
||||||
# Frequently used known non-nullable types.
|
|
||||||
NON_NULLABLE = frozenset([
|
|
||||||
'boolean', 'function', 'number', 'string', 'undefined'])
|
|
||||||
# Frequently used known nullable types.
|
|
||||||
NULLABLE_TYPE_WHITELIST = frozenset([
|
|
||||||
'Array', 'Document', 'Element', 'Function', 'Node', 'NodeList',
|
|
||||||
'Object'])
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.identifier = ''
|
|
||||||
self.sub_types = []
|
|
||||||
self.or_null = False
|
|
||||||
self.not_null = False
|
|
||||||
self.type_group = False
|
|
||||||
self.alias = None
|
|
||||||
self.key_type = None
|
|
||||||
self.record_type = False
|
|
||||||
self.opt_arg = False
|
|
||||||
self.return_type = None
|
|
||||||
self.tokens = []
|
|
||||||
|
|
||||||
def IsFunction(self):
|
|
||||||
"""Determines whether this is a function definition."""
|
|
||||||
return self.identifier == 'function'
|
|
||||||
|
|
||||||
def IsConstructor(self):
|
|
||||||
"""Determines whether this is a function definition for a constructor."""
|
|
||||||
key_type = self.sub_types and self.sub_types[0].key_type
|
|
||||||
return self.IsFunction() and key_type.identifier == 'new'
|
|
||||||
|
|
||||||
def IsRecordType(self):
|
|
||||||
"""Returns True if this type is a record type."""
|
|
||||||
return (self.record_type or
|
|
||||||
bool([t for t in self.sub_types if t.IsRecordType()]))
|
|
||||||
|
|
||||||
def IsVarArgsType(self):
|
|
||||||
"""Determines if the type is a var_args type, i.e. starts with '...'."""
|
|
||||||
return self.identifier.startswith('...') or (
|
|
||||||
self.type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP and
|
|
||||||
self.sub_types[0].identifier.startswith('...'))
|
|
||||||
|
|
||||||
def IsEmpty(self):
|
|
||||||
"""Returns True if the type is empty."""
|
|
||||||
return not self.tokens
|
|
||||||
|
|
||||||
def IsUnknownType(self):
|
|
||||||
"""Returns True if this is the unknown type {?}."""
|
|
||||||
return (self.or_null
|
|
||||||
and not self.identifier
|
|
||||||
and not self.sub_types
|
|
||||||
and not self.return_type)
|
|
||||||
|
|
||||||
def Append(self, item):
|
|
||||||
"""Adds a sub_type to this type and finalizes it.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
item: The TypeAnnotation item to append.
|
|
||||||
"""
|
|
||||||
# item is a TypeAnnotation instance, so pylint: disable=protected-access
|
|
||||||
self.sub_types.append(item._Finalize(self))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Reconstructs the type definition."""
|
|
||||||
append = ''
|
|
||||||
if self.sub_types:
|
|
||||||
separator = (',' if not self.type_group else '|')
|
|
||||||
if self.identifier == 'function':
|
|
||||||
surround = '(%s)'
|
|
||||||
else:
|
|
||||||
surround = {False: '{%s}' if self.record_type else '<%s>',
|
|
||||||
True: '(%s)',
|
|
||||||
self.IMPLICIT_TYPE_GROUP: '%s'}[self.type_group]
|
|
||||||
append = surround % separator.join([repr(t) for t in self.sub_types])
|
|
||||||
if self.return_type:
|
|
||||||
append += ':%s' % repr(self.return_type)
|
|
||||||
append += '=' if self.opt_arg else ''
|
|
||||||
prefix = '' + ('?' if self.or_null else '') + ('!' if self.not_null else '')
|
|
||||||
keyword = '%s:' % repr(self.key_type) if self.key_type else ''
|
|
||||||
return keyword + prefix + '%s' % (self.alias or self.identifier) + append
|
|
||||||
|
|
||||||
def ToString(self):
|
|
||||||
"""Concats the type's tokens to form a string again."""
|
|
||||||
ret = []
|
|
||||||
for token in self.tokens:
|
|
||||||
if not isinstance(token, TypeAnnotation):
|
|
||||||
ret.append(token.string)
|
|
||||||
else:
|
|
||||||
ret.append(token.ToString())
|
|
||||||
return ''.join(ret)
|
|
||||||
|
|
||||||
def Dump(self, indent=''):
|
|
||||||
"""Dumps this type's structure for debugging purposes."""
|
|
||||||
result = []
|
|
||||||
for t in self.tokens:
|
|
||||||
if isinstance(t, TypeAnnotation):
|
|
||||||
result.append(indent + str(t) + ' =>\n' + t.Dump(indent + ' '))
|
|
||||||
else:
|
|
||||||
result.append(indent + str(t))
|
|
||||||
return '\n'.join(result)
|
|
||||||
|
|
||||||
def IterIdentifiers(self):
|
|
||||||
"""Iterates over all identifiers in this type and its subtypes."""
|
|
||||||
if self.identifier:
|
|
||||||
yield self.identifier
|
|
||||||
for subtype in self.IterTypes():
|
|
||||||
for identifier in subtype.IterIdentifiers():
|
|
||||||
yield identifier
|
|
||||||
|
|
||||||
def IterTypeGroup(self):
|
|
||||||
"""Iterates over all types in the type group including self.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
If this is a implicit or manual type-group: all sub_types.
|
|
||||||
Otherwise: self
|
|
||||||
E.g. for @type {Foo.<Bar>} this will yield only Foo.<Bar>,
|
|
||||||
for @type {Foo|(Bar|Sample)} this will yield Foo, Bar and Sample.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.type_group:
|
|
||||||
for sub_type in self.sub_types:
|
|
||||||
for sub_type in sub_type.IterTypeGroup():
|
|
||||||
yield sub_type
|
|
||||||
else:
|
|
||||||
yield self
|
|
||||||
|
|
||||||
def IterTypes(self):
|
|
||||||
"""Iterates over each subtype as well as return and key types."""
|
|
||||||
if self.return_type:
|
|
||||||
yield self.return_type
|
|
||||||
|
|
||||||
if self.key_type:
|
|
||||||
yield self.key_type
|
|
||||||
|
|
||||||
for sub_type in self.sub_types:
|
|
||||||
yield sub_type
|
|
||||||
|
|
||||||
def GetNullability(self, modifiers=True):
|
|
||||||
"""Computes whether the type may be null.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
modifiers: Whether the modifiers ? and ! should be considered in the
|
|
||||||
evaluation.
|
|
||||||
Returns:
|
|
||||||
True if the type allows null, False if the type is strictly non nullable
|
|
||||||
and NULLABILITY_UNKNOWN if the nullability cannot be determined.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Explicitly marked nullable types or 'null' are nullable.
|
|
||||||
if (modifiers and self.or_null) or self.identifier == 'null':
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Explicitly marked non-nullable types or non-nullable base types:
|
|
||||||
if ((modifiers and self.not_null) or self.record_type
|
|
||||||
or self.identifier in self.NON_NULLABLE):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# A type group is nullable if any of its elements are nullable.
|
|
||||||
if self.type_group:
|
|
||||||
maybe_nullable = False
|
|
||||||
for sub_type in self.sub_types:
|
|
||||||
nullability = sub_type.GetNullability()
|
|
||||||
if nullability == self.NULLABILITY_UNKNOWN:
|
|
||||||
maybe_nullable = nullability
|
|
||||||
elif nullability:
|
|
||||||
return True
|
|
||||||
return maybe_nullable
|
|
||||||
|
|
||||||
# Whitelisted types are nullable.
|
|
||||||
if self.identifier.rstrip('.') in self.NULLABLE_TYPE_WHITELIST:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# All other types are unknown (most should be nullable, but
|
|
||||||
# enums are not and typedefs might not be).
|
|
||||||
return self.NULLABILITY_UNKNOWN
|
|
||||||
|
|
||||||
def WillAlwaysBeNullable(self):
|
|
||||||
"""Computes whether the ! flag is illegal for this type.
|
|
||||||
|
|
||||||
This is the case if this type or any of the subtypes is marked as
|
|
||||||
explicitly nullable.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the ! flag would be illegal.
|
|
||||||
"""
|
|
||||||
if self.or_null or self.identifier == 'null':
|
|
||||||
return True
|
|
||||||
|
|
||||||
if self.type_group:
|
|
||||||
return bool([t for t in self.sub_types if t.WillAlwaysBeNullable()])
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _Finalize(self, parent):
|
|
||||||
"""Fixes some parsing issues once the TypeAnnotation is complete."""
|
|
||||||
|
|
||||||
# Normalize functions whose definition ended up in the key type because
|
|
||||||
# they defined a return type after a colon.
|
|
||||||
if self.key_type and self.key_type.identifier == 'function':
|
|
||||||
current = self.key_type
|
|
||||||
current.return_type = self
|
|
||||||
self.key_type = None
|
|
||||||
# opt_arg never refers to the return type but to the function itself.
|
|
||||||
current.opt_arg = self.opt_arg
|
|
||||||
self.opt_arg = False
|
|
||||||
return current
|
|
||||||
|
|
||||||
# If a typedef just specified the key, it will not end up in the key type.
|
|
||||||
if parent.record_type and not self.key_type:
|
|
||||||
current = TypeAnnotation()
|
|
||||||
current.key_type = self
|
|
||||||
current.tokens.append(self)
|
|
||||||
return current
|
|
||||||
return self
|
|
||||||
|
|
||||||
def FirstToken(self):
|
|
||||||
"""Returns the first token used in this type or any of its subtypes."""
|
|
||||||
first = self.tokens[0]
|
|
||||||
return first.FirstToken() if isinstance(first, TypeAnnotation) else first
|
|
||||||
|
|
||||||
|
|
||||||
def Parse(token, token_end, error_handler):
|
|
||||||
"""Parses a type annotation and returns a TypeAnnotation object."""
|
|
||||||
return TypeAnnotationParser(error_handler).Parse(token.next, token_end)
|
|
||||||
|
|
||||||
|
|
||||||
class TypeAnnotationParser(object):
|
|
||||||
"""A parser for type annotations constructing the TypeAnnotation object."""
|
|
||||||
|
|
||||||
def __init__(self, error_handler):
|
|
||||||
self._stack = []
|
|
||||||
self._error_handler = error_handler
|
|
||||||
self._closing_error = False
|
|
||||||
|
|
||||||
def Parse(self, token, token_end):
|
|
||||||
"""Parses a type annotation and returns a TypeAnnotation object."""
|
|
||||||
root = TypeAnnotation()
|
|
||||||
self._stack.append(root)
|
|
||||||
current = TypeAnnotation()
|
|
||||||
root.tokens.append(current)
|
|
||||||
|
|
||||||
while token and token != token_end:
|
|
||||||
if token.type in (TYPE.DOC_TYPE_START_BLOCK, TYPE.DOC_START_BRACE):
|
|
||||||
if token.string == '(':
|
|
||||||
if (current.identifier and
|
|
||||||
current.identifier not in ['function', '...']):
|
|
||||||
self.Error(token,
|
|
||||||
'Invalid identifier for (): "%s"' % current.identifier)
|
|
||||||
current.type_group = current.identifier != 'function'
|
|
||||||
elif token.string == '{':
|
|
||||||
current.record_type = True
|
|
||||||
current.tokens.append(token)
|
|
||||||
self._stack.append(current)
|
|
||||||
current = TypeAnnotation()
|
|
||||||
self._stack[-1].tokens.append(current)
|
|
||||||
|
|
||||||
elif token.type in (TYPE.DOC_TYPE_END_BLOCK, TYPE.DOC_END_BRACE):
|
|
||||||
prev = self._stack.pop()
|
|
||||||
prev.Append(current)
|
|
||||||
current = prev
|
|
||||||
|
|
||||||
# If an implicit type group was created, close it as well.
|
|
||||||
if prev.type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP:
|
|
||||||
prev = self._stack.pop()
|
|
||||||
prev.Append(current)
|
|
||||||
current = prev
|
|
||||||
current.tokens.append(token)
|
|
||||||
|
|
||||||
elif token.type == TYPE.DOC_TYPE_MODIFIER:
|
|
||||||
if token.string == '!':
|
|
||||||
current.tokens.append(token)
|
|
||||||
current.not_null = True
|
|
||||||
elif token.string == '?':
|
|
||||||
current.tokens.append(token)
|
|
||||||
current.or_null = True
|
|
||||||
elif token.string == ':':
|
|
||||||
current.tokens.append(token)
|
|
||||||
prev = current
|
|
||||||
current = TypeAnnotation()
|
|
||||||
prev.tokens.append(current)
|
|
||||||
current.key_type = prev
|
|
||||||
elif token.string == '=':
|
|
||||||
# For implicit type groups the '=' refers to the parent.
|
|
||||||
try:
|
|
||||||
if self._stack[-1].type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP:
|
|
||||||
self._stack[-1].tokens.append(token)
|
|
||||||
self._stack[-1].opt_arg = True
|
|
||||||
else:
|
|
||||||
current.tokens.append(token)
|
|
||||||
current.opt_arg = True
|
|
||||||
except IndexError:
|
|
||||||
self.ClosingError(token)
|
|
||||||
elif token.string == '|':
|
|
||||||
# If a type group has explicitly been opened do a normal append.
|
|
||||||
# Otherwise we have to open the type group and move the current
|
|
||||||
# type into it, before appending
|
|
||||||
if not self._stack[-1].type_group:
|
|
||||||
type_group = TypeAnnotation()
|
|
||||||
if current.key_type and current.key_type.identifier != 'function':
|
|
||||||
type_group.key_type = current.key_type
|
|
||||||
current.key_type = None
|
|
||||||
type_group.type_group = TypeAnnotation.IMPLICIT_TYPE_GROUP
|
|
||||||
# Fix the token order
|
|
||||||
prev = self._stack[-1].tokens.pop()
|
|
||||||
self._stack[-1].tokens.append(type_group)
|
|
||||||
type_group.tokens.append(prev)
|
|
||||||
self._stack.append(type_group)
|
|
||||||
self._stack[-1].tokens.append(token)
|
|
||||||
self.Append(current, error_token=token)
|
|
||||||
current = TypeAnnotation()
|
|
||||||
self._stack[-1].tokens.append(current)
|
|
||||||
elif token.string == ',':
|
|
||||||
self.Append(current, error_token=token)
|
|
||||||
current = TypeAnnotation()
|
|
||||||
self._stack[-1].tokens.append(token)
|
|
||||||
self._stack[-1].tokens.append(current)
|
|
||||||
else:
|
|
||||||
current.tokens.append(token)
|
|
||||||
self.Error(token, 'Invalid token')
|
|
||||||
|
|
||||||
elif token.type == TYPE.COMMENT:
|
|
||||||
current.tokens.append(token)
|
|
||||||
current.identifier += token.string.strip()
|
|
||||||
|
|
||||||
elif token.type in [TYPE.DOC_PREFIX, TYPE.WHITESPACE]:
|
|
||||||
current.tokens.append(token)
|
|
||||||
|
|
||||||
else:
|
|
||||||
current.tokens.append(token)
|
|
||||||
self.Error(token, 'Unexpected token')
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
self.Append(current, error_token=token)
|
|
||||||
try:
|
|
||||||
ret = self._stack.pop()
|
|
||||||
except IndexError:
|
|
||||||
self.ClosingError(token)
|
|
||||||
# The type is screwed up, but let's return something.
|
|
||||||
return current
|
|
||||||
|
|
||||||
if self._stack and (len(self._stack) != 1 or
|
|
||||||
ret.type_group != TypeAnnotation.IMPLICIT_TYPE_GROUP):
|
|
||||||
self.Error(token, 'Too many opening items.')
|
|
||||||
|
|
||||||
return ret if len(ret.sub_types) > 1 else ret.sub_types[0]
|
|
||||||
|
|
||||||
def Append(self, type_obj, error_token):
|
|
||||||
"""Appends a new TypeAnnotation object to the current parent."""
|
|
||||||
if self._stack:
|
|
||||||
self._stack[-1].Append(type_obj)
|
|
||||||
else:
|
|
||||||
self.ClosingError(error_token)
|
|
||||||
|
|
||||||
def ClosingError(self, token):
|
|
||||||
"""Reports an error about too many closing items, but only once."""
|
|
||||||
if not self._closing_error:
|
|
||||||
self._closing_error = True
|
|
||||||
self.Error(token, 'Too many closing items.')
|
|
||||||
|
|
||||||
def Error(self, token, message):
|
|
||||||
"""Calls the error_handler to post an error message."""
|
|
||||||
if self._error_handler:
|
|
||||||
self._error_handler.HandleError(error.Error(
|
|
||||||
errors.JSDOC_DOES_NOT_PARSE,
|
|
||||||
'Error parsing jsdoc type at token "%s" (column: %d): %s' %
|
|
||||||
(token.string, token.start_index, message), token))
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Unit tests for the typeannotation module."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
CRAZY_TYPE = ('Array.<!function(new:X,{a:null},...(c|d)):'
|
|
||||||
'function(...(Object.<string>))>')
|
|
||||||
|
|
||||||
|
|
||||||
class TypeErrorException(Exception):
|
|
||||||
"""Exception for TypeErrors."""
|
|
||||||
|
|
||||||
def __init__(self, errors):
|
|
||||||
super(TypeErrorException, self).__init__()
|
|
||||||
self.errors = errors
|
|
||||||
|
|
||||||
|
|
||||||
class TypeParserTest(googletest.TestCase):
|
|
||||||
"""Tests for typeannotation parsing."""
|
|
||||||
|
|
||||||
def _ParseComment(self, script):
|
|
||||||
"""Parse a script that contains one comment and return it."""
|
|
||||||
accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
_, comments = testutil.ParseFunctionsAndComments(script, accumulator)
|
|
||||||
if accumulator.GetErrors():
|
|
||||||
raise TypeErrorException(accumulator.GetErrors())
|
|
||||||
self.assertEquals(1, len(comments))
|
|
||||||
return comments[0]
|
|
||||||
|
|
||||||
def _ParseType(self, type_str):
|
|
||||||
"""Creates a comment to parse and returns the parsed type."""
|
|
||||||
comment = self._ParseComment('/** @type {%s} **/' % type_str)
|
|
||||||
return comment.GetDocFlags()[0].jstype
|
|
||||||
|
|
||||||
def assertProperReconstruction(self, type_str, matching_str=None):
|
|
||||||
"""Parses the type and asserts the its repr matches the type.
|
|
||||||
|
|
||||||
If matching_str is specified, it will assert that the repr matches this
|
|
||||||
string instead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
type_str: The type string to parse.
|
|
||||||
matching_str: A string the __repr__ of the parsed type should match.
|
|
||||||
Returns:
|
|
||||||
The parsed js_type.
|
|
||||||
"""
|
|
||||||
parsed_type = self._ParseType(type_str)
|
|
||||||
# Use listEqual assertion to more easily identify the difference
|
|
||||||
self.assertListEqual(list(matching_str or type_str),
|
|
||||||
list(repr(parsed_type)))
|
|
||||||
self.assertEquals(matching_str or type_str, repr(parsed_type))
|
|
||||||
|
|
||||||
# Newlines will be inserted by the file writer.
|
|
||||||
self.assertEquals(type_str.replace('\n', ''), parsed_type.ToString())
|
|
||||||
return parsed_type
|
|
||||||
|
|
||||||
def assertNullable(self, type_str, nullable=True):
|
|
||||||
parsed_type = self.assertProperReconstruction(type_str)
|
|
||||||
self.assertEquals(nullable, parsed_type.GetNullability(),
|
|
||||||
'"%s" should %sbe nullable' %
|
|
||||||
(type_str, 'not ' if nullable else ''))
|
|
||||||
|
|
||||||
def assertNotNullable(self, type_str):
|
|
||||||
return self.assertNullable(type_str, nullable=False)
|
|
||||||
|
|
||||||
def testReconstruction(self):
|
|
||||||
self.assertProperReconstruction('*')
|
|
||||||
self.assertProperReconstruction('number')
|
|
||||||
self.assertProperReconstruction('(((number)))')
|
|
||||||
self.assertProperReconstruction('!number')
|
|
||||||
self.assertProperReconstruction('?!number')
|
|
||||||
self.assertProperReconstruction('number=')
|
|
||||||
self.assertProperReconstruction('number=!?', '?!number=')
|
|
||||||
self.assertProperReconstruction('number|?string')
|
|
||||||
self.assertProperReconstruction('(number|string)')
|
|
||||||
self.assertProperReconstruction('?(number|string)')
|
|
||||||
self.assertProperReconstruction('Object.<number,string>')
|
|
||||||
self.assertProperReconstruction('function(new:Object)')
|
|
||||||
self.assertProperReconstruction('function(new:Object):number')
|
|
||||||
self.assertProperReconstruction('function(new:Object,Element):number')
|
|
||||||
self.assertProperReconstruction('function(this:T,...)')
|
|
||||||
self.assertProperReconstruction('{a:?number}')
|
|
||||||
self.assertProperReconstruction('{a:?number,b:(number|string)}')
|
|
||||||
self.assertProperReconstruction('{c:{nested_element:*}|undefined}')
|
|
||||||
self.assertProperReconstruction('{handleEvent:function(?):?}')
|
|
||||||
self.assertProperReconstruction('function():?|null')
|
|
||||||
self.assertProperReconstruction('null|function():?|bar')
|
|
||||||
|
|
||||||
def testOptargs(self):
|
|
||||||
self.assertProperReconstruction('number=')
|
|
||||||
self.assertProperReconstruction('number|string=')
|
|
||||||
self.assertProperReconstruction('(number|string)=')
|
|
||||||
self.assertProperReconstruction('(number|string=)')
|
|
||||||
self.assertProperReconstruction('(number=|string)')
|
|
||||||
self.assertProperReconstruction('function(...):number=')
|
|
||||||
|
|
||||||
def testIndepth(self):
|
|
||||||
# Do an deeper check of the crazy identifier
|
|
||||||
crazy = self.assertProperReconstruction(CRAZY_TYPE)
|
|
||||||
self.assertEquals('Array.', crazy.identifier)
|
|
||||||
self.assertEquals(1, len(crazy.sub_types))
|
|
||||||
func1 = crazy.sub_types[0]
|
|
||||||
func2 = func1.return_type
|
|
||||||
self.assertEquals('function', func1.identifier)
|
|
||||||
self.assertEquals('function', func2.identifier)
|
|
||||||
self.assertEquals(3, len(func1.sub_types))
|
|
||||||
self.assertEquals(1, len(func2.sub_types))
|
|
||||||
self.assertEquals('Object.', func2.sub_types[0].sub_types[0].identifier)
|
|
||||||
|
|
||||||
def testIterIdentifiers(self):
|
|
||||||
nested_identifiers = self._ParseType('(a|{b:(c|function(new:d):e)})')
|
|
||||||
for identifier in ('a', 'b', 'c', 'd', 'e'):
|
|
||||||
self.assertIn(identifier, nested_identifiers.IterIdentifiers())
|
|
||||||
|
|
||||||
def testIsEmpty(self):
|
|
||||||
self.assertTrue(self._ParseType('').IsEmpty())
|
|
||||||
self.assertFalse(self._ParseType('?').IsEmpty())
|
|
||||||
self.assertFalse(self._ParseType('!').IsEmpty())
|
|
||||||
self.assertFalse(self._ParseType('<?>').IsEmpty())
|
|
||||||
|
|
||||||
def testIsConstructor(self):
|
|
||||||
self.assertFalse(self._ParseType('').IsConstructor())
|
|
||||||
self.assertFalse(self._ParseType('Array.<number>').IsConstructor())
|
|
||||||
self.assertTrue(self._ParseType('function(new:T)').IsConstructor())
|
|
||||||
|
|
||||||
def testIsVarArgsType(self):
|
|
||||||
self.assertTrue(self._ParseType('...number').IsVarArgsType())
|
|
||||||
self.assertTrue(self._ParseType('...Object|Array').IsVarArgsType())
|
|
||||||
self.assertTrue(self._ParseType('...(Object|Array)').IsVarArgsType())
|
|
||||||
self.assertFalse(self._ParseType('Object|...Array').IsVarArgsType())
|
|
||||||
self.assertFalse(self._ParseType('(...Object|Array)').IsVarArgsType())
|
|
||||||
|
|
||||||
def testIsUnknownType(self):
|
|
||||||
self.assertTrue(self._ParseType('?').IsUnknownType())
|
|
||||||
self.assertTrue(self._ParseType('Foo.<?>').sub_types[0].IsUnknownType())
|
|
||||||
self.assertFalse(self._ParseType('?|!').IsUnknownType())
|
|
||||||
self.assertTrue(self._ParseType('?|!').sub_types[0].IsUnknownType())
|
|
||||||
self.assertFalse(self._ParseType('!').IsUnknownType())
|
|
||||||
|
|
||||||
long_type = 'function():?|{handleEvent:function(?=):?,sample:?}|?='
|
|
||||||
record = self._ParseType(long_type)
|
|
||||||
# First check that there's not just one type with 3 return types, but three
|
|
||||||
# top-level types.
|
|
||||||
self.assertEquals(3, len(record.sub_types))
|
|
||||||
|
|
||||||
# Now extract all unknown type instances and verify that they really are.
|
|
||||||
handle_event, sample = record.sub_types[1].sub_types
|
|
||||||
for i, sub_type in enumerate([
|
|
||||||
record.sub_types[0].return_type,
|
|
||||||
handle_event.return_type,
|
|
||||||
handle_event.sub_types[0],
|
|
||||||
sample,
|
|
||||||
record.sub_types[2]]):
|
|
||||||
self.assertTrue(sub_type.IsUnknownType(),
|
|
||||||
'Type %d should be the unknown type: %s\n%s' % (
|
|
||||||
i, sub_type.tokens, record.Dump()))
|
|
||||||
|
|
||||||
def testTypedefNames(self):
|
|
||||||
easy = self._ParseType('{a}')
|
|
||||||
self.assertTrue(easy.record_type)
|
|
||||||
|
|
||||||
easy = self.assertProperReconstruction('{a}', '{a:}').sub_types[0]
|
|
||||||
self.assertEquals('a', easy.key_type.identifier)
|
|
||||||
self.assertEquals('', easy.identifier)
|
|
||||||
|
|
||||||
easy = self.assertProperReconstruction('{a:b}').sub_types[0]
|
|
||||||
self.assertEquals('a', easy.key_type.identifier)
|
|
||||||
self.assertEquals('b', easy.identifier)
|
|
||||||
|
|
||||||
def assertTypeError(self, type_str):
|
|
||||||
"""Asserts that parsing the given type raises a linter error."""
|
|
||||||
self.assertRaises(TypeErrorException, self._ParseType, type_str)
|
|
||||||
|
|
||||||
def testParseBadTypes(self):
|
|
||||||
"""Tests that several errors in types don't break the parser."""
|
|
||||||
self.assertTypeError('<')
|
|
||||||
self.assertTypeError('>')
|
|
||||||
self.assertTypeError('Foo.<Bar')
|
|
||||||
self.assertTypeError('Foo.Bar>=')
|
|
||||||
self.assertTypeError('Foo.<Bar>>=')
|
|
||||||
self.assertTypeError('(')
|
|
||||||
self.assertTypeError(')')
|
|
||||||
self.assertTypeError('Foo.<Bar)>')
|
|
||||||
self._ParseType(':')
|
|
||||||
self._ParseType(':foo')
|
|
||||||
self.assertTypeError(':)foo')
|
|
||||||
self.assertTypeError('(a|{b:(c|function(new:d):e')
|
|
||||||
|
|
||||||
def testNullable(self):
|
|
||||||
self.assertNullable('null')
|
|
||||||
self.assertNullable('Object')
|
|
||||||
self.assertNullable('?string')
|
|
||||||
self.assertNullable('?number')
|
|
||||||
|
|
||||||
self.assertNotNullable('string')
|
|
||||||
self.assertNotNullable('number')
|
|
||||||
self.assertNotNullable('boolean')
|
|
||||||
self.assertNotNullable('function(Object)')
|
|
||||||
self.assertNotNullable('function(Object):Object')
|
|
||||||
self.assertNotNullable('function(?Object):?Object')
|
|
||||||
self.assertNotNullable('!Object')
|
|
||||||
|
|
||||||
self.assertNotNullable('boolean|string')
|
|
||||||
self.assertNotNullable('(boolean|string)')
|
|
||||||
|
|
||||||
self.assertNullable('(boolean|string|null)')
|
|
||||||
self.assertNullable('(?boolean)')
|
|
||||||
self.assertNullable('?(boolean)')
|
|
||||||
|
|
||||||
self.assertNullable('(boolean|Object)')
|
|
||||||
self.assertNotNullable('(boolean|(string|{a:}))')
|
|
||||||
|
|
||||||
def testSpaces(self):
|
|
||||||
"""Tests that spaces don't change the outcome."""
|
|
||||||
type_str = (' A < b | ( c | ? ! d e f ) > | '
|
|
||||||
'function ( x : . . . ) : { y : z = } ')
|
|
||||||
two_spaces = type_str.replace(' ', ' ')
|
|
||||||
no_spaces = type_str.replace(' ', '')
|
|
||||||
newlines = type_str.replace(' ', '\n * ')
|
|
||||||
self.assertProperReconstruction(no_spaces)
|
|
||||||
self.assertProperReconstruction(type_str, no_spaces)
|
|
||||||
self.assertProperReconstruction(two_spaces, no_spaces)
|
|
||||||
self.assertProperReconstruction(newlines, no_spaces)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
Metadata-Version: 1.0
|
|
||||||
Name: closure-linter
|
|
||||||
Version: 2.3.17
|
|
||||||
Summary: Closure Linter
|
|
||||||
Home-page: http://code.google.com/p/closure-linter
|
|
||||||
Author: The Closure Linter Authors
|
|
||||||
Author-email: opensource@google.com
|
|
||||||
License: Apache
|
|
||||||
Description: UNKNOWN
|
|
||||||
Platform: UNKNOWN
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
README
|
|
||||||
setup.py
|
|
||||||
closure_linter/__init__.py
|
|
||||||
closure_linter/aliaspass.py
|
|
||||||
closure_linter/aliaspass_test.py
|
|
||||||
closure_linter/checker.py
|
|
||||||
closure_linter/checkerbase.py
|
|
||||||
closure_linter/closurizednamespacesinfo.py
|
|
||||||
closure_linter/closurizednamespacesinfo_test.py
|
|
||||||
closure_linter/ecmalintrules.py
|
|
||||||
closure_linter/ecmametadatapass.py
|
|
||||||
closure_linter/error_check.py
|
|
||||||
closure_linter/error_fixer.py
|
|
||||||
closure_linter/error_fixer_test.py
|
|
||||||
closure_linter/errorrecord.py
|
|
||||||
closure_linter/errorrules.py
|
|
||||||
closure_linter/errorrules_test.py
|
|
||||||
closure_linter/errors.py
|
|
||||||
closure_linter/fixjsstyle.py
|
|
||||||
closure_linter/fixjsstyle_test.py
|
|
||||||
closure_linter/full_test.py
|
|
||||||
closure_linter/gjslint.py
|
|
||||||
closure_linter/indentation.py
|
|
||||||
closure_linter/javascriptlintrules.py
|
|
||||||
closure_linter/javascriptstatetracker.py
|
|
||||||
closure_linter/javascriptstatetracker_test.py
|
|
||||||
closure_linter/javascripttokenizer.py
|
|
||||||
closure_linter/javascripttokens.py
|
|
||||||
closure_linter/not_strict_test.py
|
|
||||||
closure_linter/requireprovidesorter.py
|
|
||||||
closure_linter/requireprovidesorter_test.py
|
|
||||||
closure_linter/runner.py
|
|
||||||
closure_linter/runner_test.py
|
|
||||||
closure_linter/scopeutil.py
|
|
||||||
closure_linter/scopeutil_test.py
|
|
||||||
closure_linter/statetracker.py
|
|
||||||
closure_linter/statetracker_test.py
|
|
||||||
closure_linter/strict_test.py
|
|
||||||
closure_linter/testutil.py
|
|
||||||
closure_linter/tokenutil.py
|
|
||||||
closure_linter/tokenutil_test.py
|
|
||||||
closure_linter/typeannotation.py
|
|
||||||
closure_linter/typeannotation_test.py
|
|
||||||
closure_linter.egg-info/PKG-INFO
|
|
||||||
closure_linter.egg-info/SOURCES.txt
|
|
||||||
closure_linter.egg-info/dependency_links.txt
|
|
||||||
closure_linter.egg-info/entry_points.txt
|
|
||||||
closure_linter.egg-info/requires.txt
|
|
||||||
closure_linter.egg-info/top_level.txt
|
|
||||||
closure_linter/common/__init__.py
|
|
||||||
closure_linter/common/error.py
|
|
||||||
closure_linter/common/erroraccumulator.py
|
|
||||||
closure_linter/common/errorhandler.py
|
|
||||||
closure_linter/common/erroroutput.py
|
|
||||||
closure_linter/common/filetestcase.py
|
|
||||||
closure_linter/common/htmlutil.py
|
|
||||||
closure_linter/common/lintrunner.py
|
|
||||||
closure_linter/common/matcher.py
|
|
||||||
closure_linter/common/position.py
|
|
||||||
closure_linter/common/simplefileflags.py
|
|
||||||
closure_linter/common/tokenizer.py
|
|
||||||
closure_linter/common/tokens.py
|
|
||||||
closure_linter/common/tokens_test.py
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
[console_scripts]
|
|
||||||
fixjsstyle = closure_linter.fixjsstyle:main
|
|
||||||
gjslint = closure_linter.gjslint:main
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python-gflags
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
closure_linter
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Package indicator for gjslint."""
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
#!/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)')
|
|
||||||
|
|
||||||
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):]
|
|
||||||
|
|
||||||
|
|
||||||
def _SetTypeAlias(js_type, alias_map):
|
|
||||||
"""Updates the alias for identifiers in a type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
js_type: A typeannotation.TypeAnnotation instance.
|
|
||||||
alias_map: A dictionary mapping a symbol to an alias.
|
|
||||||
"""
|
|
||||||
aliased_symbol = _GetAliasForIdentifier(js_type.identifier, alias_map)
|
|
||||||
if aliased_symbol:
|
|
||||||
js_type.alias = aliased_symbol
|
|
||||||
for sub_type in js_type.IterTypes():
|
|
||||||
_SetTypeAlias(sub_type, alias_map)
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Process aliases in statements in the root scope for goog.module-style
|
|
||||||
# aliases.
|
|
||||||
global_alias_map = {}
|
|
||||||
for context in root_context.children:
|
|
||||||
if context.type == ecmametadatapass.EcmaContext.STATEMENT:
|
|
||||||
for statement_child in context.children:
|
|
||||||
if statement_child.type == ecmametadatapass.EcmaContext.VAR:
|
|
||||||
match = scopeutil.MatchModuleAlias(statement_child)
|
|
||||||
if match:
|
|
||||||
# goog.require aliases cannot use further aliases, the symbol is
|
|
||||||
# the second part of match, directly.
|
|
||||||
symbol = match[1]
|
|
||||||
if scopeutil.IsInClosurizedNamespace(symbol,
|
|
||||||
self._closurized_namespaces):
|
|
||||||
global_alias_map[match[0]] = symbol
|
|
||||||
|
|
||||||
# Process each block to find aliases.
|
|
||||||
for context in root_context.children:
|
|
||||||
self._ProcessBlock(context, global_alias_map)
|
|
||||||
|
|
||||||
def _ProcessBlock(self, context, global_alias_map):
|
|
||||||
"""Scans a goog.scope block to find aliases and mark alias tokens."""
|
|
||||||
alias_map = global_alias_map.copy()
|
|
||||||
|
|
||||||
# Iterate over every token in the context. 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 = context.start_token
|
|
||||||
while token and self._IsTokenInParentBlock(token, context):
|
|
||||||
token_context = token.metadata.context if token.metadata else None
|
|
||||||
|
|
||||||
# Check to see if this token is an alias.
|
|
||||||
if token_context and 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
|
|
||||||
scopeutil.IsGoogScopeBlock(token_context.parent.parent)):
|
|
||||||
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
|
|
||||||
|
|
||||||
elif token.type == javascripttokens.JavaScriptTokenType.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
if flag and flag.HasType() and flag.jstype:
|
|
||||||
_SetTypeAlias(flag.jstype, alias_map)
|
|
||||||
|
|
||||||
token = token.next # Get next token
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the aliaspass module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
|
|
||||||
def _GetTokenByLineAndString(start_token, string, line_number):
|
|
||||||
for token in start_token:
|
|
||||||
if token.line_number == line_number and token.string == string:
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class AliasPassTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testInvalidGoogScopeCall(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCOPE_SCRIPT)
|
|
||||||
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
alias_pass = aliaspass.AliasPass(
|
|
||||||
error_handler=error_accumulator)
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_errors = error_accumulator.GetErrors()
|
|
||||||
self.assertEquals(1, len(alias_errors))
|
|
||||||
|
|
||||||
alias_error = alias_errors[0]
|
|
||||||
|
|
||||||
self.assertEquals(errors.INVALID_USE_OF_GOOG_SCOPE, alias_error.code)
|
|
||||||
self.assertEquals('goog.scope', alias_error.token.string)
|
|
||||||
|
|
||||||
def testAliasedIdentifiers(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_ALIAS_SCRIPT)
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog', 'myproject']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_token = _GetTokenByLineAndString(start_token, 'Event', 4)
|
|
||||||
self.assertTrue(alias_token.metadata.is_alias_definition)
|
|
||||||
|
|
||||||
my_class_token = _GetTokenByLineAndString(start_token, 'myClass', 9)
|
|
||||||
self.assertIsNone(my_class_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
component_token = _GetTokenByLineAndString(start_token, 'Component', 17)
|
|
||||||
self.assertEquals('goog.ui.Component',
|
|
||||||
component_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
event_token = _GetTokenByLineAndString(start_token, 'Event.Something', 17)
|
|
||||||
self.assertEquals('goog.events.Event.Something',
|
|
||||||
event_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
non_closurized_token = _GetTokenByLineAndString(
|
|
||||||
start_token, 'NonClosurizedClass', 18)
|
|
||||||
self.assertIsNone(non_closurized_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
long_start_token = _GetTokenByLineAndString(start_token, 'Event', 24)
|
|
||||||
self.assertEquals('goog.events.Event.MultilineIdentifier.someMethod',
|
|
||||||
long_start_token.metadata.aliased_symbol)
|
|
||||||
|
|
||||||
def testAliasedDoctypes(self):
|
|
||||||
"""Tests that aliases are correctly expanded within type annotations."""
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_ALIAS_SCRIPT)
|
|
||||||
tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
tracker.DocFlagPass(start_token, error_handler=None)
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog', 'myproject']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
flag_token = _GetTokenByLineAndString(start_token, '@type', 22)
|
|
||||||
self.assertEquals(
|
|
||||||
'goog.events.Event.<goog.ui.Component,Array<myproject.foo.MyClass>>',
|
|
||||||
repr(flag_token.attached_object.jstype))
|
|
||||||
|
|
||||||
def testModuleAlias(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass("""
|
|
||||||
goog.module('goog.test');
|
|
||||||
var Alias = goog.require('goog.Alias');
|
|
||||||
Alias.use();
|
|
||||||
""")
|
|
||||||
alias_pass = aliaspass.AliasPass(set(['goog']))
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
alias_token = _GetTokenByLineAndString(start_token, 'Alias', 3)
|
|
||||||
self.assertTrue(alias_token.metadata.is_alias_definition)
|
|
||||||
|
|
||||||
def testMultipleGoogScopeCalls(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(
|
|
||||||
_TEST_MULTIPLE_SCOPE_SCRIPT)
|
|
||||||
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(
|
|
||||||
set(['goog', 'myproject']),
|
|
||||||
error_handler=error_accumulator)
|
|
||||||
alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
alias_errors = error_accumulator.GetErrors()
|
|
||||||
|
|
||||||
self.assertEquals(3, len(alias_errors))
|
|
||||||
|
|
||||||
error = alias_errors[0]
|
|
||||||
self.assertEquals(errors.INVALID_USE_OF_GOOG_SCOPE, error.code)
|
|
||||||
self.assertEquals(7, error.token.line_number)
|
|
||||||
|
|
||||||
error = alias_errors[1]
|
|
||||||
self.assertEquals(errors.EXTRA_GOOG_SCOPE_USAGE, error.code)
|
|
||||||
self.assertEquals(7, error.token.line_number)
|
|
||||||
|
|
||||||
error = alias_errors[2]
|
|
||||||
self.assertEquals(errors.EXTRA_GOOG_SCOPE_USAGE, error.code)
|
|
||||||
self.assertEquals(11, error.token.line_number)
|
|
||||||
|
|
||||||
|
|
||||||
_TEST_ALIAS_SCRIPT = """
|
|
||||||
goog.scope(function() {
|
|
||||||
var events = goog.events; // scope alias
|
|
||||||
var Event = events.
|
|
||||||
Event; // nested multiline scope alias
|
|
||||||
|
|
||||||
// This should not be registered as an aliased identifier because
|
|
||||||
// it appears before the alias.
|
|
||||||
var myClass = new MyClass();
|
|
||||||
|
|
||||||
var Component = goog.ui.Component; // scope alias
|
|
||||||
var MyClass = myproject.foo.MyClass; // scope alias
|
|
||||||
|
|
||||||
// Scope alias of non-Closurized namespace.
|
|
||||||
var NonClosurizedClass = aaa.bbb.NonClosurizedClass;
|
|
||||||
|
|
||||||
var component = new Component(Event.Something);
|
|
||||||
var nonClosurized = NonClosurizedClass();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A created namespace with a really long identifier.
|
|
||||||
* @type {events.Event.<Component,Array<MyClass>}
|
|
||||||
*/
|
|
||||||
Event.
|
|
||||||
MultilineIdentifier.
|
|
||||||
someMethod = function() {};
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEST_SCOPE_SCRIPT = """
|
|
||||||
function foo () {
|
|
||||||
// This goog.scope call is invalid.
|
|
||||||
goog.scope(function() {
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
_TEST_MULTIPLE_SCOPE_SCRIPT = """
|
|
||||||
goog.scope(function() {
|
|
||||||
// do nothing
|
|
||||||
});
|
|
||||||
|
|
||||||
function foo() {
|
|
||||||
var test = goog.scope; // We should not see goog.scope mentioned.
|
|
||||||
}
|
|
||||||
|
|
||||||
// This goog.scope invalid. There can be only one.
|
|
||||||
goog.scope(function() {
|
|
||||||
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Core methods for checking JS files for common style guide violations."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import checkerbase
|
|
||||||
from closure_linter import closurizednamespacesinfo
|
|
||||||
from closure_linter import javascriptlintrules
|
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_list('closurized_namespaces', '',
|
|
||||||
'Namespace prefixes, used for testing of'
|
|
||||||
'goog.provide/require')
|
|
||||||
flags.DEFINE_list('ignored_extra_namespaces', '',
|
|
||||||
'Fully qualified namespaces that should be not be reported '
|
|
||||||
'as extra by the linter.')
|
|
||||||
|
|
||||||
|
|
||||||
class JavaScriptStyleChecker(checkerbase.CheckerBase):
|
|
||||||
"""Checker that applies JavaScriptLintRules."""
|
|
||||||
|
|
||||||
def __init__(self, state_tracker, error_handler):
|
|
||||||
"""Initialize an JavaScriptStyleChecker object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_tracker: State tracker.
|
|
||||||
error_handler: Error handler to pass all errors to.
|
|
||||||
"""
|
|
||||||
self._namespaces_info = None
|
|
||||||
self._alias_pass = None
|
|
||||||
if flags.FLAGS.closurized_namespaces:
|
|
||||||
self._namespaces_info = (
|
|
||||||
closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
flags.FLAGS.closurized_namespaces,
|
|
||||||
flags.FLAGS.ignored_extra_namespaces))
|
|
||||||
|
|
||||||
self._alias_pass = aliaspass.AliasPass(
|
|
||||||
flags.FLAGS.closurized_namespaces, error_handler)
|
|
||||||
|
|
||||||
checkerbase.CheckerBase.__init__(
|
|
||||||
self,
|
|
||||||
error_handler=error_handler,
|
|
||||||
lint_rules=javascriptlintrules.JavaScriptLintRules(
|
|
||||||
self._namespaces_info),
|
|
||||||
state_tracker=state_tracker)
|
|
||||||
|
|
||||||
def Check(self, start_token, limited_doc_checks=False, is_html=False,
|
|
||||||
stop_token=None):
|
|
||||||
"""Checks a token stream for lint warnings/errors.
|
|
||||||
|
|
||||||
Adds a separate pass for computing dependency information based on
|
|
||||||
goog.require and goog.provide statements prior to the main linting pass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: The first token in the token stream.
|
|
||||||
limited_doc_checks: Whether to perform limited checks.
|
|
||||||
is_html: Whether this token stream is HTML.
|
|
||||||
stop_token: If given, checks should stop at this token.
|
|
||||||
"""
|
|
||||||
self._lint_rules.Initialize(self, limited_doc_checks, is_html)
|
|
||||||
|
|
||||||
self._state_tracker.DocFlagPass(start_token, self._error_handler)
|
|
||||||
|
|
||||||
if self._alias_pass:
|
|
||||||
self._alias_pass.Process(start_token)
|
|
||||||
|
|
||||||
# To maximize the amount of errors that get reported before a parse error
|
|
||||||
# is displayed, don't run the dependency pass if a parse error exists.
|
|
||||||
if self._namespaces_info:
|
|
||||||
self._namespaces_info.Reset()
|
|
||||||
self._ExecutePass(start_token, self._DependencyPass, stop_token)
|
|
||||||
|
|
||||||
self._ExecutePass(start_token, self._LintPass, stop_token)
|
|
||||||
|
|
||||||
# If we have a stop_token, we didn't end up reading the whole file and,
|
|
||||||
# thus, don't call Finalize to do end-of-file checks.
|
|
||||||
if not stop_token:
|
|
||||||
self._lint_rules.Finalize(self._state_tracker)
|
|
||||||
|
|
||||||
def _DependencyPass(self, token):
|
|
||||||
"""Processes an individual token for dependency information.
|
|
||||||
|
|
||||||
Used to encapsulate the logic needed to process an individual token so that
|
|
||||||
it can be passed to _ExecutePass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to process.
|
|
||||||
"""
|
|
||||||
self._namespaces_info.ProcessToken(token, self._state_tracker)
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Base classes for writing checkers that operate on tokens."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'jacobr@google.com (Jacob Richman)')
|
|
||||||
|
|
||||||
from closure_linter import errorrules
|
|
||||||
from closure_linter.common import error
|
|
||||||
|
|
||||||
|
|
||||||
class LintRulesBase(object):
|
|
||||||
"""Base class for all classes defining the lint rules for a language."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__checker = None
|
|
||||||
|
|
||||||
def Initialize(self, checker, limited_doc_checks, is_html):
|
|
||||||
"""Initializes to prepare to check a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
checker: Class to report errors to.
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
is_html: Whether the file is an HTML file with extracted contents.
|
|
||||||
"""
|
|
||||||
self.__checker = checker
|
|
||||||
self._limited_doc_checks = limited_doc_checks
|
|
||||||
self._is_html = is_html
|
|
||||||
|
|
||||||
def _HandleError(self, code, message, token, position=None,
|
|
||||||
fix_data=None):
|
|
||||||
"""Call the HandleError function for the checker we are associated with."""
|
|
||||||
if errorrules.ShouldReportError(code):
|
|
||||||
self.__checker.HandleError(code, message, token, position, fix_data)
|
|
||||||
|
|
||||||
def _SetLimitedDocChecks(self, limited_doc_checks):
|
|
||||||
"""Sets whether doc checking is relaxed for this file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
"""
|
|
||||||
self._limited_doc_checks = limited_doc_checks
|
|
||||||
|
|
||||||
def CheckToken(self, token, parser_state):
|
|
||||||
"""Checks a token, given the current parser_state, for warnings and errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration.
|
|
||||||
parser_state: Object that indicates the parser state in the page.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
raise TypeError('Abstract method CheckToken not implemented')
|
|
||||||
|
|
||||||
def Finalize(self, parser_state):
|
|
||||||
"""Perform all checks that need to occur after all lines are processed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parser_state: State of the parser after parsing all tokens
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
raise TypeError('Abstract method Finalize not implemented')
|
|
||||||
|
|
||||||
|
|
||||||
class CheckerBase(object):
|
|
||||||
"""This class handles checking a LintRules object against a file."""
|
|
||||||
|
|
||||||
def __init__(self, error_handler, lint_rules, state_tracker):
|
|
||||||
"""Initialize a checker object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error_handler: Object that handles errors.
|
|
||||||
lint_rules: LintRules object defining lint errors given a token
|
|
||||||
and state_tracker object.
|
|
||||||
state_tracker: Object that tracks the current state in the token stream.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._error_handler = error_handler
|
|
||||||
self._lint_rules = lint_rules
|
|
||||||
self._state_tracker = state_tracker
|
|
||||||
|
|
||||||
self._has_errors = False
|
|
||||||
|
|
||||||
def HandleError(self, code, message, token, position=None,
|
|
||||||
fix_data=None):
|
|
||||||
"""Prints out the given error message including a line number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: The error code.
|
|
||||||
message: The error to print.
|
|
||||||
token: The token where the error occurred, or None if it was a file-wide
|
|
||||||
issue.
|
|
||||||
position: The position of the error, defaults to None.
|
|
||||||
fix_data: Metadata used for fixing the error.
|
|
||||||
"""
|
|
||||||
self._has_errors = True
|
|
||||||
self._error_handler.HandleError(
|
|
||||||
error.Error(code, message, token, position, fix_data))
|
|
||||||
|
|
||||||
def HasErrors(self):
|
|
||||||
"""Returns true if the style checker has found any errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the style checker has found any errors.
|
|
||||||
"""
|
|
||||||
return self._has_errors
|
|
||||||
|
|
||||||
def Check(self, start_token, limited_doc_checks=False, is_html=False,
|
|
||||||
stop_token=None):
|
|
||||||
"""Checks a token stream, reporting errors to the error reporter.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start_token: First token in token stream.
|
|
||||||
limited_doc_checks: Whether doc checking is relaxed for this file.
|
|
||||||
is_html: Whether the file being checked is an HTML file with extracted
|
|
||||||
contents.
|
|
||||||
stop_token: If given, check should stop at this token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._lint_rules.Initialize(self, limited_doc_checks, is_html)
|
|
||||||
self._ExecutePass(start_token, self._LintPass, stop_token=stop_token)
|
|
||||||
self._lint_rules.Finalize(self._state_tracker)
|
|
||||||
|
|
||||||
def _LintPass(self, token):
|
|
||||||
"""Checks an individual token for lint warnings/errors.
|
|
||||||
|
|
||||||
Used to encapsulate the logic needed to check an individual token so that it
|
|
||||||
can be passed to _ExecutePass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to check.
|
|
||||||
"""
|
|
||||||
self._lint_rules.CheckToken(token, self._state_tracker)
|
|
||||||
|
|
||||||
def _ExecutePass(self, token, pass_function, stop_token=None):
|
|
||||||
"""Calls the given function for every token in the given token stream.
|
|
||||||
|
|
||||||
As each token is passed to the given function, state is kept up to date and,
|
|
||||||
depending on the error_trace flag, errors are either caught and reported, or
|
|
||||||
allowed to bubble up so developers can see the full stack trace. If a parse
|
|
||||||
error is specified, the pass will proceed as normal until the token causing
|
|
||||||
the parse error is reached.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token in the token stream.
|
|
||||||
pass_function: The function to call for each token in the token stream.
|
|
||||||
stop_token: The last token to check (if given).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If any error occurred while calling the given function.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self._state_tracker.Reset()
|
|
||||||
while token:
|
|
||||||
# When we are looking at a token and decided to delete the whole line, we
|
|
||||||
# will delete all of them in the "HandleToken()" below. So the current
|
|
||||||
# token and subsequent ones may already be deleted here. The way we
|
|
||||||
# delete a token does not wipe out the previous and next pointers of the
|
|
||||||
# deleted token. So we need to check the token itself to make sure it is
|
|
||||||
# not deleted.
|
|
||||||
if not token.is_deleted:
|
|
||||||
# End the pass at the stop token
|
|
||||||
if stop_token and token is stop_token:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._state_tracker.HandleToken(
|
|
||||||
token, self._state_tracker.GetLastNonSpaceToken())
|
|
||||||
pass_function(token)
|
|
||||||
self._state_tracker.HandleAfterToken(token)
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
@@ -1,578 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Logic for computing dependency information for closurized JavaScript files.
|
|
||||||
|
|
||||||
Closurized JavaScript files express dependencies using goog.require and
|
|
||||||
goog.provide statements. In order for the linter to detect when a statement is
|
|
||||||
missing or unnecessary, all identifiers in the JavaScript file must first be
|
|
||||||
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=g-bad-name
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
DEFAULT_EXTRA_NAMESPACES = [
|
|
||||||
'goog.testing.asserts',
|
|
||||||
'goog.testing.jsunit',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class UsedNamespace(object):
|
|
||||||
"""A type for information about a used namespace."""
|
|
||||||
|
|
||||||
def __init__(self, namespace, identifier, token, alias_definition):
|
|
||||||
"""Initializes the instance.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
namespace: the namespace of an identifier used in the file
|
|
||||||
identifier: the complete identifier
|
|
||||||
token: the token that uses the namespace
|
|
||||||
alias_definition: a boolean stating whether the namespace is only to used
|
|
||||||
for an alias definition and should not be required.
|
|
||||||
"""
|
|
||||||
self.namespace = namespace
|
|
||||||
self.identifier = identifier
|
|
||||||
self.token = token
|
|
||||||
self.alias_definition = alias_definition
|
|
||||||
|
|
||||||
def GetLine(self):
|
|
||||||
return self.token.line_number
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'UsedNamespace(%s)' % ', '.join(
|
|
||||||
['%s=%s' % (k, repr(v)) for k, v in self.__dict__.iteritems()])
|
|
||||||
|
|
||||||
|
|
||||||
class ClosurizedNamespacesInfo(object):
|
|
||||||
"""Dependency information for closurized JavaScript files.
|
|
||||||
|
|
||||||
Processes token streams for dependency creation or usage and provides logic
|
|
||||||
for determining if a given require or provide statement is unnecessary or if
|
|
||||||
there are missing require or provide statements.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, closurized_namespaces, ignored_extra_namespaces):
|
|
||||||
"""Initializes an instance the ClosurizedNamespacesInfo class.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
closurized_namespaces: A list of namespace prefixes that should be
|
|
||||||
processed for dependency information. Non-matching namespaces are
|
|
||||||
ignored.
|
|
||||||
ignored_extra_namespaces: A list of namespaces that should not be reported
|
|
||||||
as extra regardless of whether they are actually used.
|
|
||||||
"""
|
|
||||||
self._closurized_namespaces = closurized_namespaces
|
|
||||||
self._ignored_extra_namespaces = (ignored_extra_namespaces +
|
|
||||||
DEFAULT_EXTRA_NAMESPACES)
|
|
||||||
self.Reset()
|
|
||||||
|
|
||||||
def Reset(self):
|
|
||||||
"""Resets the internal state to prepare for processing a new file."""
|
|
||||||
|
|
||||||
# A list of goog.provide tokens in the order they appeared in the file.
|
|
||||||
self._provide_tokens = []
|
|
||||||
|
|
||||||
# A list of goog.require tokens in the order they appeared in the file.
|
|
||||||
self._require_tokens = []
|
|
||||||
|
|
||||||
# Namespaces that are already goog.provided.
|
|
||||||
self._provided_namespaces = []
|
|
||||||
|
|
||||||
# Namespaces that are already goog.required.
|
|
||||||
self._required_namespaces = []
|
|
||||||
|
|
||||||
# Note that created_namespaces and used_namespaces contain both namespaces
|
|
||||||
# and identifiers because there are many existing cases where a method or
|
|
||||||
# constant is provided directly instead of its namespace. Ideally, these
|
|
||||||
# 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, the second is the identifier itself and the third is
|
|
||||||
# the line number where it's created.
|
|
||||||
self._created_namespaces = []
|
|
||||||
|
|
||||||
# A list of UsedNamespace instances.
|
|
||||||
self._used_namespaces = []
|
|
||||||
|
|
||||||
# A list of seemingly-unnecessary namespaces that are goog.required() and
|
|
||||||
# annotated with @suppress {extraRequire}.
|
|
||||||
self._suppressed_requires = []
|
|
||||||
|
|
||||||
# A list of goog.provide tokens which are duplicates.
|
|
||||||
self._duplicate_provide_tokens = []
|
|
||||||
|
|
||||||
# A list of goog.require tokens which are duplicates.
|
|
||||||
self._duplicate_require_tokens = []
|
|
||||||
|
|
||||||
# Whether this file is in a goog.scope. Someday, we may add support
|
|
||||||
# for checking scopified namespaces, but for now let's just fail
|
|
||||||
# in a more reasonable way.
|
|
||||||
self._scopified_file = False
|
|
||||||
|
|
||||||
# TODO(user): Handle the case where there are 2 different requires
|
|
||||||
# that can satisfy the same dependency, but only one is necessary.
|
|
||||||
|
|
||||||
def GetProvidedNamespaces(self):
|
|
||||||
"""Returns the namespaces which are already provided by this file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of strings where each string is a 'namespace' corresponding to an
|
|
||||||
existing goog.provide statement in the file being checked.
|
|
||||||
"""
|
|
||||||
return set(self._provided_namespaces)
|
|
||||||
|
|
||||||
def GetRequiredNamespaces(self):
|
|
||||||
"""Returns the namespaces which are already required by this file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of strings where each string is a 'namespace' corresponding to an
|
|
||||||
existing goog.require statement in the file being checked.
|
|
||||||
"""
|
|
||||||
return set(self._required_namespaces)
|
|
||||||
|
|
||||||
def IsExtraProvide(self, token):
|
|
||||||
"""Returns whether the given goog.provide token is unnecessary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A goog.provide token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the given token corresponds to an unnecessary goog.provide
|
|
||||||
statement, otherwise False.
|
|
||||||
"""
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
if self.GetClosurizedNamespace(namespace) is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token in self._duplicate_provide_tokens:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO(user): There's probably a faster way to compute this.
|
|
||||||
for created_namespace, created_identifier, _ in self._created_namespaces:
|
|
||||||
if namespace == created_namespace or namespace == created_identifier:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def IsExtraRequire(self, token):
|
|
||||||
"""Returns whether the given goog.require token is unnecessary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: A goog.require token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the given token corresponds to an unnecessary goog.require
|
|
||||||
statement, otherwise False.
|
|
||||||
"""
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
|
|
||||||
if self.GetClosurizedNamespace(namespace) is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if namespace in self._ignored_extra_namespaces:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token in self._duplicate_require_tokens:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if namespace in self._suppressed_requires:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If the namespace contains a component that is initial caps, then that
|
|
||||||
# must be the last component of the namespace.
|
|
||||||
parts = namespace.split('.')
|
|
||||||
if len(parts) > 1 and parts[-2][0].isupper():
|
|
||||||
return True
|
|
||||||
|
|
||||||
# TODO(user): There's probably a faster way to compute this.
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
if (not ns.alias_definition and (
|
|
||||||
namespace == ns.namespace or namespace == ns.identifier)):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def GetMissingProvides(self):
|
|
||||||
"""Returns the dict of missing provided namespaces for the current file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
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 and
|
|
||||||
namespace not in missing_provides):
|
|
||||||
missing_provides[namespace] = line_number
|
|
||||||
|
|
||||||
return missing_provides
|
|
||||||
|
|
||||||
def GetMissingRequires(self):
|
|
||||||
"""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.
|
|
||||||
goog.require statements can satisfy the identifier by requiring either the
|
|
||||||
namespace of the identifier or the identifier itself. goog.provide
|
|
||||||
statements can satisfy the identifier by providing the namespace of the
|
|
||||||
identifier. A created identifier can only satisfy the used identifier if
|
|
||||||
it matches it exactly (necessary since things can be defined on a
|
|
||||||
namespace in more than one file). Note that provided namespaces should be
|
|
||||||
a subset of created namespaces, but we check both because in some cases we
|
|
||||||
can't always detect the creation of the namespace.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
external_dependencies = set(self._required_namespaces)
|
|
||||||
|
|
||||||
# Assume goog namespace is always available.
|
|
||||||
external_dependencies.add('goog')
|
|
||||||
# goog.module is treated as a builtin, too (for goog.module.get).
|
|
||||||
external_dependencies.add('goog.module')
|
|
||||||
|
|
||||||
created_identifiers = set()
|
|
||||||
for unused_namespace, identifier, unused_line_number in (
|
|
||||||
self._created_namespaces):
|
|
||||||
created_identifiers.add(identifier)
|
|
||||||
|
|
||||||
missing_requires = dict()
|
|
||||||
illegal_alias_statements = dict()
|
|
||||||
|
|
||||||
def ShouldRequireNamespace(namespace, identifier):
|
|
||||||
"""Checks if a namespace would normally be required."""
|
|
||||||
return (
|
|
||||||
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 and
|
|
||||||
namespace not in missing_requires)
|
|
||||||
|
|
||||||
# First check all the used identifiers where we know that their namespace
|
|
||||||
# needs to be provided (unless they are optional).
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
namespace = ns.namespace
|
|
||||||
identifier = ns.identifier
|
|
||||||
if (not ns.alias_definition and
|
|
||||||
ShouldRequireNamespace(namespace, identifier)):
|
|
||||||
missing_requires[namespace] = ns.GetLine()
|
|
||||||
|
|
||||||
# Now that all required namespaces are known, we can check if the alias
|
|
||||||
# definitions (that are likely being used for typeannotations that don't
|
|
||||||
# need explicit goog.require statements) are already covered. If not
|
|
||||||
# the user shouldn't use the alias.
|
|
||||||
for ns in self._used_namespaces:
|
|
||||||
if (not ns.alias_definition or
|
|
||||||
not ShouldRequireNamespace(ns.namespace, ns.identifier)):
|
|
||||||
continue
|
|
||||||
if self._FindNamespace(ns.identifier, self._provided_namespaces,
|
|
||||||
created_identifiers, external_dependencies,
|
|
||||||
missing_requires):
|
|
||||||
continue
|
|
||||||
namespace = ns.identifier.rsplit('.', 1)[0]
|
|
||||||
illegal_alias_statements[namespace] = ns.token
|
|
||||||
|
|
||||||
return missing_requires, illegal_alias_statements
|
|
||||||
|
|
||||||
def _FindNamespace(self, identifier, *namespaces_list):
|
|
||||||
"""Finds the namespace of an identifier given a list of other namespaces.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: An identifier whose parent needs to be defined.
|
|
||||||
e.g. for goog.bar.foo we search something that provides
|
|
||||||
goog.bar.
|
|
||||||
*namespaces_list: var args of iterables of namespace identifiers
|
|
||||||
Returns:
|
|
||||||
The namespace that the given identifier is part of or None.
|
|
||||||
"""
|
|
||||||
identifier = identifier.rsplit('.', 1)[0]
|
|
||||||
identifier_prefix = identifier + '.'
|
|
||||||
for namespaces in namespaces_list:
|
|
||||||
for namespace in namespaces:
|
|
||||||
if namespace == identifier or namespace.startswith(identifier_prefix):
|
|
||||||
return namespace
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _IsPrivateIdentifier(self, identifier):
|
|
||||||
"""Returns whether the given identifier is private."""
|
|
||||||
pieces = identifier.split('.')
|
|
||||||
for piece in pieces:
|
|
||||||
if piece.endswith('_'):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def IsFirstProvide(self, token):
|
|
||||||
"""Returns whether token is the first provide token."""
|
|
||||||
return self._provide_tokens and token == self._provide_tokens[0]
|
|
||||||
|
|
||||||
def IsFirstRequire(self, token):
|
|
||||||
"""Returns whether token is the first require token."""
|
|
||||||
return self._require_tokens and token == self._require_tokens[0]
|
|
||||||
|
|
||||||
def IsLastProvide(self, token):
|
|
||||||
"""Returns whether token is the last provide token."""
|
|
||||||
return self._provide_tokens and token == self._provide_tokens[-1]
|
|
||||||
|
|
||||||
def IsLastRequire(self, token):
|
|
||||||
"""Returns whether token is the last require token."""
|
|
||||||
return self._require_tokens and token == self._require_tokens[-1]
|
|
||||||
|
|
||||||
def ProcessToken(self, token, state_tracker):
|
|
||||||
"""Processes the given token for dependency information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to process.
|
|
||||||
state_tracker: The JavaScript state tracker.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Note that this method is in the critical path for the linter and has been
|
|
||||||
# optimized for performance in the following ways:
|
|
||||||
# - Tokens are checked by type first to minimize the number of function
|
|
||||||
# calls necessary to determine if action needs to be taken for the token.
|
|
||||||
# - The most common tokens types are checked for first.
|
|
||||||
# - The number of function calls has been minimized (thus the length of this
|
|
||||||
# function.
|
|
||||||
|
|
||||||
if token.type == TokenType.IDENTIFIER:
|
|
||||||
# TODO(user): Consider saving the whole identifier in metadata.
|
|
||||||
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
|
|
||||||
# identifier, but it was not the first token of the identifier.
|
|
||||||
return
|
|
||||||
|
|
||||||
# In the odd case that a goog.require is encountered inside a function,
|
|
||||||
# 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.GetStringAfterToken(token)
|
|
||||||
if namespace in self._required_namespaces:
|
|
||||||
self._duplicate_require_tokens.append(token)
|
|
||||||
else:
|
|
||||||
self._required_namespaces.append(namespace)
|
|
||||||
|
|
||||||
# If there is a suppression for the require, add a usage for it so it
|
|
||||||
# gets treated as a regular goog.require (i.e. still gets sorted).
|
|
||||||
if self._HasSuppression(state_tracker, 'extraRequire'):
|
|
||||||
self._suppressed_requires.append(namespace)
|
|
||||||
self._AddUsedNamespace(state_tracker, namespace, token)
|
|
||||||
|
|
||||||
elif token.string == 'goog.provide':
|
|
||||||
self._provide_tokens.append(token)
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
if namespace in self._provided_namespaces:
|
|
||||||
self._duplicate_provide_tokens.append(token)
|
|
||||||
else:
|
|
||||||
self._provided_namespaces.append(namespace)
|
|
||||||
|
|
||||||
# If there is a suppression for the provide, add a creation for it so it
|
|
||||||
# gets treated as a regular goog.provide (i.e. still gets sorted).
|
|
||||||
if self._HasSuppression(state_tracker, 'extraProvide'):
|
|
||||||
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
|
|
||||||
elif (token.string == 'goog.module.get' and
|
|
||||||
not self._HasSuppression(state_tracker, 'extraRequire')):
|
|
||||||
# Cannot use _AddUsedNamespace as this is not an identifier, but
|
|
||||||
# already the entire namespace that's required.
|
|
||||||
namespace = tokenutil.GetStringAfterToken(token)
|
|
||||||
namespace = UsedNamespace(namespace, namespace, token,
|
|
||||||
alias_definition=False)
|
|
||||||
self._used_namespaces.append(namespace)
|
|
||||||
if jsdoc and jsdoc.HasFlag('typedef'):
|
|
||||||
self._AddCreatedNamespace(state_tracker, whole_identifier_string,
|
|
||||||
token.line_number,
|
|
||||||
namespace=self.GetClosurizedNamespace(
|
|
||||||
whole_identifier_string))
|
|
||||||
else:
|
|
||||||
is_alias_definition = (token.metadata and
|
|
||||||
token.metadata.is_alias_definition)
|
|
||||||
self._AddUsedNamespace(state_tracker, whole_identifier_string,
|
|
||||||
token, is_alias_definition)
|
|
||||||
|
|
||||||
elif token.type == TokenType.SIMPLE_LVALUE:
|
|
||||||
identifier = token.values['identifier']
|
|
||||||
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)
|
|
||||||
elif namespace and namespace != 'goog':
|
|
||||||
self._AddCreatedNamespace(state_tracker, identifier,
|
|
||||||
token.line_number, namespace=namespace)
|
|
||||||
|
|
||||||
elif token.type == TokenType.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
flag_type = flag.flag_type
|
|
||||||
if flag and flag.HasType() and flag.jstype:
|
|
||||||
is_interface = state_tracker.GetDocComment().HasFlag('interface')
|
|
||||||
if flag_type == 'implements' or (flag_type == 'extends'
|
|
||||||
and is_interface):
|
|
||||||
identifier = flag.jstype.alias or flag.jstype.identifier
|
|
||||||
self._AddUsedNamespace(state_tracker, identifier, token)
|
|
||||||
# Since we process doctypes only for implements and extends, the
|
|
||||||
# type is a simple one and we don't need any iteration for subtypes.
|
|
||||||
|
|
||||||
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
|
|
||||||
not added.
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if not namespace:
|
|
||||||
namespace = identifier
|
|
||||||
|
|
||||||
if self._HasSuppression(state_tracker, 'missingProvide'):
|
|
||||||
return
|
|
||||||
|
|
||||||
self._created_namespaces.append([namespace, identifier, line_number])
|
|
||||||
|
|
||||||
def _AddUsedNamespace(self, state_tracker, identifier, token,
|
|
||||||
is_alias_definition=False):
|
|
||||||
"""Adds the namespace of an identifier to the list of used namespaces.
|
|
||||||
|
|
||||||
If the identifier is annotated with a 'missingRequire' suppression, it is
|
|
||||||
not added.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state_tracker: The JavaScriptStateTracker instance.
|
|
||||||
identifier: An identifier which has been used.
|
|
||||||
token: The token in which the namespace is used.
|
|
||||||
is_alias_definition: If the used namespace is part of an alias_definition.
|
|
||||||
Aliased symbols need their parent namespace to be available, if it is
|
|
||||||
not yet required through another symbol, an error will be thrown.
|
|
||||||
"""
|
|
||||||
if self._HasSuppression(state_tracker, 'missingRequire'):
|
|
||||||
return
|
|
||||||
|
|
||||||
namespace = self.GetClosurizedNamespace(identifier)
|
|
||||||
# b/5362203 If its a variable in scope then its not a required namespace.
|
|
||||||
if namespace and not state_tracker.IsVariableInScope(namespace):
|
|
||||||
namespace = UsedNamespace(namespace, identifier, token,
|
|
||||||
is_alias_definition)
|
|
||||||
self._used_namespaces.append(namespace)
|
|
||||||
|
|
||||||
def _HasSuppression(self, state_tracker, suppression):
|
|
||||||
jsdoc = state_tracker.GetDocComment()
|
|
||||||
return jsdoc and suppression in jsdoc.suppressions
|
|
||||||
|
|
||||||
def GetClosurizedNamespace(self, identifier):
|
|
||||||
"""Given an identifier, returns the namespace that identifier is from.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
identifier: The identifier to extract a namespace from.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The namespace the given identifier resides in, or None if one could not
|
|
||||||
be found.
|
|
||||||
"""
|
|
||||||
if identifier.startswith('goog.global'):
|
|
||||||
# Ignore goog.global, since it is, by definition, global.
|
|
||||||
return None
|
|
||||||
|
|
||||||
parts = identifier.split('.')
|
|
||||||
for namespace in self._closurized_namespaces:
|
|
||||||
if not identifier.startswith(namespace + '.'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# The namespace for a class is the shortest prefix ending in a class
|
|
||||||
# name, which starts with a capital letter but is not a capitalized word.
|
|
||||||
#
|
|
||||||
# We ultimately do not want to allow requiring or providing of inner
|
|
||||||
# classes/enums. Instead, a file should provide only the top-level class
|
|
||||||
# and users should require only that.
|
|
||||||
namespace = []
|
|
||||||
for part in parts:
|
|
||||||
if part == 'prototype' or part.isupper():
|
|
||||||
return '.'.join(namespace)
|
|
||||||
namespace.append(part)
|
|
||||||
if part[0].isupper():
|
|
||||||
return '.'.join(namespace)
|
|
||||||
|
|
||||||
# At this point, we know there's no class or enum, so the namespace is
|
|
||||||
# just the identifier with the last part removed. With the exception of
|
|
||||||
# apply, inherits, and call, which should also be stripped.
|
|
||||||
if parts[-1] in ('apply', 'inherits', 'call'):
|
|
||||||
parts.pop()
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
# If the last part ends with an underscore, it is a private variable,
|
|
||||||
# method, or enum. The namespace is whatever is before it.
|
|
||||||
if parts and parts[-1].endswith('_'):
|
|
||||||
parts.pop()
|
|
||||||
|
|
||||||
return '.'.join(parts)
|
|
||||||
|
|
||||||
return None
|
|
||||||
@@ -1,873 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Unit tests for ClosurizedNamespacesInfo."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import aliaspass
|
|
||||||
from closure_linter import closurizednamespacesinfo
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import testutil
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
# pylint: disable=g-bad-name
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
def _ToLineDict(illegal_alias_stmts):
|
|
||||||
"""Replaces tokens with the respective line number."""
|
|
||||||
return {k: v.line_number for k, v in illegal_alias_stmts.iteritems()}
|
|
||||||
|
|
||||||
|
|
||||||
class ClosurizedNamespacesInfoTest(googletest.TestCase):
|
|
||||||
"""Tests for ClosurizedNamespacesInfo."""
|
|
||||||
|
|
||||||
_test_cases = {
|
|
||||||
'goog.global.anything': None,
|
|
||||||
'package.CONSTANT': 'package',
|
|
||||||
'package.methodName': 'package',
|
|
||||||
'package.subpackage.methodName': 'package.subpackage',
|
|
||||||
'package.subpackage.methodName.apply': 'package.subpackage',
|
|
||||||
'package.ClassName.something': 'package.ClassName',
|
|
||||||
'package.ClassName.Enum.VALUE.methodName': 'package.ClassName',
|
|
||||||
'package.ClassName.CONSTANT': 'package.ClassName',
|
|
||||||
'package.namespace.CONSTANT.methodName': 'package.namespace',
|
|
||||||
'package.ClassName.inherits': 'package.ClassName',
|
|
||||||
'package.ClassName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.methodName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.methodName.call': 'package.ClassName',
|
|
||||||
'package.ClassName.prototype.methodName': 'package.ClassName',
|
|
||||||
'package.ClassName.privateMethod_': 'package.ClassName',
|
|
||||||
'package.className.privateProperty_': 'package.className',
|
|
||||||
'package.className.privateProperty_.methodName': 'package.className',
|
|
||||||
'package.ClassName.PrivateEnum_': 'package.ClassName',
|
|
||||||
'package.ClassName.prototype.methodName.apply': 'package.ClassName',
|
|
||||||
'package.ClassName.property.subProperty': 'package.ClassName',
|
|
||||||
'package.className.prototype.something.somethingElse': 'package.className'
|
|
||||||
}
|
|
||||||
|
|
||||||
def testGetClosurizedNamespace(self):
|
|
||||||
"""Tests that the correct namespace is returned for various identifiers."""
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=['package'], ignored_extra_namespaces=[])
|
|
||||||
for identifier, expected_namespace in self._test_cases.items():
|
|
||||||
actual_namespace = namespaces_info.GetClosurizedNamespace(identifier)
|
|
||||||
self.assertEqual(
|
|
||||||
expected_namespace,
|
|
||||||
actual_namespace,
|
|
||||||
'expected namespace "' + str(expected_namespace) +
|
|
||||||
'" for identifier "' + str(identifier) + '" but was "' +
|
|
||||||
str(actual_namespace) + '"')
|
|
||||||
|
|
||||||
def testIgnoredExtraNamespaces(self):
|
|
||||||
"""Tests that ignored_extra_namespaces are ignored."""
|
|
||||||
token = self._GetRequireTokens('package.Something')
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=['package'],
|
|
||||||
ignored_extra_namespaces=['package.Something'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be valid since it is in ignored namespaces.')
|
|
||||||
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
['package'], [])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be invalid since it is not in ignored namespaces.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_created(self):
|
|
||||||
"""Tests that provides for created namespaces are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_createdIdentifier(self):
|
|
||||||
"""Tests that provides for created identifiers are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo.methodName\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notCreated(self):
|
|
||||||
"""Tests that provides for non-created namespaces are extra."""
|
|
||||||
input_lines = ['goog.provide(\'package.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is not created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notCreatedMultipartClosurizedNamespace(self):
|
|
||||||
"""Tests that provides for non-created namespaces are extra."""
|
|
||||||
input_lines = ['goog.provide(\'multi.part.namespace.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['multi.part'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is not created.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_duplicate(self):
|
|
||||||
"""Tests that providing a namespace twice makes the second one extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
# Advance to the second goog.provide token.
|
|
||||||
token = tokenutil.Search(token.next, TokenType.IDENTIFIER)
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should be extra since it is already provided.')
|
|
||||||
|
|
||||||
def testIsExtraProvide_notClosurized(self):
|
|
||||||
"""Tests that provides of non-closurized namespaces are not extra."""
|
|
||||||
input_lines = ['goog.provide(\'notclosurized.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token),
|
|
||||||
'Should not be extra since it is not closurized.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_used(self):
|
|
||||||
"""Tests that requires for used namespaces are not extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'var x = package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_usedIdentifier(self):
|
|
||||||
"""Tests that requires for used methods on classes are extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.methodName\');',
|
|
||||||
'var x = package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should require the package, not the method specifically.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notUsed(self):
|
|
||||||
"""Tests that requires for unused namespaces are extra."""
|
|
||||||
input_lines = ['goog.require(\'package.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be extra since it is not used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notUsedMultiPartClosurizedNamespace(self):
|
|
||||||
"""Tests unused require with multi-part closurized namespaces."""
|
|
||||||
|
|
||||||
input_lines = ['goog.require(\'multi.part.namespace.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['multi.part'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should be extra since it is not used.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_notClosurized(self):
|
|
||||||
"""Tests that requires of non-closurized namespaces are not extra."""
|
|
||||||
input_lines = ['goog.require(\'notclosurized.Foo\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is not closurized.')
|
|
||||||
|
|
||||||
def testIsExtraRequire_objectOnClass(self):
|
|
||||||
"""Tests that requiring an object on a class is extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.Enum\');',
|
|
||||||
'var x = package.Foo.Enum.VALUE1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'The whole class, not the object, should be required.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_constantOnClass(self):
|
|
||||||
"""Tests that requiring a constant on a class is extra."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.CONSTANT\');',
|
|
||||||
'var x = package.Foo.CONSTANT',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(token),
|
|
||||||
'The class, not the constant, should be required.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_constantNotOnClass(self):
|
|
||||||
"""Tests that requiring a constant not on a class is OK."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.subpackage.CONSTANT\');',
|
|
||||||
'var x = package.subpackage.CONSTANT',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Constants can be required except on classes.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_methodNotOnClass(self):
|
|
||||||
"""Tests that requiring a method not on a class is OK."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.subpackage.method\');',
|
|
||||||
'var x = package.subpackage.method()',
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Methods can be required except on classes.');
|
|
||||||
|
|
||||||
def testIsExtraRequire_defaults(self):
|
|
||||||
"""Tests that there are no warnings about extra requires for test utils"""
|
|
||||||
input_lines = ['goog.require(\'goog.testing.jsunit\');']
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['goog'])
|
|
||||||
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(token),
|
|
||||||
'Should not be extra since it is for testing.')
|
|
||||||
|
|
||||||
def testGetMissingProvides_provided(self):
|
|
||||||
"""Tests that provided functions don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_providedIdentifier(self):
|
|
||||||
"""Tests that provided identifiers don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo.methodName\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_providedParentIdentifier(self):
|
|
||||||
"""Tests that provided identifiers on a class don't cause a missing provide
|
|
||||||
on objects attached to that class."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.foo.ClassName\');',
|
|
||||||
'package.foo.ClassName.methodName = function() {};',
|
|
||||||
'package.foo.ClassName.ObjectName = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_unprovided(self):
|
|
||||||
"""Tests that unprovided functions cause a missing provide."""
|
|
||||||
input_lines = ['package.Foo = function() {};']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
self.assertEquals(1, len(missing_provides))
|
|
||||||
missing_provide = missing_provides.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_provide[0])
|
|
||||||
self.assertEquals(1, missing_provide[1])
|
|
||||||
|
|
||||||
def testGetMissingProvides_privatefunction(self):
|
|
||||||
"""Tests that unprovided private functions don't cause a missing provide."""
|
|
||||||
input_lines = ['package.Foo_ = function() {};']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingProvides_required(self):
|
|
||||||
"""Tests that required namespaces don't cause a missing provide."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
self.assertEquals(0, len(namespaces_info.GetMissingProvides()))
|
|
||||||
|
|
||||||
def testGetMissingRequires_required(self):
|
|
||||||
"""Tests that required namespaces don't cause a missing require."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredIdentifier(self):
|
|
||||||
"""Tests that required namespaces satisfy identifiers on that namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredNamespace(self):
|
|
||||||
"""Tests that required namespaces satisfy the namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.soy.fooTemplate\');',
|
|
||||||
'render(package.soy.fooTemplate);'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_requiredParentClass(self):
|
|
||||||
"""Tests that requiring a parent class of an object is sufficient to prevent
|
|
||||||
a missing require on that object."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();',
|
|
||||||
'package.Foo.methodName(package.Foo.ObjectName);'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_unrequired(self):
|
|
||||||
"""Tests that unrequired namespaces cause a missing require."""
|
|
||||||
input_lines = ['package.Foo();']
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires))
|
|
||||||
missing_req = missing_requires.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_req[0])
|
|
||||||
self.assertEquals(1, missing_req[1])
|
|
||||||
|
|
||||||
def testGetMissingRequires_provided(self):
|
|
||||||
"""Tests that provided namespaces satisfy identifiers on that namespace."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_created(self):
|
|
||||||
"""Tests that created namespaces do not satisfy usage of an identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo = function();',
|
|
||||||
'package.Foo.methodName();',
|
|
||||||
'package.Foo.anotherMethodName1();',
|
|
||||||
'package.Foo.anotherMethodName2();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires))
|
|
||||||
missing_require = missing_requires.popitem()
|
|
||||||
self.assertEquals('package.Foo', missing_require[0])
|
|
||||||
# Make sure line number of first occurrence is reported
|
|
||||||
self.assertEquals(2, missing_require[1])
|
|
||||||
|
|
||||||
def testGetMissingRequires_createdIdentifier(self):
|
|
||||||
"""Tests that created identifiers satisfy usage of the identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo.methodName = function();',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(0, len(missing_requires))
|
|
||||||
|
|
||||||
def testGetMissingRequires_implements(self):
|
|
||||||
"""Tests that a parametrized type requires the correct identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'/** @constructor @implements {package.Bar<T>} */',
|
|
||||||
'package.Foo = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertItemsEqual({'package.Bar': 1}, missing_requires)
|
|
||||||
|
|
||||||
def testGetMissingRequires_objectOnClass(self):
|
|
||||||
"""Tests that we should require a class, not the object on the class."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'package.Foo.Enum\');',
|
|
||||||
'var x = package.Foo.Enum.VALUE1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['package'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(1, len(missing_requires),
|
|
||||||
'The whole class, not the object, should be required.')
|
|
||||||
|
|
||||||
def testGetMissingRequires_variableWithSameName(self):
|
|
||||||
"""Tests that we should not goog.require variables and parameters.
|
|
||||||
|
|
||||||
b/5362203 Variables in scope are not missing namespaces.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'Foo\');',
|
|
||||||
'Foo.A = function();',
|
|
||||||
'Foo.A.prototype.method = function(ab) {',
|
|
||||||
' if (ab) {',
|
|
||||||
' var docs;',
|
|
||||||
' var lvalue = new Obj();',
|
|
||||||
' // Variable in scope hence not goog.require here.',
|
|
||||||
' docs.foo.abc = 1;',
|
|
||||||
' lvalue.next();',
|
|
||||||
' }',
|
|
||||||
' // Since js is function scope this should also not goog.require.',
|
|
||||||
' docs.foo.func();',
|
|
||||||
' // Its not a variable in scope hence goog.require.',
|
|
||||||
' dummy.xyz.reset();',
|
|
||||||
' return this.method2();',
|
|
||||||
'};',
|
|
||||||
'Foo.A.prototype.method1 = function(docs, abcd, xyz) {',
|
|
||||||
' // Parameter hence not goog.require.',
|
|
||||||
' docs.nodes.length = 2;',
|
|
||||||
' lvalue.abc.reset();',
|
|
||||||
'};'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['Foo',
|
|
||||||
'docs',
|
|
||||||
'lvalue',
|
|
||||||
'dummy'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals(2, len(missing_requires))
|
|
||||||
self.assertItemsEqual(
|
|
||||||
{'dummy.xyz': 14,
|
|
||||||
'lvalue.abc': 20}, missing_requires)
|
|
||||||
|
|
||||||
def testIsFirstProvide(self):
|
|
||||||
"""Tests operation of the isFirstProvide method."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'package.Foo\');',
|
|
||||||
'package.Foo.methodName();'
|
|
||||||
]
|
|
||||||
|
|
||||||
token, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
input_lines, ['package'])
|
|
||||||
self.assertTrue(namespaces_info.IsFirstProvide(token))
|
|
||||||
|
|
||||||
def testGetWholeIdentifierString(self):
|
|
||||||
"""Tests that created identifiers satisfy usage of the identifier."""
|
|
||||||
input_lines = [
|
|
||||||
'package.Foo.',
|
|
||||||
' veryLong.',
|
|
||||||
' identifier;'
|
|
||||||
]
|
|
||||||
|
|
||||||
token = testutil.TokenizeSource(input_lines)
|
|
||||||
|
|
||||||
self.assertEquals('package.Foo.veryLong.identifier',
|
|
||||||
tokenutil.GetIdentifierForToken(token))
|
|
||||||
|
|
||||||
self.assertEquals(None,
|
|
||||||
tokenutil.GetIdentifierForToken(token.next))
|
|
||||||
|
|
||||||
def testScopified(self):
|
|
||||||
"""Tests that a goog.scope call is noticed."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertTrue(namespaces_info._scopified_file)
|
|
||||||
|
|
||||||
def testScope_unusedAlias(self):
|
|
||||||
"""Tests that an unused alias symbol is illegal."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_usedMultilevelAlias(self):
|
|
||||||
"""Tests that an used alias symbol in a deep namespace is ok."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.Events\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.Events.DeepNamespace.Event;',
|
|
||||||
'Event();',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_usedAlias(self):
|
|
||||||
"""Tests that aliased symbols result in correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'var dom = goog.dom;',
|
|
||||||
'Event(dom.classes.get);',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
self.assertEquals({'goog.dom.classes': 4, 'goog.events.Event': 4},
|
|
||||||
missing_requires)
|
|
||||||
|
|
||||||
def testModule_alias(self):
|
|
||||||
"""Tests that goog.module style aliases are supported."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.module(\'test.module\');',
|
|
||||||
'var Unused = goog.require(\'goog.Unused\');',
|
|
||||||
'var AliasedClass = goog.require(\'goog.AliasedClass\');',
|
|
||||||
'var x = new AliasedClass();',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
namespaceToken = self._GetRequireTokens('goog.AliasedClass')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(namespaceToken),
|
|
||||||
'AliasedClass should be marked as used')
|
|
||||||
unusedToken = self._GetRequireTokens('goog.Unused')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(unusedToken),
|
|
||||||
'Unused should be marked as not used')
|
|
||||||
|
|
||||||
def testModule_aliasInScope(self):
|
|
||||||
"""Tests that goog.module style aliases are supported."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.module(\'test.module\');',
|
|
||||||
'var AliasedClass = goog.require(\'goog.AliasedClass\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var x = new AliasedClass();',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
namespaceToken = self._GetRequireTokens('goog.AliasedClass')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraRequire(namespaceToken),
|
|
||||||
'AliasedClass should be marked as used')
|
|
||||||
|
|
||||||
def testModule_getAlwaysProvided(self):
|
|
||||||
"""Tests that goog.module.get is recognized as a built-in."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'test.MyClass\');',
|
|
||||||
'goog.require(\'goog.someModule\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var someModule = goog.module.get(\'goog.someModule\');',
|
|
||||||
'test.MyClass = function() {};',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertEquals({}, namespaces_info.GetMissingRequires()[0])
|
|
||||||
|
|
||||||
def testModule_requireForGet(self):
|
|
||||||
"""Tests that goog.module.get needs a goog.require call."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'test.MyClass\');',
|
|
||||||
'function foo() {',
|
|
||||||
' var someModule = goog.module.get(\'goog.someModule\');',
|
|
||||||
' someModule.doSth();',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
self.assertEquals({'goog.someModule': 3},
|
|
||||||
namespaces_info.GetMissingRequires()[0])
|
|
||||||
|
|
||||||
def testScope_usedTypeAlias(self):
|
|
||||||
"""Tests aliased symbols in type annotations."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Event = goog.events.Event;',
|
|
||||||
'/** @type {Event} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_partialAlias_typeOnly(self):
|
|
||||||
"""Tests a partial alias only used in type annotations.
|
|
||||||
|
|
||||||
In this example, some goog.events namespace would need to be required
|
|
||||||
so that evaluating goog.events.bar doesn't throw an error.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Foo} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_partialAlias(self):
|
|
||||||
"""Tests a partial alias in conjunction with a type annotation.
|
|
||||||
|
|
||||||
In this example, the partial alias is already defined by another type,
|
|
||||||
therefore the doc-only type doesn't need to be required.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({'goog.events.bar.EventType': 4}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_partialAliasRequires(self):
|
|
||||||
"""Tests partial aliases with correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.bar.EventType\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_partialAliasRequiresBoth(self):
|
|
||||||
"""Tests partial aliases with correct requires."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.bar.Event\');',
|
|
||||||
'goog.require(\'goog.events.bar.EventType\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var bar = goog.events.bar;',
|
|
||||||
'/** @type {bar.Event} */;',
|
|
||||||
'bar.EventType();'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
event_token = self._GetRequireTokens('goog.events.bar.Event')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraRequire(event_token))
|
|
||||||
|
|
||||||
def testScope_partialAliasNoSubtypeRequires(self):
|
|
||||||
"""Tests that partial aliases don't yield subtype requires (regression)."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.provide(\'goog.events.Foo\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'goog.events.Foo = {};',
|
|
||||||
'var Foo = goog.events.Foo;'
|
|
||||||
'Foo.CssName_ = {};'
|
|
||||||
'var CssName_ = Foo.CssName_;'
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, _ = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
|
|
||||||
def testScope_aliasNamespace(self):
|
|
||||||
"""Tests that an unused alias namespace is not required when available.
|
|
||||||
|
|
||||||
In the example goog.events.Bar is not required, because the namespace
|
|
||||||
goog.events is already defined because goog.events.Foo is required.
|
|
||||||
"""
|
|
||||||
input_lines = [
|
|
||||||
'goog.require(\'goog.events.Foo\');',
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Bar = goog.events.Bar;',
|
|
||||||
'/** @type {Bar} */;',
|
|
||||||
'goog.events.Foo;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testScope_aliasNamespaceIllegal(self):
|
|
||||||
"""Tests that an unused alias namespace is not required when available."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'var Bar = goog.events.Bar;',
|
|
||||||
'/** @type {Bar} */;',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_requires, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, missing_requires)
|
|
||||||
self.assertEquals({'goog.events': 2}, _ToLineDict(illegal_alias_stmts))
|
|
||||||
|
|
||||||
def testScope_provides(self):
|
|
||||||
"""Tests that aliased symbols result in correct provides."""
|
|
||||||
input_lines = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'goog.bar = {};',
|
|
||||||
'var bar = goog.bar;',
|
|
||||||
'bar.Foo = {};',
|
|
||||||
'});'
|
|
||||||
]
|
|
||||||
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript(input_lines, ['goog'])
|
|
||||||
missing_provides = namespaces_info.GetMissingProvides()
|
|
||||||
self.assertEquals({'goog.bar.Foo': 4}, missing_provides)
|
|
||||||
_, illegal_alias_stmts = namespaces_info.GetMissingRequires()
|
|
||||||
self.assertEquals({}, illegal_alias_stmts)
|
|
||||||
|
|
||||||
def testSetTestOnlyNamespaces(self):
|
|
||||||
"""Tests that a namespace in setTestOnly makes it a valid provide."""
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript([
|
|
||||||
'goog.setTestOnly(\'goog.foo.barTest\');'
|
|
||||||
], ['goog'])
|
|
||||||
|
|
||||||
token = self._GetProvideTokens('goog.foo.barTest')
|
|
||||||
self.assertFalse(namespaces_info.IsExtraProvide(token))
|
|
||||||
|
|
||||||
token = self._GetProvideTokens('goog.foo.bazTest')
|
|
||||||
self.assertTrue(namespaces_info.IsExtraProvide(token))
|
|
||||||
|
|
||||||
def testSetTestOnlyComment(self):
|
|
||||||
"""Ensure a comment in setTestOnly does not cause a created namespace."""
|
|
||||||
namespaces_info = self._GetNamespacesInfoForScript([
|
|
||||||
'goog.setTestOnly(\'this is a comment\');'
|
|
||||||
], ['goog'])
|
|
||||||
|
|
||||||
self.assertEquals(
|
|
||||||
[], namespaces_info._created_namespaces,
|
|
||||||
'A comment in setTestOnly should not modify created namespaces.')
|
|
||||||
|
|
||||||
def _GetNamespacesInfoForScript(self, script, closurized_namespaces=None):
|
|
||||||
_, namespaces_info = self._GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
script, closurized_namespaces)
|
|
||||||
|
|
||||||
return namespaces_info
|
|
||||||
|
|
||||||
def _GetStartTokenAndNamespacesInfoForScript(
|
|
||||||
self, script, closurized_namespaces):
|
|
||||||
|
|
||||||
token = testutil.TokenizeSource(script)
|
|
||||||
return token, self._GetInitializedNamespacesInfo(
|
|
||||||
token, closurized_namespaces, [])
|
|
||||||
|
|
||||||
def _GetInitializedNamespacesInfo(self, token, closurized_namespaces,
|
|
||||||
ignored_extra_namespaces):
|
|
||||||
"""Returns a namespaces info initialized with the given token stream."""
|
|
||||||
namespaces_info = closurizednamespacesinfo.ClosurizedNamespacesInfo(
|
|
||||||
closurized_namespaces=closurized_namespaces,
|
|
||||||
ignored_extra_namespaces=ignored_extra_namespaces)
|
|
||||||
state_tracker = javascriptstatetracker.JavaScriptStateTracker()
|
|
||||||
|
|
||||||
ecma_pass = ecmametadatapass.EcmaMetaDataPass()
|
|
||||||
ecma_pass.Process(token)
|
|
||||||
|
|
||||||
state_tracker.DocFlagPass(token, error_handler=None)
|
|
||||||
|
|
||||||
alias_pass = aliaspass.AliasPass(closurized_namespaces)
|
|
||||||
alias_pass.Process(token)
|
|
||||||
|
|
||||||
while token:
|
|
||||||
state_tracker.HandleToken(token, state_tracker.GetLastNonSpaceToken())
|
|
||||||
namespaces_info.ProcessToken(token, state_tracker)
|
|
||||||
state_tracker.HandleAfterToken(token)
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
return namespaces_info
|
|
||||||
|
|
||||||
def _GetProvideTokens(self, namespace):
|
|
||||||
"""Returns a list of tokens for a goog.require of the given namespace."""
|
|
||||||
line_text = 'goog.require(\'' + namespace + '\');\n'
|
|
||||||
return testutil.TokenizeSource([line_text])
|
|
||||||
|
|
||||||
def _GetRequireTokens(self, namespace):
|
|
||||||
"""Returns a list of tokens for a goog.require of the given namespace."""
|
|
||||||
line_text = 'goog.require(\'' + namespace + '\');\n'
|
|
||||||
return testutil.TokenizeSource([line_text])
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Package indicator for gjslint.common."""
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Error object commonly used in linters."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class Error(object):
|
|
||||||
"""Object representing a style error."""
|
|
||||||
|
|
||||||
def __init__(self, code, message, token=None, position=None, fix_data=None):
|
|
||||||
"""Initialize the error object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
code: The numeric error code.
|
|
||||||
message: The error message string.
|
|
||||||
token: The tokens.Token where the error occurred.
|
|
||||||
position: The position of the error within the token.
|
|
||||||
fix_data: Data to be used in autofixing. Codes with fix_data are:
|
|
||||||
GOOG_REQUIRES_NOT_ALPHABETIZED - List of string value tokens that are
|
|
||||||
class names in goog.requires calls.
|
|
||||||
"""
|
|
||||||
self.code = code
|
|
||||||
self.message = message
|
|
||||||
self.token = token
|
|
||||||
self.position = position
|
|
||||||
if token:
|
|
||||||
self.start_index = token.start_index
|
|
||||||
else:
|
|
||||||
self.start_index = 0
|
|
||||||
self.fix_data = fix_data
|
|
||||||
if self.position:
|
|
||||||
self.start_index += self.position.start
|
|
||||||
|
|
||||||
def Compare(a, b):
|
|
||||||
"""Compare two error objects, by source code order.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
a: First error object.
|
|
||||||
b: Second error object.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Negative/0/Positive number when a is before/the same as/after b.
|
|
||||||
"""
|
|
||||||
line_diff = a.token.line_number - b.token.line_number
|
|
||||||
if line_diff:
|
|
||||||
return line_diff
|
|
||||||
|
|
||||||
return a.start_index - b.start_index
|
|
||||||
Compare = staticmethod(Compare)
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Linter error handler class that accumulates an array of errors."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
from closure_linter.common import errorhandler
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorAccumulator(errorhandler.ErrorHandler):
|
|
||||||
"""Error handler object that accumulates errors in a list."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._errors = []
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Append the error to the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
self._errors.append(error)
|
|
||||||
|
|
||||||
def GetErrors(self):
|
|
||||||
"""Returns the accumulated errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of errors.
|
|
||||||
"""
|
|
||||||
return self._errors
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Interface for a linter error handler.
|
|
||||||
|
|
||||||
Error handlers aggregate a set of errors from multiple files and can optionally
|
|
||||||
perform some action based on the reported errors, for example, logging the error
|
|
||||||
or automatically fixing it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorHandler(object):
|
|
||||||
"""Error handler interface."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if self.__class__ == ErrorHandler:
|
|
||||||
raise NotImplementedError('class ErrorHandler is abstract')
|
|
||||||
|
|
||||||
def HandleFile(self, filename, first_token):
|
|
||||||
"""Notifies this ErrorHandler that subsequent errors are in filename.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The file being linted.
|
|
||||||
first_token: The first token of the file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Append the error to the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
|
|
||||||
def FinishFile(self):
|
|
||||||
"""Finishes handling the current file.
|
|
||||||
|
|
||||||
Should be called after all errors in a file have been handled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def GetErrors(self):
|
|
||||||
"""Returns the accumulated errors.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of errors.
|
|
||||||
"""
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Utility functions to format errors."""
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'nnaze@google.com (Nathan Naze)')
|
|
||||||
|
|
||||||
|
|
||||||
def GetUnixErrorOutput(filename, error, new_error=False):
|
|
||||||
"""Get a output line for an error in UNIX format."""
|
|
||||||
|
|
||||||
line = ''
|
|
||||||
|
|
||||||
if error.token:
|
|
||||||
line = '%d' % error.token.line_number
|
|
||||||
|
|
||||||
error_code = '%04d' % error.code
|
|
||||||
if new_error:
|
|
||||||
error_code = 'New Error ' + error_code
|
|
||||||
return '%s:%s:(%s) %s' % (filename, line, error_code, error.message)
|
|
||||||
|
|
||||||
|
|
||||||
def GetErrorOutput(error, new_error=False):
|
|
||||||
"""Get a output line for an error in regular format."""
|
|
||||||
|
|
||||||
line = ''
|
|
||||||
if error.token:
|
|
||||||
line = 'Line %d, ' % error.token.line_number
|
|
||||||
|
|
||||||
code = 'E:%04d' % error.code
|
|
||||||
|
|
||||||
error_message = error.message
|
|
||||||
if new_error:
|
|
||||||
error_message = 'New Error ' + error_message
|
|
||||||
|
|
||||||
return '%s%s: %s' % (line, code, error.message)
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Test case that runs a checker on a file, matching errors against annotations.
|
|
||||||
|
|
||||||
Runs the given checker on the given file, accumulating all errors. The list
|
|
||||||
of errors is then matched against those annotated in the file. Based heavily
|
|
||||||
on devtools/javascript/gpylint/full_test.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
|
|
||||||
class AnnotatedFileTestCase(googletest.TestCase):
|
|
||||||
"""Test case to run a linter against a single file."""
|
|
||||||
|
|
||||||
# Matches an all caps letters + underscores error identifer
|
|
||||||
_MESSAGE = {'msg': '[A-Z][A-Z_]+'}
|
|
||||||
# Matches a //, followed by an optional line number with a +/-, followed by a
|
|
||||||
# list of message IDs. Used to extract expected messages from testdata files.
|
|
||||||
# TODO(robbyw): Generalize to use different commenting patterns.
|
|
||||||
_EXPECTED_RE = re.compile(r'\s*//\s*(?:(?P<line>[+-]?[0-9]+):)?'
|
|
||||||
r'\s*(?P<msgs>%(msg)s(?:,\s*%(msg)s)*)' % _MESSAGE)
|
|
||||||
|
|
||||||
def __init__(self, filename, lint_callable, converter):
|
|
||||||
"""Create a single file lint test case.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to test.
|
|
||||||
lint_callable: Callable that lints a file. This is usually runner.Run().
|
|
||||||
converter: Function taking an error string and returning an error code.
|
|
||||||
"""
|
|
||||||
|
|
||||||
googletest.TestCase.__init__(self, 'runTest')
|
|
||||||
self._filename = filename
|
|
||||||
self._messages = []
|
|
||||||
self._lint_callable = lint_callable
|
|
||||||
self._converter = converter
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = True
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = False
|
|
||||||
|
|
||||||
def shortDescription(self):
|
|
||||||
"""Provides a description for the test."""
|
|
||||||
return 'Run linter on %s' % self._filename
|
|
||||||
|
|
||||||
def runTest(self):
|
|
||||||
"""Runs the test."""
|
|
||||||
try:
|
|
||||||
filename = self._filename
|
|
||||||
stream = open(filename)
|
|
||||||
except IOError as ex:
|
|
||||||
raise IOError('Could not find testdata resource for %s: %s' %
|
|
||||||
(self._filename, ex))
|
|
||||||
|
|
||||||
expected = self._GetExpectedMessages(stream)
|
|
||||||
got = self._ProcessFileAndGetMessages(filename)
|
|
||||||
self.assertEqual(expected, got)
|
|
||||||
|
|
||||||
def _GetExpectedMessages(self, stream):
|
|
||||||
"""Parse a file and get a sorted list of expected messages."""
|
|
||||||
messages = []
|
|
||||||
for i, line in enumerate(stream):
|
|
||||||
match = self._EXPECTED_RE.search(line)
|
|
||||||
if match:
|
|
||||||
line = match.group('line')
|
|
||||||
msg_ids = match.group('msgs')
|
|
||||||
if line is None:
|
|
||||||
line = i + 1
|
|
||||||
elif line.startswith('+') or line.startswith('-'):
|
|
||||||
line = i + 1 + int(line)
|
|
||||||
else:
|
|
||||||
line = int(line)
|
|
||||||
for msg_id in msg_ids.split(','):
|
|
||||||
# Ignore a spurious message from the license preamble.
|
|
||||||
if msg_id != 'WITHOUT':
|
|
||||||
messages.append((line, self._converter(msg_id.strip())))
|
|
||||||
stream.seek(0)
|
|
||||||
messages.sort()
|
|
||||||
return messages
|
|
||||||
|
|
||||||
def _ProcessFileAndGetMessages(self, filename):
|
|
||||||
"""Trap gjslint's output parse it to get messages added."""
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
self._lint_callable(filename, error_accumulator)
|
|
||||||
|
|
||||||
errors = error_accumulator.GetErrors()
|
|
||||||
|
|
||||||
# Convert to expected tuple format.
|
|
||||||
|
|
||||||
error_msgs = [(error.token.line_number, error.code) for error in errors]
|
|
||||||
error_msgs.sort()
|
|
||||||
return error_msgs
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Utilities for dealing with HTML."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)')
|
|
||||||
|
|
||||||
import cStringIO
|
|
||||||
import formatter
|
|
||||||
import htmllib
|
|
||||||
import HTMLParser
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class ScriptExtractor(htmllib.HTMLParser):
|
|
||||||
"""Subclass of HTMLParser that extracts script contents from an HTML file.
|
|
||||||
|
|
||||||
Also inserts appropriate blank lines so that line numbers in the extracted
|
|
||||||
code match the line numbers in the original HTML.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize a ScriptExtractor."""
|
|
||||||
htmllib.HTMLParser.__init__(self, formatter.NullFormatter())
|
|
||||||
self._in_script = False
|
|
||||||
self._text = ''
|
|
||||||
|
|
||||||
def start_script(self, attrs):
|
|
||||||
"""Internal handler for the start of a script tag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
attrs: The attributes of the script tag, as a list of tuples.
|
|
||||||
"""
|
|
||||||
for attribute in attrs:
|
|
||||||
if attribute[0].lower() == 'src':
|
|
||||||
# Skip script tags with a src specified.
|
|
||||||
return
|
|
||||||
self._in_script = True
|
|
||||||
|
|
||||||
def end_script(self):
|
|
||||||
"""Internal handler for the end of a script tag."""
|
|
||||||
self._in_script = False
|
|
||||||
|
|
||||||
def handle_data(self, data):
|
|
||||||
"""Internal handler for character data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The character data from the HTML file.
|
|
||||||
"""
|
|
||||||
if self._in_script:
|
|
||||||
# If the last line contains whitespace only, i.e. is just there to
|
|
||||||
# properly align a </script> tag, strip the whitespace.
|
|
||||||
if data.rstrip(' \t') != data.rstrip(' \t\n\r\f'):
|
|
||||||
data = data.rstrip(' \t')
|
|
||||||
self._text += data
|
|
||||||
else:
|
|
||||||
self._AppendNewlines(data)
|
|
||||||
|
|
||||||
def handle_comment(self, data):
|
|
||||||
"""Internal handler for HTML comments.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The text of the comment.
|
|
||||||
"""
|
|
||||||
self._AppendNewlines(data)
|
|
||||||
|
|
||||||
def _AppendNewlines(self, data):
|
|
||||||
"""Count the number of newlines in the given string and append them.
|
|
||||||
|
|
||||||
This ensures line numbers are correct for reported errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: The data to count newlines in.
|
|
||||||
"""
|
|
||||||
# We append 'x' to both sides of the string to ensure that splitlines
|
|
||||||
# gives us an accurate count.
|
|
||||||
for i in xrange(len(('x' + data + 'x').splitlines()) - 1):
|
|
||||||
self._text += '\n'
|
|
||||||
|
|
||||||
def GetScriptLines(self):
|
|
||||||
"""Return the extracted script lines.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The extracted script lines as a list of strings.
|
|
||||||
"""
|
|
||||||
return self._text.splitlines()
|
|
||||||
|
|
||||||
|
|
||||||
def GetScriptLines(f):
|
|
||||||
"""Extract script tag contents from the given HTML file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
f: The HTML file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Lines in the HTML file that are from script tags.
|
|
||||||
"""
|
|
||||||
extractor = ScriptExtractor()
|
|
||||||
|
|
||||||
# The HTML parser chokes on text like Array.<!string>, so we patch
|
|
||||||
# that bug by replacing the < with < - escaping all text inside script
|
|
||||||
# tags would be better but it's a bit of a catch 22.
|
|
||||||
contents = f.read()
|
|
||||||
contents = re.sub(r'<([^\s\w/])',
|
|
||||||
lambda x: '<%s' % x.group(1),
|
|
||||||
contents)
|
|
||||||
|
|
||||||
extractor.feed(contents)
|
|
||||||
extractor.close()
|
|
||||||
return extractor.GetScriptLines()
|
|
||||||
|
|
||||||
|
|
||||||
def StripTags(str):
|
|
||||||
"""Returns the string with HTML tags stripped.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
str: An html string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The html string with all tags stripped. If there was a parse error, returns
|
|
||||||
the text successfully parsed so far.
|
|
||||||
"""
|
|
||||||
# Brute force approach to stripping as much HTML as possible. If there is a
|
|
||||||
# parsing error, don't strip text before parse error position, and continue
|
|
||||||
# trying from there.
|
|
||||||
final_text = ''
|
|
||||||
finished = False
|
|
||||||
while not finished:
|
|
||||||
try:
|
|
||||||
strip = _HtmlStripper()
|
|
||||||
strip.feed(str)
|
|
||||||
strip.close()
|
|
||||||
str = strip.get_output()
|
|
||||||
final_text += str
|
|
||||||
finished = True
|
|
||||||
except HTMLParser.HTMLParseError, e:
|
|
||||||
final_text += str[:e.offset]
|
|
||||||
str = str[e.offset + 1:]
|
|
||||||
|
|
||||||
return final_text
|
|
||||||
|
|
||||||
|
|
||||||
class _HtmlStripper(HTMLParser.HTMLParser):
|
|
||||||
"""Simple class to strip tags from HTML.
|
|
||||||
|
|
||||||
Does so by doing nothing when encountering tags, and appending character data
|
|
||||||
to a buffer when that is encountered.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.reset()
|
|
||||||
self.__output = cStringIO.StringIO()
|
|
||||||
|
|
||||||
def handle_data(self, d):
|
|
||||||
self.__output.write(d)
|
|
||||||
|
|
||||||
def get_output(self):
|
|
||||||
return self.__output.getvalue()
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Interface for a lint running wrapper."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class LintRunner(object):
|
|
||||||
"""Interface for a lint running wrapper."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if self.__class__ == LintRunner:
|
|
||||||
raise NotImplementedError('class LintRunner is abstract')
|
|
||||||
|
|
||||||
def Run(self, filenames, error_handler):
|
|
||||||
"""Run a linter on the given filenames.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filenames: The filenames to check
|
|
||||||
error_handler: An ErrorHandler object
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The error handler, which may have been used to collect error info.
|
|
||||||
"""
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Regular expression based JavaScript matcher classes."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
from closure_linter.common import position
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Token = tokens.Token
|
|
||||||
Position = position.Position
|
|
||||||
|
|
||||||
|
|
||||||
class Matcher(object):
|
|
||||||
"""A token matcher.
|
|
||||||
|
|
||||||
Specifies a pattern to match, the type of token it represents, what mode the
|
|
||||||
token changes to, and what mode the token applies to.
|
|
||||||
|
|
||||||
Modes allow more advanced grammars to be incorporated, and are also necessary
|
|
||||||
to tokenize line by line. We can have different patterns apply to different
|
|
||||||
modes - i.e. looking for documentation while in comment mode.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
regex: The regular expression representing this matcher.
|
|
||||||
type: The type of token indicated by a successful match.
|
|
||||||
result_mode: The mode to move to after a successful match.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, regex, token_type, result_mode=None, line_start=False):
|
|
||||||
"""Create a new matcher template.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
regex: The regular expression to match.
|
|
||||||
token_type: The type of token a successful match indicates.
|
|
||||||
result_mode: What mode to change to after a successful match. Defaults to
|
|
||||||
None, which means to not change the current mode.
|
|
||||||
line_start: Whether this matcher should only match string at the start
|
|
||||||
of a line.
|
|
||||||
"""
|
|
||||||
self.regex = regex
|
|
||||||
self.type = token_type
|
|
||||||
self.result_mode = result_mode
|
|
||||||
self.line_start = line_start
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Classes to represent positions within strings."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class Position(object):
|
|
||||||
"""Object representing a segment of a string.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
start: The index in to the string where the segment starts.
|
|
||||||
length: The length of the string segment.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, start, length):
|
|
||||||
"""Initialize the position object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
start: The start index.
|
|
||||||
length: The number of characters to include.
|
|
||||||
"""
|
|
||||||
self.start = start
|
|
||||||
self.length = length
|
|
||||||
|
|
||||||
def Get(self, string):
|
|
||||||
"""Returns this range of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to slice.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The string within the range specified by this object.
|
|
||||||
"""
|
|
||||||
return string[self.start:self.start + self.length]
|
|
||||||
|
|
||||||
def Set(self, target, source):
|
|
||||||
"""Sets this range within the target string to the source string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target: The target string.
|
|
||||||
source: The source string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The resulting string
|
|
||||||
"""
|
|
||||||
return target[:self.start] + source + target[self.start + self.length:]
|
|
||||||
|
|
||||||
def AtEnd(string):
|
|
||||||
"""Create a Position representing the end of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to represent the end of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(len(string), 0)
|
|
||||||
AtEnd = staticmethod(AtEnd)
|
|
||||||
|
|
||||||
def IsAtEnd(self, string):
|
|
||||||
"""Returns whether this position is at the end of the given string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to test for the end of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether this position is at the end of the given string.
|
|
||||||
"""
|
|
||||||
return self.start == len(string) and self.length == 0
|
|
||||||
|
|
||||||
def AtBeginning():
|
|
||||||
"""Create a Position representing the beginning of any string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(0, 0)
|
|
||||||
AtBeginning = staticmethod(AtBeginning)
|
|
||||||
|
|
||||||
def IsAtBeginning(self):
|
|
||||||
"""Returns whether this position is at the beginning of any string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether this position is at the beginning of any string.
|
|
||||||
"""
|
|
||||||
return self.start == 0 and self.length == 0
|
|
||||||
|
|
||||||
def All(string):
|
|
||||||
"""Create a Position representing the entire string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string to represent the entirety of.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(0, len(string))
|
|
||||||
All = staticmethod(All)
|
|
||||||
|
|
||||||
def Index(index):
|
|
||||||
"""Returns a Position object for the specified index.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
index: The index to select, inclusively.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The created Position object.
|
|
||||||
"""
|
|
||||||
return Position(index, 1)
|
|
||||||
Index = staticmethod(Index)
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Determines the list of files to be checked from command line arguments."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import glob
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
flags.DEFINE_multistring(
|
|
||||||
'recurse',
|
|
||||||
None,
|
|
||||||
'Recurse in to the subdirectories of the given path',
|
|
||||||
short_name='r')
|
|
||||||
flags.DEFINE_list(
|
|
||||||
'exclude_directories',
|
|
||||||
('_demos'),
|
|
||||||
'Exclude the specified directories (only applicable along with -r or '
|
|
||||||
'--presubmit)',
|
|
||||||
short_name='e')
|
|
||||||
flags.DEFINE_list(
|
|
||||||
'exclude_files',
|
|
||||||
('deps.js'),
|
|
||||||
'Exclude the specified files',
|
|
||||||
short_name='x')
|
|
||||||
|
|
||||||
|
|
||||||
def MatchesSuffixes(filename, suffixes):
|
|
||||||
"""Returns whether the given filename matches one of the given suffixes.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Filename to check.
|
|
||||||
suffixes: Sequence of suffixes to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the given filename matches one of the given suffixes.
|
|
||||||
"""
|
|
||||||
suffix = filename[filename.rfind('.'):]
|
|
||||||
return suffix in suffixes
|
|
||||||
|
|
||||||
|
|
||||||
def _GetUserSpecifiedFiles(argv, suffixes):
|
|
||||||
"""Returns files to be linted, specified directly on the command line.
|
|
||||||
|
|
||||||
Can handle the '*' wildcard in filenames, but no other wildcards.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments. The second and following arguments
|
|
||||||
are assumed to be files that should be linted.
|
|
||||||
suffixes: Expected suffixes for the file type being checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A sequence of files to be linted.
|
|
||||||
"""
|
|
||||||
files = argv[1:] or []
|
|
||||||
all_files = []
|
|
||||||
lint_files = []
|
|
||||||
|
|
||||||
# Perform any necessary globs.
|
|
||||||
for f in files:
|
|
||||||
if f.find('*') != -1:
|
|
||||||
for result in glob.glob(f):
|
|
||||||
all_files.append(result)
|
|
||||||
else:
|
|
||||||
all_files.append(f)
|
|
||||||
|
|
||||||
for f in all_files:
|
|
||||||
if MatchesSuffixes(f, suffixes):
|
|
||||||
lint_files.append(f)
|
|
||||||
return lint_files
|
|
||||||
|
|
||||||
|
|
||||||
def _GetRecursiveFiles(suffixes):
|
|
||||||
"""Returns files to be checked specified by the --recurse flag.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
suffixes: Expected suffixes for the file type being checked.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of files to be checked.
|
|
||||||
"""
|
|
||||||
lint_files = []
|
|
||||||
# Perform any request recursion
|
|
||||||
if FLAGS.recurse:
|
|
||||||
for start in FLAGS.recurse:
|
|
||||||
for root, subdirs, files in os.walk(start):
|
|
||||||
for f in files:
|
|
||||||
if MatchesSuffixes(f, suffixes):
|
|
||||||
lint_files.append(os.path.join(root, f))
|
|
||||||
return lint_files
|
|
||||||
|
|
||||||
|
|
||||||
def GetAllSpecifiedFiles(argv, suffixes):
|
|
||||||
"""Returns all files specified by the user on the commandline.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments. The second and following arguments
|
|
||||||
are assumed to be files that should be linted.
|
|
||||||
suffixes: Expected suffixes for the file type
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of all files specified directly or indirectly (via flags) on the
|
|
||||||
command line by the user.
|
|
||||||
"""
|
|
||||||
files = _GetUserSpecifiedFiles(argv, suffixes)
|
|
||||||
|
|
||||||
if FLAGS.recurse:
|
|
||||||
files += _GetRecursiveFiles(suffixes)
|
|
||||||
|
|
||||||
return FilterFiles(files)
|
|
||||||
|
|
||||||
|
|
||||||
def FilterFiles(files):
|
|
||||||
"""Filters the list of files to be linted be removing any excluded files.
|
|
||||||
|
|
||||||
Filters out files excluded using --exclude_files and --exclude_directories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
files: Sequence of files that needs filtering.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Filtered list of files to be linted.
|
|
||||||
"""
|
|
||||||
num_files = len(files)
|
|
||||||
|
|
||||||
ignore_dirs_regexs = []
|
|
||||||
for ignore in FLAGS.exclude_directories:
|
|
||||||
ignore_dirs_regexs.append(re.compile(r'(^|[\\/])%s[\\/]' % ignore))
|
|
||||||
|
|
||||||
result_files = []
|
|
||||||
for f in files:
|
|
||||||
add_file = True
|
|
||||||
for exclude in FLAGS.exclude_files:
|
|
||||||
if f.endswith('/' + exclude) or f == exclude:
|
|
||||||
add_file = False
|
|
||||||
break
|
|
||||||
for ignore in ignore_dirs_regexs:
|
|
||||||
if ignore.search(f):
|
|
||||||
# Break out of ignore loop so we don't add to
|
|
||||||
# filtered files.
|
|
||||||
add_file = False
|
|
||||||
break
|
|
||||||
if add_file:
|
|
||||||
# Convert everything to absolute paths so we can easily remove duplicates
|
|
||||||
# using a set.
|
|
||||||
result_files.append(os.path.abspath(f))
|
|
||||||
|
|
||||||
skipped = num_files - len(result_files)
|
|
||||||
if skipped:
|
|
||||||
print 'Skipping %d file(s).' % skipped
|
|
||||||
|
|
||||||
return set(result_files)
|
|
||||||
|
|
||||||
|
|
||||||
def GetFileList(argv, file_type, suffixes):
|
|
||||||
"""Parse the flags and return the list of files to check.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
suffixes: Sequence of acceptable suffixes for the file type.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The list of files to check.
|
|
||||||
"""
|
|
||||||
return sorted(GetAllSpecifiedFiles(argv, suffixes))
|
|
||||||
|
|
||||||
|
|
||||||
def IsEmptyArgumentList(argv):
|
|
||||||
return not (len(argv[1:]) or FLAGS.recurse)
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Regular expression based lexer."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Type = tokens.TokenType
|
|
||||||
|
|
||||||
|
|
||||||
class Tokenizer(object):
|
|
||||||
"""General purpose tokenizer.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
mode: The latest mode of the tokenizer. This allows patterns to distinguish
|
|
||||||
if they are mid-comment, mid-parameter list, etc.
|
|
||||||
matchers: Dictionary of modes to sequences of matchers that define the
|
|
||||||
patterns to check at any given time.
|
|
||||||
default_types: Dictionary of modes to types, defining what type to give
|
|
||||||
non-matched text when in the given mode. Defaults to Type.NORMAL.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, starting_mode, matchers, default_types):
|
|
||||||
"""Initialize the tokenizer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
starting_mode: Mode to start in.
|
|
||||||
matchers: Dictionary of modes to sequences of matchers that defines the
|
|
||||||
patterns to check at any given time.
|
|
||||||
default_types: Dictionary of modes to types, defining what type to give
|
|
||||||
non-matched text when in the given mode. Defaults to Type.NORMAL.
|
|
||||||
"""
|
|
||||||
self.__starting_mode = starting_mode
|
|
||||||
self.matchers = matchers
|
|
||||||
self.default_types = default_types
|
|
||||||
|
|
||||||
def TokenizeFile(self, file):
|
|
||||||
"""Tokenizes the given file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
file: An iterable that yields one line of the file at a time.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The first token in the file
|
|
||||||
"""
|
|
||||||
# The current mode.
|
|
||||||
self.mode = self.__starting_mode
|
|
||||||
# The first token in the stream.
|
|
||||||
self.__first_token = None
|
|
||||||
# The last token added to the token stream.
|
|
||||||
self.__last_token = None
|
|
||||||
# The current line number.
|
|
||||||
self.__line_number = 0
|
|
||||||
|
|
||||||
for line in file:
|
|
||||||
self.__line_number += 1
|
|
||||||
self.__TokenizeLine(line)
|
|
||||||
|
|
||||||
return self.__first_token
|
|
||||||
|
|
||||||
def _CreateToken(self, string, token_type, line, line_number, values=None):
|
|
||||||
"""Creates a new Token object (or subclass).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string of input the token represents.
|
|
||||||
token_type: The type of token.
|
|
||||||
line: The text of the line this token is in.
|
|
||||||
line_number: The line number of the token.
|
|
||||||
values: A dict of named values within the token. For instance, a
|
|
||||||
function declaration may have a value called 'name' which captures the
|
|
||||||
name of the function.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The newly created Token object.
|
|
||||||
"""
|
|
||||||
return tokens.Token(string, token_type, line, line_number, values,
|
|
||||||
line_number)
|
|
||||||
|
|
||||||
def __TokenizeLine(self, line):
|
|
||||||
"""Tokenizes the given line.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line: The contents of the line.
|
|
||||||
"""
|
|
||||||
string = line.rstrip('\n\r\f')
|
|
||||||
line_number = self.__line_number
|
|
||||||
self.__start_index = 0
|
|
||||||
|
|
||||||
if not string:
|
|
||||||
self.__AddToken(self._CreateToken('', Type.BLANK_LINE, line, line_number))
|
|
||||||
return
|
|
||||||
|
|
||||||
normal_token = ''
|
|
||||||
index = 0
|
|
||||||
while index < len(string):
|
|
||||||
for matcher in self.matchers[self.mode]:
|
|
||||||
if matcher.line_start and index > 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
match = matcher.regex.match(string, index)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
if normal_token:
|
|
||||||
self.__AddToken(
|
|
||||||
self.__CreateNormalToken(self.mode, normal_token, line,
|
|
||||||
line_number))
|
|
||||||
normal_token = ''
|
|
||||||
|
|
||||||
# Add the match.
|
|
||||||
self.__AddToken(self._CreateToken(match.group(), matcher.type, line,
|
|
||||||
line_number, match.groupdict()))
|
|
||||||
|
|
||||||
# Change the mode to the correct one for after this match.
|
|
||||||
self.mode = matcher.result_mode or self.mode
|
|
||||||
|
|
||||||
# Shorten the string to be matched.
|
|
||||||
index = match.end()
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
# If the for loop finishes naturally (i.e. no matches) we just add the
|
|
||||||
# first character to the string of consecutive non match characters.
|
|
||||||
# These will constitute a NORMAL token.
|
|
||||||
if string:
|
|
||||||
normal_token += string[index:index + 1]
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
if normal_token:
|
|
||||||
self.__AddToken(
|
|
||||||
self.__CreateNormalToken(self.mode, normal_token, line, line_number))
|
|
||||||
|
|
||||||
def __CreateNormalToken(self, mode, string, line, line_number):
|
|
||||||
"""Creates a normal token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mode: The current mode.
|
|
||||||
string: The string to tokenize.
|
|
||||||
line: The line of text.
|
|
||||||
line_number: The line number within the file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A Token object, of the default type for the current mode.
|
|
||||||
"""
|
|
||||||
type = Type.NORMAL
|
|
||||||
if mode in self.default_types:
|
|
||||||
type = self.default_types[mode]
|
|
||||||
return self._CreateToken(string, type, line, line_number)
|
|
||||||
|
|
||||||
def __AddToken(self, token):
|
|
||||||
"""Add the given token to the token stream.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to add.
|
|
||||||
"""
|
|
||||||
# Store the first token, or point the previous token to this one.
|
|
||||||
if not self.__first_token:
|
|
||||||
self.__first_token = token
|
|
||||||
else:
|
|
||||||
self.__last_token.next = token
|
|
||||||
|
|
||||||
# Establish the doubly linked list
|
|
||||||
token.previous = self.__last_token
|
|
||||||
self.__last_token = token
|
|
||||||
|
|
||||||
# Compute the character indices
|
|
||||||
token.start_index = self.__start_index
|
|
||||||
self.__start_index += token.length
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Classes to represent tokens and positions within them."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
class TokenType(object):
|
|
||||||
"""Token types common to all languages."""
|
|
||||||
NORMAL = 'normal'
|
|
||||||
WHITESPACE = 'whitespace'
|
|
||||||
BLANK_LINE = 'blank line'
|
|
||||||
|
|
||||||
|
|
||||||
class Token(object):
|
|
||||||
"""Token class for intelligent text splitting.
|
|
||||||
|
|
||||||
The token class represents a string of characters and an identifying type.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The type of token.
|
|
||||||
string: The characters the token comprises.
|
|
||||||
length: The length of the token.
|
|
||||||
line: The text of the line the token is found in.
|
|
||||||
line_number: The number of the line the token is found in.
|
|
||||||
values: Dictionary of values returned from the tokens regex match.
|
|
||||||
previous: The token before this one.
|
|
||||||
next: The token after this one.
|
|
||||||
start_index: The character index in the line where this token starts.
|
|
||||||
attached_object: Object containing more information about this token.
|
|
||||||
metadata: Object containing metadata about this token. Must be added by
|
|
||||||
a separate metadata pass.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, string, token_type, line, line_number, values=None,
|
|
||||||
orig_line_number=None):
|
|
||||||
"""Creates a new Token object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string: The string of input the token contains.
|
|
||||||
token_type: The type of token.
|
|
||||||
line: The text of the line this token is in.
|
|
||||||
line_number: The line number of the token.
|
|
||||||
values: A dict of named values within the token. For instance, a
|
|
||||||
function declaration may have a value called 'name' which captures the
|
|
||||||
name of the function.
|
|
||||||
orig_line_number: The line number of the original file this token comes
|
|
||||||
from. This should be only set during the tokenization process. For newly
|
|
||||||
created error fix tokens after that, it should be None.
|
|
||||||
"""
|
|
||||||
self.type = token_type
|
|
||||||
self.string = string
|
|
||||||
self.length = len(string)
|
|
||||||
self.line = line
|
|
||||||
self.line_number = line_number
|
|
||||||
self.orig_line_number = orig_line_number
|
|
||||||
self.values = values
|
|
||||||
self.is_deleted = False
|
|
||||||
|
|
||||||
# These parts can only be computed when the file is fully tokenized
|
|
||||||
self.previous = None
|
|
||||||
self.next = None
|
|
||||||
self.start_index = None
|
|
||||||
|
|
||||||
# This part is set in statetracker.py
|
|
||||||
# TODO(robbyw): Wrap this in to metadata
|
|
||||||
self.attached_object = None
|
|
||||||
|
|
||||||
# This part is set in *metadatapass.py
|
|
||||||
self.metadata = None
|
|
||||||
|
|
||||||
def IsFirstInLine(self):
|
|
||||||
"""Tests if this token is the first token in its line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the token is the first token in its line.
|
|
||||||
"""
|
|
||||||
return not self.previous or self.previous.line_number != self.line_number
|
|
||||||
|
|
||||||
def IsLastInLine(self):
|
|
||||||
"""Tests if this token is the last token in its line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether the token is the last token in its line.
|
|
||||||
"""
|
|
||||||
return not self.next or self.next.line_number != self.line_number
|
|
||||||
|
|
||||||
def IsType(self, token_type):
|
|
||||||
"""Tests if this token is of the given type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_type: The type to test for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the type of this token matches the type passed in.
|
|
||||||
"""
|
|
||||||
return self.type == token_type
|
|
||||||
|
|
||||||
def IsAnyType(self, *token_types):
|
|
||||||
"""Tests if this token is any of the given types.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token_types: The types to check. Also accepts a single array.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the type of this token is any of the types passed in.
|
|
||||||
"""
|
|
||||||
if not isinstance(token_types[0], basestring):
|
|
||||||
return self.type in token_types[0]
|
|
||||||
else:
|
|
||||||
return self.type in token_types
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Token: %s, "%s", %r, %d, %r>' % (self.type, self.string,
|
|
||||||
self.values, self.line_number,
|
|
||||||
self.metadata)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Returns a token iterator."""
|
|
||||||
node = self
|
|
||||||
while node:
|
|
||||||
yield node
|
|
||||||
node = node.next
|
|
||||||
|
|
||||||
def __reversed__(self):
|
|
||||||
"""Returns a reverse-direction token iterator."""
|
|
||||||
node = self
|
|
||||||
while node:
|
|
||||||
yield node
|
|
||||||
node = node.previous
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
|
|
||||||
__author__ = 'nnaze@google.com (Nathan Naze)'
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter.common import tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _CreateDummyToken():
|
|
||||||
return tokens.Token('foo', None, 1, 1)
|
|
||||||
|
|
||||||
|
|
||||||
def _CreateDummyTokens(count):
|
|
||||||
dummy_tokens = []
|
|
||||||
for _ in xrange(count):
|
|
||||||
dummy_tokens.append(_CreateDummyToken())
|
|
||||||
return dummy_tokens
|
|
||||||
|
|
||||||
|
|
||||||
def _SetTokensAsNeighbors(neighbor_tokens):
|
|
||||||
for i in xrange(len(neighbor_tokens)):
|
|
||||||
prev_index = i - 1
|
|
||||||
next_index = i + 1
|
|
||||||
|
|
||||||
if prev_index >= 0:
|
|
||||||
neighbor_tokens[i].previous = neighbor_tokens[prev_index]
|
|
||||||
|
|
||||||
if next_index < len(neighbor_tokens):
|
|
||||||
neighbor_tokens[i].next = neighbor_tokens[next_index]
|
|
||||||
|
|
||||||
|
|
||||||
class TokensTest(googletest.TestCase):
|
|
||||||
|
|
||||||
def testIsFirstInLine(self):
|
|
||||||
|
|
||||||
# First token in file (has no previous).
|
|
||||||
self.assertTrue(_CreateDummyToken().IsFirstInLine())
|
|
||||||
|
|
||||||
a, b = _CreateDummyTokens(2)
|
|
||||||
_SetTokensAsNeighbors([a, b])
|
|
||||||
|
|
||||||
# Tokens on same line
|
|
||||||
a.line_number = 30
|
|
||||||
b.line_number = 30
|
|
||||||
|
|
||||||
self.assertFalse(b.IsFirstInLine())
|
|
||||||
|
|
||||||
# Tokens on different lines
|
|
||||||
b.line_number = 31
|
|
||||||
self.assertTrue(b.IsFirstInLine())
|
|
||||||
|
|
||||||
def testIsLastInLine(self):
|
|
||||||
# Last token in file (has no next).
|
|
||||||
self.assertTrue(_CreateDummyToken().IsLastInLine())
|
|
||||||
|
|
||||||
a, b = _CreateDummyTokens(2)
|
|
||||||
_SetTokensAsNeighbors([a, b])
|
|
||||||
|
|
||||||
# Tokens on same line
|
|
||||||
a.line_number = 30
|
|
||||||
b.line_number = 30
|
|
||||||
self.assertFalse(a.IsLastInLine())
|
|
||||||
|
|
||||||
b.line_number = 31
|
|
||||||
self.assertTrue(a.IsLastInLine())
|
|
||||||
|
|
||||||
def testIsType(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertTrue(a.IsType('fakeType1'))
|
|
||||||
self.assertFalse(a.IsType('fakeType2'))
|
|
||||||
|
|
||||||
def testIsAnyType(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertTrue(a.IsAnyType(['fakeType1', 'fakeType2']))
|
|
||||||
self.assertFalse(a.IsAnyType(['fakeType3', 'fakeType4']))
|
|
||||||
|
|
||||||
def testRepr(self):
|
|
||||||
a = tokens.Token('foo', 'fakeType1', 1, 1)
|
|
||||||
self.assertEquals('<Token: fakeType1, "foo", None, 1, None>', str(a))
|
|
||||||
|
|
||||||
def testIter(self):
|
|
||||||
dummy_tokens = _CreateDummyTokens(5)
|
|
||||||
_SetTokensAsNeighbors(dummy_tokens)
|
|
||||||
a, b, c, d, e = dummy_tokens
|
|
||||||
|
|
||||||
i = iter(a)
|
|
||||||
self.assertListEqual([a, b, c, d, e], list(i))
|
|
||||||
|
|
||||||
def testReverseIter(self):
|
|
||||||
dummy_tokens = _CreateDummyTokens(5)
|
|
||||||
_SetTokensAsNeighbors(dummy_tokens)
|
|
||||||
a, b, c, d, e = dummy_tokens
|
|
||||||
|
|
||||||
ri = reversed(e)
|
|
||||||
self.assertListEqual([e, d, c, b, a], list(ri))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,844 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Core methods for checking EcmaScript files for common style guide violations.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'jacobr@google.com (Jacob Richman)')
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import checkerbase
|
|
||||||
from closure_linter import ecmametadatapass
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errorrules
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import indentation
|
|
||||||
from closure_linter import javascripttokenizer
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import statetracker
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import error
|
|
||||||
from closure_linter.common import position
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow')
|
|
||||||
# TODO(user): When flipping this to True, remove logic from unit tests
|
|
||||||
# that overrides this flag.
|
|
||||||
flags.DEFINE_boolean('dot_on_next_line', False, 'Require dots to be'
|
|
||||||
'placed on the next line for wrapped expressions')
|
|
||||||
|
|
||||||
# TODO(robbyw): Check for extra parens on return statements
|
|
||||||
# TODO(robbyw): Check for 0px in strings
|
|
||||||
# TODO(robbyw): Ensure inline jsDoc is in {}
|
|
||||||
# TODO(robbyw): Check for valid JS types in parameter docs
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Context = ecmametadatapass.EcmaContext
|
|
||||||
Error = error.Error
|
|
||||||
Modes = javascripttokenizer.JavaScriptModes
|
|
||||||
Position = position.Position
|
|
||||||
Rule = error_check.Rule
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaScriptLintRules(checkerbase.LintRulesBase):
|
|
||||||
"""EmcaScript lint style checking rules.
|
|
||||||
|
|
||||||
Can be used to find common style errors in JavaScript, ActionScript and other
|
|
||||||
Ecma like scripting languages. Style checkers for Ecma scripting languages
|
|
||||||
should inherit from this style checker.
|
|
||||||
Please do not add any state to EcmaScriptLintRules or to any subclasses.
|
|
||||||
|
|
||||||
All state should be added to the StateTracker subclass used for a particular
|
|
||||||
language.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# It will be initialized in constructor so the flags are initialized.
|
|
||||||
max_line_length = -1
|
|
||||||
|
|
||||||
# Static constants.
|
|
||||||
MISSING_PARAMETER_SPACE = re.compile(r',\S')
|
|
||||||
|
|
||||||
EXTRA_SPACE = re.compile(r'(\(\s|\s\))')
|
|
||||||
|
|
||||||
ENDS_WITH_SPACE = re.compile(r'\s$')
|
|
||||||
|
|
||||||
ILLEGAL_TAB = re.compile(r'\t')
|
|
||||||
|
|
||||||
# Regex used to split up complex types to check for invalid use of ? and |.
|
|
||||||
TYPE_SPLIT = re.compile(r'[,<>()]')
|
|
||||||
|
|
||||||
# Regex for form of author lines after the @author tag.
|
|
||||||
AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)')
|
|
||||||
|
|
||||||
# Acceptable tokens to remove for line too long testing.
|
|
||||||
LONG_LINE_IGNORE = frozenset(
|
|
||||||
['*', '//', '@see'] +
|
|
||||||
['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE])
|
|
||||||
|
|
||||||
JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([
|
|
||||||
'@fileoverview', '@param', '@return', '@returns'])
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize this lint rule object."""
|
|
||||||
checkerbase.LintRulesBase.__init__(self)
|
|
||||||
if EcmaScriptLintRules.max_line_length == -1:
|
|
||||||
EcmaScriptLintRules.max_line_length = errorrules.GetMaxLineLength()
|
|
||||||
|
|
||||||
def Initialize(self, checker, limited_doc_checks, is_html):
|
|
||||||
"""Initialize this lint rule object before parsing a new file."""
|
|
||||||
checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks,
|
|
||||||
is_html)
|
|
||||||
self._indentation = indentation.IndentationRules()
|
|
||||||
|
|
||||||
def HandleMissingParameterDoc(self, token, param_name):
|
|
||||||
"""Handle errors associated with a parameter missing a @param tag."""
|
|
||||||
raise TypeError('Abstract method HandleMissingParameterDoc not implemented')
|
|
||||||
|
|
||||||
def _CheckLineLength(self, last_token, state):
|
|
||||||
"""Checks whether the line is too long.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
last_token: The last token in the line.
|
|
||||||
state: parser_state object that indicates the current state in the page
|
|
||||||
"""
|
|
||||||
# Start from the last token so that we have the flag object attached to
|
|
||||||
# and DOC_FLAG tokens.
|
|
||||||
line_number = last_token.line_number
|
|
||||||
token = last_token
|
|
||||||
|
|
||||||
# Build a representation of the string where spaces indicate potential
|
|
||||||
# line-break locations.
|
|
||||||
line = []
|
|
||||||
while token and token.line_number == line_number:
|
|
||||||
if state.IsTypeToken(token):
|
|
||||||
line.insert(0, 'x' * len(token.string))
|
|
||||||
elif token.type in (Type.IDENTIFIER, Type.OPERATOR):
|
|
||||||
# Dots are acceptable places to wrap (may be tokenized as identifiers).
|
|
||||||
line.insert(0, token.string.replace('.', ' '))
|
|
||||||
else:
|
|
||||||
line.insert(0, token.string)
|
|
||||||
token = token.previous
|
|
||||||
|
|
||||||
line = ''.join(line)
|
|
||||||
line = line.rstrip('\n\r\f')
|
|
||||||
try:
|
|
||||||
length = len(unicode(line, 'utf-8'))
|
|
||||||
except (LookupError, UnicodeDecodeError):
|
|
||||||
# Unknown encoding. The line length may be wrong, as was originally the
|
|
||||||
# case for utf-8 (see bug 1735846). For now just accept the default
|
|
||||||
# length, but as we find problems we can either add test for other
|
|
||||||
# possible encodings or return without an error to protect against
|
|
||||||
# false positives at the cost of more false negatives.
|
|
||||||
length = len(line)
|
|
||||||
|
|
||||||
if length > EcmaScriptLintRules.max_line_length:
|
|
||||||
|
|
||||||
# If the line matches one of the exceptions, then it's ok.
|
|
||||||
for long_line_regexp in self.GetLongLineExceptions():
|
|
||||||
if long_line_regexp.match(last_token.line):
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the line consists of only one "word", or multiple words but all
|
|
||||||
# except one are ignoreable, then it's ok.
|
|
||||||
parts = set(line.split())
|
|
||||||
|
|
||||||
# We allow two "words" (type and name) when the line contains @param
|
|
||||||
max_parts = 1
|
|
||||||
if '@param' in parts:
|
|
||||||
max_parts = 2
|
|
||||||
|
|
||||||
# Custom tags like @requires may have url like descriptions, so ignore
|
|
||||||
# the tag, similar to how we handle @see.
|
|
||||||
custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags])
|
|
||||||
if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags))
|
|
||||||
> max_parts):
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_TOO_LONG,
|
|
||||||
'Line too long (%d characters).' % len(line), last_token)
|
|
||||||
|
|
||||||
def _CheckJsDocType(self, token, js_type):
|
|
||||||
"""Checks the given type for style errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The DOC_FLAG token for the flag whose type to check.
|
|
||||||
js_type: The flag's typeannotation.TypeAnnotation instance.
|
|
||||||
"""
|
|
||||||
if not js_type: return
|
|
||||||
|
|
||||||
if js_type.type_group and len(js_type.sub_types) == 2:
|
|
||||||
identifiers = [t.identifier for t in js_type.sub_types]
|
|
||||||
if 'null' in identifiers:
|
|
||||||
# Don't warn if the identifier is a template type (e.g. {TYPE|null}.
|
|
||||||
if not identifiers[0].isupper() and not identifiers[1].isupper():
|
|
||||||
self._HandleError(
|
|
||||||
errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL,
|
|
||||||
'Prefer "?Type" to "Type|null": "%s"' % js_type, token)
|
|
||||||
|
|
||||||
# TODO(user): We should report an error for wrong usage of '?' and '|'
|
|
||||||
# e.g. {?number|string|null} etc.
|
|
||||||
|
|
||||||
for sub_type in js_type.IterTypes():
|
|
||||||
self._CheckJsDocType(token, sub_type)
|
|
||||||
|
|
||||||
def _CheckForMissingSpaceBeforeToken(self, token):
|
|
||||||
"""Checks for a missing space at the beginning of a token.
|
|
||||||
|
|
||||||
Reports a MISSING_SPACE error if the token does not begin with a space or
|
|
||||||
the previous token doesn't end with a space and the previous token is on the
|
|
||||||
same line as the token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token being checked
|
|
||||||
"""
|
|
||||||
# TODO(user): Check if too many spaces?
|
|
||||||
if (len(token.string) == len(token.string.lstrip()) and
|
|
||||||
token.previous and token.line_number == token.previous.line_number and
|
|
||||||
len(token.previous.string) - len(token.previous.string.rstrip()) == 0):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SPACE,
|
|
||||||
'Missing space before "%s"' % token.string,
|
|
||||||
token,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
def _CheckOperator(self, token):
|
|
||||||
"""Checks an operator for spacing and line style.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The operator token.
|
|
||||||
"""
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
|
|
||||||
if not self._ExpectSpaceBeforeOperator(token):
|
|
||||||
if (token.previous and token.previous.type == Type.WHITESPACE and
|
|
||||||
last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER) and
|
|
||||||
last_code.line_number == token.line_number):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "%s"' % token.string,
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif (token.previous and
|
|
||||||
not token.previous.IsComment() and
|
|
||||||
not tokenutil.IsDot(token) and
|
|
||||||
token.previous.type in Type.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._HandleError(errors.MISSING_SPACE,
|
|
||||||
'Missing space before "%s"' % token.string, token,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
# Check wrapping of operators.
|
|
||||||
next_code = tokenutil.GetNextCodeToken(token)
|
|
||||||
|
|
||||||
is_dot = tokenutil.IsDot(token)
|
|
||||||
wrapped_before = last_code and last_code.line_number != token.line_number
|
|
||||||
wrapped_after = next_code and next_code.line_number != token.line_number
|
|
||||||
|
|
||||||
if FLAGS.dot_on_next_line and is_dot and wrapped_after:
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_ENDS_WITH_DOT,
|
|
||||||
'"." must go on the following line',
|
|
||||||
token)
|
|
||||||
if (not is_dot and wrapped_before and
|
|
||||||
not token.metadata.IsUnaryOperator()):
|
|
||||||
self._HandleError(
|
|
||||||
errors.LINE_STARTS_WITH_OPERATOR,
|
|
||||||
'Binary operator must go on previous line "%s"' % token.string,
|
|
||||||
token)
|
|
||||||
|
|
||||||
def _IsLabel(self, token):
|
|
||||||
# A ':' token is considered part of a label if it occurs in a case
|
|
||||||
# statement, a plain label, or an object literal, i.e. is not part of a
|
|
||||||
# ternary.
|
|
||||||
|
|
||||||
return (token.string == ':' and
|
|
||||||
token.metadata.context.type in (Context.LITERAL_ELEMENT,
|
|
||||||
Context.CASE_BLOCK,
|
|
||||||
Context.STATEMENT))
|
|
||||||
|
|
||||||
def _ExpectSpaceBeforeOperator(self, token):
|
|
||||||
"""Returns whether a space should appear before the given operator token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The operator token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Whether there should be a space before the token.
|
|
||||||
"""
|
|
||||||
if token.string == ',' or token.metadata.IsUnaryPostOperator():
|
|
||||||
return False
|
|
||||||
|
|
||||||
if tokenutil.IsDot(token):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Colons should appear in labels, object literals, the case of a switch
|
|
||||||
# statement, and ternary operator. Only want a space in the case of the
|
|
||||||
# ternary operator.
|
|
||||||
if self._IsLabel(token):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if token.metadata.IsUnaryOperator() and token.IsFirstInLine():
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def CheckToken(self, token, state):
|
|
||||||
"""Checks a token, given the current parser_state, for warnings and errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The current token under consideration
|
|
||||||
state: parser_state object that indicates the current state in the page
|
|
||||||
"""
|
|
||||||
# Store some convenience variables
|
|
||||||
first_in_line = token.IsFirstInLine()
|
|
||||||
last_in_line = token.IsLastInLine()
|
|
||||||
last_non_space_token = state.GetLastNonSpaceToken()
|
|
||||||
|
|
||||||
token_type = token.type
|
|
||||||
|
|
||||||
# Process the line change.
|
|
||||||
if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION):
|
|
||||||
# TODO(robbyw): Support checking indentation in HTML files.
|
|
||||||
indentation_errors = self._indentation.CheckToken(token, state)
|
|
||||||
for indentation_error in indentation_errors:
|
|
||||||
self._HandleError(*indentation_error)
|
|
||||||
|
|
||||||
if last_in_line:
|
|
||||||
self._CheckLineLength(token, state)
|
|
||||||
|
|
||||||
if token_type == Type.PARAMETERS:
|
|
||||||
# Find missing spaces in parameter lists.
|
|
||||||
if self.MISSING_PARAMETER_SPACE.search(token.string):
|
|
||||||
fix_data = ', '.join([s.strip() for s in token.string.split(',')])
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space after ","',
|
|
||||||
token, position=None, fix_data=fix_data.strip())
|
|
||||||
|
|
||||||
# Find extra spaces at the beginning of parameter lists. Make sure
|
|
||||||
# we aren't at the beginning of a continuing multi-line list.
|
|
||||||
if not first_in_line:
|
|
||||||
space_count = len(token.string) - len(token.string.lstrip())
|
|
||||||
if space_count:
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("',
|
|
||||||
token, position=Position(0, space_count))
|
|
||||||
|
|
||||||
elif (token_type == Type.START_BLOCK and
|
|
||||||
token.metadata.context.type == Context.BLOCK):
|
|
||||||
self._CheckForMissingSpaceBeforeToken(token)
|
|
||||||
|
|
||||||
elif token_type == Type.END_BLOCK:
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if state.InFunction() and state.IsFunctionClose():
|
|
||||||
if state.InTopLevelFunction():
|
|
||||||
# A semicolons should not be included at the end of a function
|
|
||||||
# declaration.
|
|
||||||
if not state.InAssignedFunction():
|
|
||||||
if not last_in_line and token.next.type == Type.SEMICOLON:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
'Illegal semicolon after function declaration',
|
|
||||||
token.next, position=Position.All(token.next.string))
|
|
||||||
|
|
||||||
# A semicolon should be included at the end of a function expression
|
|
||||||
# that is not immediately called or used by a dot operator.
|
|
||||||
if (state.InAssignedFunction() and token.next
|
|
||||||
and token.next.type != Type.SEMICOLON):
|
|
||||||
next_token = tokenutil.GetNextCodeToken(token)
|
|
||||||
is_immediately_used = (next_token.type == Type.START_PAREN or
|
|
||||||
tokenutil.IsDot(next_token))
|
|
||||||
if not is_immediately_used:
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
'Missing semicolon after function assigned to a variable',
|
|
||||||
token, position=Position.AtEnd(token.string))
|
|
||||||
|
|
||||||
if state.InInterfaceMethod() and last_code.type != Type.START_BLOCK:
|
|
||||||
self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE,
|
|
||||||
'Interface methods cannot contain code', last_code)
|
|
||||||
|
|
||||||
elif (state.IsBlockClose() and
|
|
||||||
token.next and token.next.type == Type.SEMICOLON):
|
|
||||||
if (last_code.metadata.context.parent.type != Context.OBJECT_LITERAL
|
|
||||||
and last_code.metadata.context.type != Context.OBJECT_LITERAL):
|
|
||||||
self._HandleError(
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
'No semicolon is required to end a code block',
|
|
||||||
token.next, position=Position.All(token.next.string))
|
|
||||||
|
|
||||||
elif token_type == Type.SEMICOLON:
|
|
||||||
if token.previous and token.previous.type == Type.WHITESPACE:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before ";"',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
if token.next and token.next.line_number == token.line_number:
|
|
||||||
if token.metadata.context.type != Context.FOR_GROUP_BLOCK:
|
|
||||||
# TODO(robbyw): Error about no multi-statement lines.
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif token.next.type not in (
|
|
||||||
Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_SPACE,
|
|
||||||
'Missing space after ";" in for statement',
|
|
||||||
token.next,
|
|
||||||
position=Position.AtBeginning())
|
|
||||||
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if last_code and last_code.type == Type.SEMICOLON:
|
|
||||||
# Allow a single double semi colon in for loops for cases like:
|
|
||||||
# for (;;) { }.
|
|
||||||
# NOTE(user): This is not a perfect check, and will not throw an error
|
|
||||||
# for cases like: for (var i = 0;; i < n; i++) {}, but then your code
|
|
||||||
# probably won't work either.
|
|
||||||
for_token = tokenutil.CustomSearch(
|
|
||||||
last_code,
|
|
||||||
lambda token: token.type == Type.KEYWORD and token.string == 'for',
|
|
||||||
end_func=lambda token: token.type == Type.SEMICOLON,
|
|
||||||
distance=None,
|
|
||||||
reverse=True)
|
|
||||||
|
|
||||||
if not for_token:
|
|
||||||
self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
|
|
||||||
elif token_type == Type.START_PAREN:
|
|
||||||
# Ensure that opening parentheses have a space before any keyword
|
|
||||||
# that is not being invoked like a member function.
|
|
||||||
if (token.previous and token.previous.type == Type.KEYWORD and
|
|
||||||
(not token.previous.metadata or
|
|
||||||
not token.previous.metadata.last_code or
|
|
||||||
not token.previous.metadata.last_code.string or
|
|
||||||
token.previous.metadata.last_code.string[-1:] != '.')):
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space before "("',
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
elif token.previous and token.previous.type == Type.WHITESPACE:
|
|
||||||
before_space = token.previous.previous
|
|
||||||
# Ensure that there is no extra space before a function invocation,
|
|
||||||
# even if the function being invoked happens to be a keyword.
|
|
||||||
if (before_space and before_space.line_number == token.line_number and
|
|
||||||
before_space.type == Type.IDENTIFIER or
|
|
||||||
(before_space.type == Type.KEYWORD and before_space.metadata and
|
|
||||||
before_space.metadata.last_code and
|
|
||||||
before_space.metadata.last_code.string and
|
|
||||||
before_space.metadata.last_code.string[-1:] == '.')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "("',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif token_type == Type.START_BRACKET:
|
|
||||||
self._HandleStartBracket(token, last_non_space_token)
|
|
||||||
elif token_type in (Type.END_PAREN, Type.END_BRACKET):
|
|
||||||
# Ensure there is no space before closing parentheses, except when
|
|
||||||
# it's in a for statement with an omitted section, or when it's at the
|
|
||||||
# beginning of a line.
|
|
||||||
if (token.previous and token.previous.type == Type.WHITESPACE and
|
|
||||||
not token.previous.IsFirstInLine() and
|
|
||||||
not (last_non_space_token and last_non_space_token.line_number ==
|
|
||||||
token.line_number and
|
|
||||||
last_non_space_token.type == Type.SEMICOLON)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "%s"' %
|
|
||||||
token.string, token.previous,
|
|
||||||
position=Position.All(token.previous.string))
|
|
||||||
|
|
||||||
elif token_type == Type.WHITESPACE:
|
|
||||||
if self.ILLEGAL_TAB.search(token.string):
|
|
||||||
if token.IsFirstInLine():
|
|
||||||
if token.next:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace before "%s"' % token.next.string,
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
else:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
else:
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in whitespace after "%s"' % token.previous.string,
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
|
|
||||||
# Check whitespace length if it's not the first token of the line and
|
|
||||||
# if it's not immediately before a comment.
|
|
||||||
if last_in_line:
|
|
||||||
# Check for extra whitespace at the end of a line.
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
|
|
||||||
token, position=Position.All(token.string))
|
|
||||||
elif not first_in_line and not token.next.IsComment():
|
|
||||||
if token.length > 1:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space after "%s"' %
|
|
||||||
token.previous.string, token,
|
|
||||||
position=Position(1, len(token.string) - 1))
|
|
||||||
|
|
||||||
elif token_type == Type.OPERATOR:
|
|
||||||
self._CheckOperator(token)
|
|
||||||
elif token_type == Type.DOC_FLAG:
|
|
||||||
flag = token.attached_object
|
|
||||||
|
|
||||||
if flag.flag_type == 'bug':
|
|
||||||
# TODO(robbyw): Check for exactly 1 space on the left.
|
|
||||||
string = token.next.string.lstrip()
|
|
||||||
string = string.split(' ', 1)[0]
|
|
||||||
|
|
||||||
if not string.isdigit():
|
|
||||||
self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG,
|
|
||||||
'@bug should be followed by a bug number', token)
|
|
||||||
|
|
||||||
elif flag.flag_type == 'suppress':
|
|
||||||
if flag.type is None:
|
|
||||||
# A syntactically invalid suppress tag will get tokenized as a normal
|
|
||||||
# flag, indicating an error.
|
|
||||||
self._HandleError(
|
|
||||||
errors.INCORRECT_SUPPRESS_SYNTAX,
|
|
||||||
'Invalid suppress syntax: should be @suppress {errortype}. '
|
|
||||||
'Spaces matter.', token)
|
|
||||||
else:
|
|
||||||
for suppress_type in flag.jstype.IterIdentifiers():
|
|
||||||
if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES:
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_SUPPRESS_TYPE,
|
|
||||||
'Invalid suppression type: %s' % suppress_type, token)
|
|
||||||
|
|
||||||
elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and
|
|
||||||
flag.flag_type == 'author'):
|
|
||||||
# TODO(user): In non strict mode check the author tag for as much as
|
|
||||||
# it exists, though the full form checked below isn't required.
|
|
||||||
string = token.next.string
|
|
||||||
result = self.AUTHOR_SPEC.match(string)
|
|
||||||
if not result:
|
|
||||||
self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION,
|
|
||||||
'Author tag line should be of the form: '
|
|
||||||
'@author foo@somewhere.com (Your Name)',
|
|
||||||
token.next)
|
|
||||||
else:
|
|
||||||
# Check spacing between email address and name. Do this before
|
|
||||||
# checking earlier spacing so positions are easier to calculate for
|
|
||||||
# autofixing.
|
|
||||||
num_spaces = len(result.group(2))
|
|
||||||
if num_spaces < 1:
|
|
||||||
self._HandleError(errors.MISSING_SPACE,
|
|
||||||
'Missing space after email address',
|
|
||||||
token.next, position=Position(result.start(2), 0))
|
|
||||||
elif num_spaces > 1:
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space after email address',
|
|
||||||
token.next,
|
|
||||||
position=Position(result.start(2) + 1, num_spaces - 1))
|
|
||||||
|
|
||||||
# Check for extra spaces before email address. Can't be too few, if
|
|
||||||
# not at least one we wouldn't match @author tag.
|
|
||||||
num_spaces = len(result.group(1))
|
|
||||||
if num_spaces > 1:
|
|
||||||
self._HandleError(errors.EXTRA_SPACE,
|
|
||||||
'Extra space before email address',
|
|
||||||
token.next, position=Position(1, num_spaces - 1))
|
|
||||||
|
|
||||||
elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and
|
|
||||||
not self._limited_doc_checks):
|
|
||||||
if flag.flag_type == 'param':
|
|
||||||
if flag.name is None:
|
|
||||||
self._HandleError(errors.MISSING_JSDOC_PARAM_NAME,
|
|
||||||
'Missing name in @param tag', token)
|
|
||||||
|
|
||||||
if not flag.description or flag.description is None:
|
|
||||||
flag_name = token.type
|
|
||||||
if 'name' in token.values:
|
|
||||||
flag_name = '@' + token.values['name']
|
|
||||||
|
|
||||||
if flag_name not in self.JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED:
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_JSDOC_TAG_DESCRIPTION,
|
|
||||||
'Missing description in %s tag' % flag_name, token)
|
|
||||||
else:
|
|
||||||
self._CheckForMissingSpaceBeforeToken(flag.description_start_token)
|
|
||||||
|
|
||||||
if flag.HasType():
|
|
||||||
if flag.type_start_token is not None:
|
|
||||||
self._CheckForMissingSpaceBeforeToken(
|
|
||||||
token.attached_object.type_start_token)
|
|
||||||
|
|
||||||
if flag.jstype and not flag.jstype.IsEmpty():
|
|
||||||
self._CheckJsDocType(token, flag.jstype)
|
|
||||||
|
|
||||||
if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and (
|
|
||||||
flag.type_start_token.type != Type.DOC_START_BRACE or
|
|
||||||
flag.type_end_token.type != Type.DOC_END_BRACE):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_BRACES_AROUND_TYPE,
|
|
||||||
'Type must always be surrounded by curly braces.', token)
|
|
||||||
|
|
||||||
if token_type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
|
|
||||||
if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and
|
|
||||||
token.values['name'] not in FLAGS.custom_jsdoc_tags):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_JSDOC_TAG,
|
|
||||||
'Invalid JsDoc tag: %s' % token.values['name'], token)
|
|
||||||
|
|
||||||
if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and
|
|
||||||
token.values['name'] == 'inheritDoc' and
|
|
||||||
token_type == Type.DOC_INLINE_FLAG):
|
|
||||||
self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC,
|
|
||||||
'Unnecessary braces around @inheritDoc',
|
|
||||||
token)
|
|
||||||
|
|
||||||
elif token_type == Type.SIMPLE_LVALUE:
|
|
||||||
identifier = token.values['identifier']
|
|
||||||
|
|
||||||
if ((not state.InFunction() or state.InConstructor()) and
|
|
||||||
state.InTopLevel() and not state.InObjectLiteralDescendant()):
|
|
||||||
jsdoc = state.GetDocComment()
|
|
||||||
if not state.HasDocComment(identifier):
|
|
||||||
# Only test for documentation on identifiers with .s in them to
|
|
||||||
# avoid checking things like simple variables. We don't require
|
|
||||||
# documenting assignments to .prototype itself (bug 1880803).
|
|
||||||
if (not state.InConstructor() and
|
|
||||||
identifier.find('.') != -1 and not
|
|
||||||
identifier.endswith('.prototype') and not
|
|
||||||
self._limited_doc_checks):
|
|
||||||
comment = state.GetLastComment()
|
|
||||||
if not (comment and comment.lower().count('jsdoc inherited')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_MEMBER_DOCUMENTATION,
|
|
||||||
"No docs found for member '%s'" % identifier,
|
|
||||||
token)
|
|
||||||
elif jsdoc and (not state.InConstructor() or
|
|
||||||
identifier.startswith('this.')):
|
|
||||||
# We are at the top level and the function/member is documented.
|
|
||||||
if identifier.endswith('_') and not identifier.endswith('__'):
|
|
||||||
# Can have a private class which inherits documentation from a
|
|
||||||
# public superclass.
|
|
||||||
#
|
|
||||||
# @inheritDoc is deprecated in favor of using @override, and they
|
|
||||||
if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor')
|
|
||||||
and ('accessControls' not in jsdoc.suppressions)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_OVERRIDE_PRIVATE,
|
|
||||||
'%s should not override a private member.' % identifier,
|
|
||||||
jsdoc.GetFlag('override').flag_token)
|
|
||||||
if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor')
|
|
||||||
and ('accessControls' not in jsdoc.suppressions)):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_INHERIT_DOC_PRIVATE,
|
|
||||||
'%s should not inherit from a private member.' % identifier,
|
|
||||||
jsdoc.GetFlag('inheritDoc').flag_token)
|
|
||||||
if (not jsdoc.HasFlag('private') and
|
|
||||||
('underscore' not in jsdoc.suppressions) and not
|
|
||||||
((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and
|
|
||||||
('accessControls' in jsdoc.suppressions))):
|
|
||||||
self._HandleError(
|
|
||||||
errors.MISSING_PRIVATE,
|
|
||||||
'Member "%s" must have @private JsDoc.' %
|
|
||||||
identifier, token)
|
|
||||||
if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions:
|
|
||||||
self._HandleError(
|
|
||||||
errors.UNNECESSARY_SUPPRESS,
|
|
||||||
'@suppress {underscore} is not necessary with @private',
|
|
||||||
jsdoc.suppressions['underscore'])
|
|
||||||
elif (jsdoc.HasFlag('private') and
|
|
||||||
not self.InExplicitlyTypedLanguage()):
|
|
||||||
# It is convention to hide public fields in some ECMA
|
|
||||||
# implementations from documentation using the @private tag.
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_PRIVATE,
|
|
||||||
'Member "%s" must not have @private JsDoc' %
|
|
||||||
identifier, token)
|
|
||||||
|
|
||||||
# These flags are only legal on localizable message definitions;
|
|
||||||
# such variables always begin with the prefix MSG_.
|
|
||||||
for f in ('desc', 'hidden', 'meaning'):
|
|
||||||
if (jsdoc.HasFlag(f)
|
|
||||||
and not identifier.startswith('MSG_')
|
|
||||||
and identifier.find('.MSG_') == -1):
|
|
||||||
self._HandleError(
|
|
||||||
errors.INVALID_USE_OF_DESC_TAG,
|
|
||||||
'Member "%s" should not have @%s JsDoc' % (identifier, f),
|
|
||||||
token)
|
|
||||||
|
|
||||||
# Check for illegaly assigning live objects as prototype property values.
|
|
||||||
index = identifier.find('.prototype.')
|
|
||||||
# Ignore anything with additional .s after the prototype.
|
|
||||||
if index != -1 and identifier.find('.', index + 11) == -1:
|
|
||||||
equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
|
|
||||||
next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES)
|
|
||||||
if next_code and (
|
|
||||||
next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or
|
|
||||||
next_code.IsOperator('new')):
|
|
||||||
self._HandleError(
|
|
||||||
errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE,
|
|
||||||
'Member %s cannot have a non-primitive value' % identifier,
|
|
||||||
token)
|
|
||||||
|
|
||||||
elif token_type == Type.END_PARAMETERS:
|
|
||||||
# Find extra space at the end of parameter lists. We check the token
|
|
||||||
# prior to the current one when it is a closing paren.
|
|
||||||
if (token.previous and token.previous.type == Type.PARAMETERS
|
|
||||||
and self.ENDS_WITH_SPACE.search(token.previous.string)):
|
|
||||||
self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"',
|
|
||||||
token.previous)
|
|
||||||
|
|
||||||
jsdoc = state.GetDocComment()
|
|
||||||
if state.GetFunction().is_interface:
|
|
||||||
if token.previous and token.previous.type == Type.PARAMETERS:
|
|
||||||
self._HandleError(
|
|
||||||
errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS,
|
|
||||||
'Interface constructor cannot have parameters',
|
|
||||||
token.previous)
|
|
||||||
elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see')
|
|
||||||
and not jsdoc.InheritsDocumentation()
|
|
||||||
and not state.InObjectLiteralDescendant() and not
|
|
||||||
jsdoc.IsInvalidated()):
|
|
||||||
distance, edit = jsdoc.CompareParameters(state.GetParams())
|
|
||||||
if distance:
|
|
||||||
params_iter = iter(state.GetParams())
|
|
||||||
docs_iter = iter(jsdoc.ordered_params)
|
|
||||||
|
|
||||||
for op in edit:
|
|
||||||
if op == 'I':
|
|
||||||
# Insertion.
|
|
||||||
# Parsing doc comments is the same for all languages
|
|
||||||
# but some languages care about parameters that don't have
|
|
||||||
# doc comments and some languages don't care.
|
|
||||||
# Languages that don't allow variables to by typed such as
|
|
||||||
# JavaScript care but languages such as ActionScript or Java
|
|
||||||
# that allow variables to be typed don't care.
|
|
||||||
if not self._limited_doc_checks:
|
|
||||||
self.HandleMissingParameterDoc(token, params_iter.next())
|
|
||||||
|
|
||||||
elif op == 'D':
|
|
||||||
# Deletion
|
|
||||||
self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION,
|
|
||||||
'Found docs for non-existing parameter: "%s"' %
|
|
||||||
docs_iter.next(), token)
|
|
||||||
elif op == 'S':
|
|
||||||
# Substitution
|
|
||||||
if not self._limited_doc_checks:
|
|
||||||
self._HandleError(
|
|
||||||
errors.WRONG_PARAMETER_DOCUMENTATION,
|
|
||||||
'Parameter mismatch: got "%s", expected "%s"' %
|
|
||||||
(params_iter.next(), docs_iter.next()), token)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Equality - just advance the iterators
|
|
||||||
params_iter.next()
|
|
||||||
docs_iter.next()
|
|
||||||
|
|
||||||
elif token_type == Type.STRING_TEXT:
|
|
||||||
# If this is the first token after the start of the string, but it's at
|
|
||||||
# the end of a line, we know we have a multi-line string.
|
|
||||||
if token.previous.type in (
|
|
||||||
Type.SINGLE_QUOTE_STRING_START,
|
|
||||||
Type.DOUBLE_QUOTE_STRING_START) and last_in_line:
|
|
||||||
self._HandleError(errors.MULTI_LINE_STRING,
|
|
||||||
'Multi-line strings are not allowed', token)
|
|
||||||
|
|
||||||
# This check is orthogonal to the ones above, and repeats some types, so
|
|
||||||
# it is a plain if and not an elif.
|
|
||||||
if token.type in Type.COMMENT_TYPES:
|
|
||||||
if self.ILLEGAL_TAB.search(token.string):
|
|
||||||
self._HandleError(errors.ILLEGAL_TAB,
|
|
||||||
'Illegal tab in comment "%s"' % token.string, token)
|
|
||||||
|
|
||||||
trimmed = token.string.rstrip()
|
|
||||||
if last_in_line and token.string != trimmed:
|
|
||||||
# Check for extra whitespace at the end of a line.
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space at end of line', token,
|
|
||||||
position=Position(len(trimmed), len(token.string) - len(trimmed)))
|
|
||||||
|
|
||||||
# This check is also orthogonal since it is based on metadata.
|
|
||||||
if token.metadata.is_implied_semicolon:
|
|
||||||
self._HandleError(errors.MISSING_SEMICOLON,
|
|
||||||
'Missing semicolon at end of line', token)
|
|
||||||
|
|
||||||
def _HandleStartBracket(self, token, last_non_space_token):
|
|
||||||
"""Handles a token that is an open bracket.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to handle.
|
|
||||||
last_non_space_token: The last token that was not a space.
|
|
||||||
"""
|
|
||||||
if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and
|
|
||||||
last_non_space_token and
|
|
||||||
last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._HandleError(
|
|
||||||
errors.EXTRA_SPACE, 'Extra space before "["',
|
|
||||||
token.previous, position=Position.All(token.previous.string))
|
|
||||||
# If the [ token is the first token in a line we shouldn't complain
|
|
||||||
# about a missing space before [. This is because some Ecma script
|
|
||||||
# languages allow syntax like:
|
|
||||||
# [Annotation]
|
|
||||||
# class MyClass {...}
|
|
||||||
# So we don't want to blindly warn about missing spaces before [.
|
|
||||||
# In the the future, when rules for computing exactly how many spaces
|
|
||||||
# lines should be indented are added, then we can return errors for
|
|
||||||
# [ tokens that are improperly indented.
|
|
||||||
# For example:
|
|
||||||
# var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName =
|
|
||||||
# [a,b,c];
|
|
||||||
# should trigger a proper indentation warning message as [ is not indented
|
|
||||||
# by four spaces.
|
|
||||||
elif (not token.IsFirstInLine() and token.previous and
|
|
||||||
token.previous.type not in (
|
|
||||||
[Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] +
|
|
||||||
Type.EXPRESSION_ENDER_TYPES)):
|
|
||||||
self._HandleError(errors.MISSING_SPACE, 'Missing space before "["',
|
|
||||||
token, position=Position.AtBeginning())
|
|
||||||
|
|
||||||
def Finalize(self, state):
|
|
||||||
"""Perform all checks that need to occur after all lines are processed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
state: State of the parser after parsing all tokens
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
TypeError: If not overridden.
|
|
||||||
"""
|
|
||||||
last_non_space_token = state.GetLastNonSpaceToken()
|
|
||||||
# Check last line for ending with newline.
|
|
||||||
if state.GetLastLine() and not (
|
|
||||||
state.GetLastLine().isspace() or
|
|
||||||
state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()):
|
|
||||||
self._HandleError(
|
|
||||||
errors.FILE_MISSING_NEWLINE,
|
|
||||||
'File does not end with new line. (%s)' % state.GetLastLine(),
|
|
||||||
last_non_space_token)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._indentation.Finalize()
|
|
||||||
except Exception, e:
|
|
||||||
self._HandleError(
|
|
||||||
errors.FILE_DOES_NOT_PARSE,
|
|
||||||
str(e),
|
|
||||||
last_non_space_token)
|
|
||||||
|
|
||||||
def GetLongLineExceptions(self):
|
|
||||||
"""Gets a list of regexps for lines which can be longer than the limit.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of regexps, used as matches (rather than searches).
|
|
||||||
"""
|
|
||||||
return []
|
|
||||||
|
|
||||||
def InExplicitlyTypedLanguage(self):
|
|
||||||
"""Returns whether this ecma implementation is explicitly typed."""
|
|
||||||
return False
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Metadata pass for annotating tokens in EcmaScript files."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)')
|
|
||||||
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
|
|
||||||
|
|
||||||
TokenType = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
|
||||||
"""Exception indicating a parse error at the given token.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
token: The token where the parse error occurred.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, token, message=None):
|
|
||||||
"""Initialize a parse error at the given token with an optional message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token where the parse error occurred.
|
|
||||||
message: A message describing the parse error.
|
|
||||||
"""
|
|
||||||
Exception.__init__(self, message)
|
|
||||||
self.token = token
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaContext(object):
|
|
||||||
"""Context object for EcmaScript languages.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
end_token: The token where this context ends.
|
|
||||||
parent: The parent context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The root context.
|
|
||||||
ROOT = 'root'
|
|
||||||
|
|
||||||
# A block of code.
|
|
||||||
BLOCK = 'block'
|
|
||||||
|
|
||||||
# A pseudo-block of code for a given case or default section.
|
|
||||||
CASE_BLOCK = 'case_block'
|
|
||||||
|
|
||||||
# Block of statements in a for loop's parentheses.
|
|
||||||
FOR_GROUP_BLOCK = 'for_block'
|
|
||||||
|
|
||||||
# An implied block of code for 1 line if, while, and for statements
|
|
||||||
IMPLIED_BLOCK = 'implied_block'
|
|
||||||
|
|
||||||
# An index in to an array or object.
|
|
||||||
INDEX = 'index'
|
|
||||||
|
|
||||||
# An array literal in [].
|
|
||||||
ARRAY_LITERAL = 'array_literal'
|
|
||||||
|
|
||||||
# An object literal in {}.
|
|
||||||
OBJECT_LITERAL = 'object_literal'
|
|
||||||
|
|
||||||
# An individual element in an array or object literal.
|
|
||||||
LITERAL_ELEMENT = 'literal_element'
|
|
||||||
|
|
||||||
# The portion of a ternary statement between ? and :
|
|
||||||
TERNARY_TRUE = 'ternary_true'
|
|
||||||
|
|
||||||
# The portion of a ternary statment after :
|
|
||||||
TERNARY_FALSE = 'ternary_false'
|
|
||||||
|
|
||||||
# The entire switch statment. This will contain a GROUP with the variable
|
|
||||||
# and a BLOCK with the code.
|
|
||||||
|
|
||||||
# Since that BLOCK is not a normal block, it can not contain statements except
|
|
||||||
# for case and default.
|
|
||||||
SWITCH = 'switch'
|
|
||||||
|
|
||||||
# A normal comment.
|
|
||||||
COMMENT = 'comment'
|
|
||||||
|
|
||||||
# A JsDoc comment.
|
|
||||||
DOC = 'doc'
|
|
||||||
|
|
||||||
# An individual statement.
|
|
||||||
STATEMENT = 'statement'
|
|
||||||
|
|
||||||
# Code within parentheses.
|
|
||||||
GROUP = 'group'
|
|
||||||
|
|
||||||
# Parameter names in a function declaration.
|
|
||||||
PARAMETERS = 'parameters'
|
|
||||||
|
|
||||||
# A set of variable declarations appearing after the 'var' keyword.
|
|
||||||
VAR = 'var'
|
|
||||||
|
|
||||||
# Context types that are blocks.
|
|
||||||
BLOCK_TYPES = frozenset([
|
|
||||||
ROOT, BLOCK, CASE_BLOCK, FOR_GROUP_BLOCK, IMPLIED_BLOCK])
|
|
||||||
|
|
||||||
def __init__(self, context_type, start_token, parent=None):
|
|
||||||
"""Initializes the context object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context_type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
parent: The parent context.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
type: The context type.
|
|
||||||
start_token: The token where this context starts.
|
|
||||||
end_token: The token where this context ends.
|
|
||||||
parent: The parent context.
|
|
||||||
children: The child contexts of this context, in order.
|
|
||||||
"""
|
|
||||||
self.type = context_type
|
|
||||||
self.start_token = start_token
|
|
||||||
self.end_token = None
|
|
||||||
|
|
||||||
self.parent = None
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
parent.AddChild(self)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Returns a string representation of the context object."""
|
|
||||||
stack = []
|
|
||||||
context = self
|
|
||||||
while context:
|
|
||||||
stack.append(context.type)
|
|
||||||
context = context.parent
|
|
||||||
return 'Context(%s)' % ' > '.join(stack)
|
|
||||||
|
|
||||||
def AddChild(self, child):
|
|
||||||
"""Adds a child to this context and sets child's parent to this context.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
child: A child EcmaContext. The child's parent will be set to this
|
|
||||||
context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
child.parent = self
|
|
||||||
|
|
||||||
self.children.append(child)
|
|
||||||
self.children.sort(EcmaContext._CompareContexts)
|
|
||||||
|
|
||||||
def GetRoot(self):
|
|
||||||
"""Get the root context that contains this context, if any."""
|
|
||||||
context = self
|
|
||||||
while context:
|
|
||||||
if context.type is EcmaContext.ROOT:
|
|
||||||
return context
|
|
||||||
context = context.parent
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _CompareContexts(context1, context2):
|
|
||||||
"""Sorts contexts 1 and 2 by start token document position."""
|
|
||||||
return tokenutil.Compare(context1.start_token, context2.start_token)
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaMetaData(object):
|
|
||||||
"""Token metadata for EcmaScript languages.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
last_code: The last code token to appear before this one.
|
|
||||||
context: The context this token appears in.
|
|
||||||
operator_type: The operator type, will be one of the *_OPERATOR constants
|
|
||||||
defined below.
|
|
||||||
aliased_symbol: The full symbol being identified, as a string (e.g. an
|
|
||||||
'XhrIo' alias for 'goog.net.XhrIo'). Only applicable to identifier
|
|
||||||
tokens. This is set in aliaspass.py and is a best guess.
|
|
||||||
is_alias_definition: True if the symbol is part of an alias definition.
|
|
||||||
If so, these symbols won't be counted towards goog.requires/provides.
|
|
||||||
"""
|
|
||||||
|
|
||||||
UNARY_OPERATOR = 'unary'
|
|
||||||
|
|
||||||
UNARY_POST_OPERATOR = 'unary_post'
|
|
||||||
|
|
||||||
BINARY_OPERATOR = 'binary'
|
|
||||||
|
|
||||||
TERNARY_OPERATOR = 'ternary'
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initializes a token metadata object."""
|
|
||||||
self.last_code = None
|
|
||||||
self.context = None
|
|
||||||
self.operator_type = None
|
|
||||||
self.is_implied_semicolon = False
|
|
||||||
self.is_implied_block = False
|
|
||||||
self.is_implied_block_close = False
|
|
||||||
self.aliased_symbol = None
|
|
||||||
self.is_alias_definition = False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Returns a string representation of the context object."""
|
|
||||||
parts = ['%r' % self.context]
|
|
||||||
if self.operator_type:
|
|
||||||
parts.append('optype: %r' % self.operator_type)
|
|
||||||
if self.is_implied_semicolon:
|
|
||||||
parts.append('implied;')
|
|
||||||
if self.aliased_symbol:
|
|
||||||
parts.append('alias for: %s' % self.aliased_symbol)
|
|
||||||
return 'MetaData(%s)' % ', '.join(parts)
|
|
||||||
|
|
||||||
def IsUnaryOperator(self):
|
|
||||||
return self.operator_type in (EcmaMetaData.UNARY_OPERATOR,
|
|
||||||
EcmaMetaData.UNARY_POST_OPERATOR)
|
|
||||||
|
|
||||||
def IsUnaryPostOperator(self):
|
|
||||||
return self.operator_type == EcmaMetaData.UNARY_POST_OPERATOR
|
|
||||||
|
|
||||||
|
|
||||||
class EcmaMetaDataPass(object):
|
|
||||||
"""A pass that iterates over all tokens and builds metadata about them."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
"""Initialize the meta data pass object."""
|
|
||||||
self.Reset()
|
|
||||||
|
|
||||||
def Reset(self):
|
|
||||||
"""Resets the metadata pass to prepare for the next file."""
|
|
||||||
self._token = None
|
|
||||||
self._context = None
|
|
||||||
self._AddContext(EcmaContext.ROOT)
|
|
||||||
self._last_code = None
|
|
||||||
|
|
||||||
def _CreateContext(self, context_type):
|
|
||||||
"""Overridable by subclasses to create the appropriate context type."""
|
|
||||||
return EcmaContext(context_type, self._token, self._context)
|
|
||||||
|
|
||||||
def _CreateMetaData(self):
|
|
||||||
"""Overridable by subclasses to create the appropriate metadata type."""
|
|
||||||
return EcmaMetaData()
|
|
||||||
|
|
||||||
def _AddContext(self, context_type):
|
|
||||||
"""Adds a context of the given type to the context stack.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
context_type: The type of context to create
|
|
||||||
"""
|
|
||||||
self._context = self._CreateContext(context_type)
|
|
||||||
|
|
||||||
def _PopContext(self):
|
|
||||||
"""Moves up one level in the context stack.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The former context.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ParseError: If the root context is popped.
|
|
||||||
"""
|
|
||||||
top_context = self._context
|
|
||||||
top_context.end_token = self._token
|
|
||||||
self._context = top_context.parent
|
|
||||||
if self._context:
|
|
||||||
return top_context
|
|
||||||
else:
|
|
||||||
raise ParseError(self._token)
|
|
||||||
|
|
||||||
def _PopContextType(self, *stop_types):
|
|
||||||
"""Pops the context stack until a context of the given type is popped.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
*stop_types: The types of context to pop to - stops at the first match.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The context object of the given type that was popped.
|
|
||||||
"""
|
|
||||||
last = None
|
|
||||||
while not last or last.type not in stop_types:
|
|
||||||
last = self._PopContext()
|
|
||||||
return last
|
|
||||||
|
|
||||||
def _EndStatement(self):
|
|
||||||
"""Process the end of a statement."""
|
|
||||||
self._PopContextType(EcmaContext.STATEMENT)
|
|
||||||
if self._context.type == EcmaContext.IMPLIED_BLOCK:
|
|
||||||
self._token.metadata.is_implied_block_close = True
|
|
||||||
self._PopContext()
|
|
||||||
|
|
||||||
def _ProcessContext(self):
|
|
||||||
"""Process the context at the current token.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The context that should be assigned to the current token, or None if
|
|
||||||
the current context after this method should be used.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ParseError: When the token appears in an invalid context.
|
|
||||||
"""
|
|
||||||
token = self._token
|
|
||||||
token_type = token.type
|
|
||||||
|
|
||||||
if self._context.type in EcmaContext.BLOCK_TYPES:
|
|
||||||
# Whenever we're in a block, we add a statement context. We make an
|
|
||||||
# exception for switch statements since they can only contain case: and
|
|
||||||
# default: and therefore don't directly contain statements.
|
|
||||||
# The block we add here may be immediately removed in some cases, but
|
|
||||||
# that causes no harm.
|
|
||||||
parent = self._context.parent
|
|
||||||
if not parent or parent.type != EcmaContext.SWITCH:
|
|
||||||
self._AddContext(EcmaContext.STATEMENT)
|
|
||||||
|
|
||||||
elif self._context.type == EcmaContext.ARRAY_LITERAL:
|
|
||||||
self._AddContext(EcmaContext.LITERAL_ELEMENT)
|
|
||||||
|
|
||||||
if token_type == TokenType.START_PAREN:
|
|
||||||
if self._last_code and self._last_code.IsKeyword('for'):
|
|
||||||
# for loops contain multiple statements in the group unlike while,
|
|
||||||
# switch, if, etc.
|
|
||||||
self._AddContext(EcmaContext.FOR_GROUP_BLOCK)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.GROUP)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_PAREN:
|
|
||||||
result = self._PopContextType(EcmaContext.GROUP,
|
|
||||||
EcmaContext.FOR_GROUP_BLOCK)
|
|
||||||
keyword_token = result.start_token.metadata.last_code
|
|
||||||
# keyword_token will not exist if the open paren is the first line of the
|
|
||||||
# file, for example if all code is wrapped in an immediately executed
|
|
||||||
# annonymous function.
|
|
||||||
if keyword_token and keyword_token.string in ('if', 'for', 'while'):
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
if next_code.type != TokenType.START_BLOCK:
|
|
||||||
# Check for do-while.
|
|
||||||
is_do_while = False
|
|
||||||
pre_keyword_token = keyword_token.metadata.last_code
|
|
||||||
if (pre_keyword_token and
|
|
||||||
pre_keyword_token.type == TokenType.END_BLOCK):
|
|
||||||
start_block_token = pre_keyword_token.metadata.context.start_token
|
|
||||||
is_do_while = start_block_token.metadata.last_code.string == 'do'
|
|
||||||
|
|
||||||
# If it's not do-while, it's an implied block.
|
|
||||||
if not is_do_while:
|
|
||||||
self._AddContext(EcmaContext.IMPLIED_BLOCK)
|
|
||||||
token.metadata.is_implied_block = True
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
# else (not else if) with no open brace after it should be considered the
|
|
||||||
# start of an implied block, similar to the case with if, for, and while
|
|
||||||
# above.
|
|
||||||
elif (token_type == TokenType.KEYWORD and
|
|
||||||
token.string == 'else'):
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
if (next_code.type != TokenType.START_BLOCK and
|
|
||||||
(next_code.type != TokenType.KEYWORD or next_code.string != 'if')):
|
|
||||||
self._AddContext(EcmaContext.IMPLIED_BLOCK)
|
|
||||||
token.metadata.is_implied_block = True
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_PARAMETERS:
|
|
||||||
self._AddContext(EcmaContext.PARAMETERS)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_PARAMETERS:
|
|
||||||
return self._PopContextType(EcmaContext.PARAMETERS)
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_BRACKET:
|
|
||||||
if (self._last_code and
|
|
||||||
self._last_code.type in TokenType.EXPRESSION_ENDER_TYPES):
|
|
||||||
self._AddContext(EcmaContext.INDEX)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.ARRAY_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_BRACKET:
|
|
||||||
return self._PopContextType(EcmaContext.INDEX, EcmaContext.ARRAY_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.START_BLOCK:
|
|
||||||
if (self._last_code.type in (TokenType.END_PAREN,
|
|
||||||
TokenType.END_PARAMETERS) or
|
|
||||||
self._last_code.IsKeyword('else') or
|
|
||||||
self._last_code.IsKeyword('do') or
|
|
||||||
self._last_code.IsKeyword('try') or
|
|
||||||
self._last_code.IsKeyword('finally') or
|
|
||||||
(self._last_code.IsOperator(':') and
|
|
||||||
self._last_code.metadata.context.type == EcmaContext.CASE_BLOCK)):
|
|
||||||
# else, do, try, and finally all might have no () before {.
|
|
||||||
# Also, handle the bizzare syntax case 10: {...}.
|
|
||||||
self._AddContext(EcmaContext.BLOCK)
|
|
||||||
else:
|
|
||||||
self._AddContext(EcmaContext.OBJECT_LITERAL)
|
|
||||||
|
|
||||||
elif token_type == TokenType.END_BLOCK:
|
|
||||||
context = self._PopContextType(EcmaContext.BLOCK,
|
|
||||||
EcmaContext.OBJECT_LITERAL)
|
|
||||||
if self._context.type == EcmaContext.SWITCH:
|
|
||||||
# The end of the block also means the end of the switch statement it
|
|
||||||
# applies to.
|
|
||||||
return self._PopContext()
|
|
||||||
return context
|
|
||||||
|
|
||||||
elif token.IsKeyword('switch'):
|
|
||||||
self._AddContext(EcmaContext.SWITCH)
|
|
||||||
|
|
||||||
elif (token_type == TokenType.KEYWORD and
|
|
||||||
token.string in ('case', 'default') and
|
|
||||||
self._context.type != EcmaContext.OBJECT_LITERAL):
|
|
||||||
# Pop up to but not including the switch block.
|
|
||||||
while self._context.parent.type != EcmaContext.SWITCH:
|
|
||||||
self._PopContext()
|
|
||||||
if self._context.parent is None:
|
|
||||||
raise ParseError(token, 'Encountered case/default statement '
|
|
||||||
'without switch statement')
|
|
||||||
|
|
||||||
elif token.IsOperator('?'):
|
|
||||||
self._AddContext(EcmaContext.TERNARY_TRUE)
|
|
||||||
|
|
||||||
elif token.IsOperator(':'):
|
|
||||||
if self._context.type == EcmaContext.OBJECT_LITERAL:
|
|
||||||
self._AddContext(EcmaContext.LITERAL_ELEMENT)
|
|
||||||
|
|
||||||
elif self._context.type == EcmaContext.TERNARY_TRUE:
|
|
||||||
self._PopContext()
|
|
||||||
self._AddContext(EcmaContext.TERNARY_FALSE)
|
|
||||||
|
|
||||||
# Handle nested ternary statements like:
|
|
||||||
# foo = bar ? baz ? 1 : 2 : 3
|
|
||||||
# When we encounter the second ":" the context is
|
|
||||||
# ternary_false > ternary_true > statement > root
|
|
||||||
elif (self._context.type == EcmaContext.TERNARY_FALSE and
|
|
||||||
self._context.parent.type == EcmaContext.TERNARY_TRUE):
|
|
||||||
self._PopContext() # Leave current ternary false context.
|
|
||||||
self._PopContext() # Leave current parent ternary true
|
|
||||||
self._AddContext(EcmaContext.TERNARY_FALSE)
|
|
||||||
|
|
||||||
elif self._context.parent.type == EcmaContext.SWITCH:
|
|
||||||
self._AddContext(EcmaContext.CASE_BLOCK)
|
|
||||||
|
|
||||||
elif token.IsKeyword('var'):
|
|
||||||
self._AddContext(EcmaContext.VAR)
|
|
||||||
|
|
||||||
elif token.IsOperator(','):
|
|
||||||
while self._context.type not in (EcmaContext.VAR,
|
|
||||||
EcmaContext.ARRAY_LITERAL,
|
|
||||||
EcmaContext.OBJECT_LITERAL,
|
|
||||||
EcmaContext.STATEMENT,
|
|
||||||
EcmaContext.PARAMETERS,
|
|
||||||
EcmaContext.GROUP):
|
|
||||||
self._PopContext()
|
|
||||||
|
|
||||||
elif token_type == TokenType.SEMICOLON:
|
|
||||||
self._EndStatement()
|
|
||||||
|
|
||||||
def Process(self, first_token):
|
|
||||||
"""Processes the token stream starting with the given token."""
|
|
||||||
self._token = first_token
|
|
||||||
while self._token:
|
|
||||||
self._ProcessToken()
|
|
||||||
|
|
||||||
if self._token.IsCode():
|
|
||||||
self._last_code = self._token
|
|
||||||
|
|
||||||
self._token = self._token.next
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._PopContextType(self, EcmaContext.ROOT)
|
|
||||||
except ParseError:
|
|
||||||
# Ignore the "popped to root" error.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _ProcessToken(self):
|
|
||||||
"""Process the given token."""
|
|
||||||
token = self._token
|
|
||||||
token.metadata = self._CreateMetaData()
|
|
||||||
context = (self._ProcessContext() or self._context)
|
|
||||||
token.metadata.context = context
|
|
||||||
token.metadata.last_code = self._last_code
|
|
||||||
|
|
||||||
# Determine the operator type of the token, if applicable.
|
|
||||||
if token.type == TokenType.OPERATOR:
|
|
||||||
token.metadata.operator_type = self._GetOperatorType(token)
|
|
||||||
|
|
||||||
# Determine if there is an implied semicolon after the token.
|
|
||||||
if token.type != TokenType.SEMICOLON:
|
|
||||||
next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES)
|
|
||||||
# A statement like if (x) does not need a semicolon after it
|
|
||||||
is_implied_block = self._context == EcmaContext.IMPLIED_BLOCK
|
|
||||||
is_last_code_in_line = token.IsCode() and (
|
|
||||||
not next_code or next_code.line_number != token.line_number)
|
|
||||||
is_continued_operator = (token.type == TokenType.OPERATOR and
|
|
||||||
not token.metadata.IsUnaryPostOperator())
|
|
||||||
is_continued_dot = token.string == '.'
|
|
||||||
next_code_is_operator = next_code and next_code.type == TokenType.OPERATOR
|
|
||||||
is_end_of_block = (
|
|
||||||
token.type == TokenType.END_BLOCK and
|
|
||||||
token.metadata.context.type != EcmaContext.OBJECT_LITERAL)
|
|
||||||
is_multiline_string = token.type == TokenType.STRING_TEXT
|
|
||||||
is_continued_var_decl = (token.IsKeyword('var') and
|
|
||||||
next_code and
|
|
||||||
(next_code.type in [TokenType.IDENTIFIER,
|
|
||||||
TokenType.SIMPLE_LVALUE]) and
|
|
||||||
token.line_number < next_code.line_number)
|
|
||||||
next_code_is_block = next_code and next_code.type == TokenType.START_BLOCK
|
|
||||||
if (is_last_code_in_line and
|
|
||||||
self._StatementCouldEndInContext() and
|
|
||||||
not is_multiline_string and
|
|
||||||
not is_end_of_block and
|
|
||||||
not is_continued_var_decl and
|
|
||||||
not is_continued_operator and
|
|
||||||
not is_continued_dot and
|
|
||||||
not next_code_is_operator and
|
|
||||||
not is_implied_block and
|
|
||||||
not next_code_is_block):
|
|
||||||
token.metadata.is_implied_semicolon = True
|
|
||||||
self._EndStatement()
|
|
||||||
|
|
||||||
def _StatementCouldEndInContext(self):
|
|
||||||
"""Returns if the current statement (if any) may end in this context."""
|
|
||||||
# In the basic statement or variable declaration context, statement can
|
|
||||||
# always end in this context.
|
|
||||||
if self._context.type in (EcmaContext.STATEMENT, EcmaContext.VAR):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# End of a ternary false branch inside a statement can also be the
|
|
||||||
# end of the statement, for example:
|
|
||||||
# var x = foo ? foo.bar() : null
|
|
||||||
# In this case the statement ends after the null, when the context stack
|
|
||||||
# looks like ternary_false > var > statement > root.
|
|
||||||
if (self._context.type == EcmaContext.TERNARY_FALSE and
|
|
||||||
self._context.parent.type in (EcmaContext.STATEMENT, EcmaContext.VAR)):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# In all other contexts like object and array literals, ternary true, etc.
|
|
||||||
# the statement can't yet end.
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _GetOperatorType(self, token):
|
|
||||||
"""Returns the operator type of the given operator token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to get arity for.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The type of the operator. One of the *_OPERATOR constants defined in
|
|
||||||
EcmaMetaData.
|
|
||||||
"""
|
|
||||||
if token.string == '?':
|
|
||||||
return EcmaMetaData.TERNARY_OPERATOR
|
|
||||||
|
|
||||||
if token.string in TokenType.UNARY_OPERATORS:
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
last_code = token.metadata.last_code
|
|
||||||
if not last_code or last_code.type == TokenType.END_BLOCK:
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
if (token.string in TokenType.UNARY_POST_OPERATORS and
|
|
||||||
last_code.type in TokenType.EXPRESSION_ENDER_TYPES):
|
|
||||||
return EcmaMetaData.UNARY_POST_OPERATOR
|
|
||||||
|
|
||||||
if (token.string in TokenType.UNARY_OK_OPERATORS and
|
|
||||||
last_code.type not in TokenType.EXPRESSION_ENDER_TYPES and
|
|
||||||
last_code.string not in TokenType.UNARY_POST_OPERATORS):
|
|
||||||
return EcmaMetaData.UNARY_OPERATOR
|
|
||||||
|
|
||||||
return EcmaMetaData.BINARY_OPERATOR
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2011 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.
|
|
||||||
|
|
||||||
|
|
||||||
"""Specific JSLint errors checker."""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
|
|
||||||
class Rule(object):
|
|
||||||
"""Different rules to check."""
|
|
||||||
|
|
||||||
# Documentations for specific rules goes in flag definition.
|
|
||||||
BLANK_LINES_AT_TOP_LEVEL = 'blank_lines_at_top_level'
|
|
||||||
INDENTATION = 'indentation'
|
|
||||||
WELL_FORMED_AUTHOR = 'well_formed_author'
|
|
||||||
NO_BRACES_AROUND_INHERIT_DOC = 'no_braces_around_inherit_doc'
|
|
||||||
BRACES_AROUND_TYPE = 'braces_around_type'
|
|
||||||
OPTIONAL_TYPE_MARKER = 'optional_type_marker'
|
|
||||||
VARIABLE_ARG_MARKER = 'variable_arg_marker'
|
|
||||||
UNUSED_PRIVATE_MEMBERS = 'unused_private_members'
|
|
||||||
UNUSED_LOCAL_VARIABLES = 'unused_local_variables'
|
|
||||||
|
|
||||||
# Rule to raise all known errors.
|
|
||||||
ALL = 'all'
|
|
||||||
|
|
||||||
# All rules that are to be checked when using the strict flag. E.g. the rules
|
|
||||||
# that are specific to the stricter Closure style.
|
|
||||||
CLOSURE_RULES = frozenset([BLANK_LINES_AT_TOP_LEVEL,
|
|
||||||
INDENTATION,
|
|
||||||
WELL_FORMED_AUTHOR,
|
|
||||||
NO_BRACES_AROUND_INHERIT_DOC,
|
|
||||||
BRACES_AROUND_TYPE,
|
|
||||||
OPTIONAL_TYPE_MARKER,
|
|
||||||
VARIABLE_ARG_MARKER])
|
|
||||||
|
|
||||||
|
|
||||||
flags.DEFINE_boolean('strict', False,
|
|
||||||
'Whether to validate against the stricter Closure style. '
|
|
||||||
'This includes ' + (', '.join(Rule.CLOSURE_RULES)) + '.')
|
|
||||||
flags.DEFINE_multistring('jslint_error', [],
|
|
||||||
'List of specific lint errors to check. Here is a list'
|
|
||||||
' of accepted values:\n'
|
|
||||||
' - ' + Rule.ALL + ': enables all following errors.\n'
|
|
||||||
' - ' + Rule.BLANK_LINES_AT_TOP_LEVEL + ': validates'
|
|
||||||
'number of blank lines between blocks at top level.\n'
|
|
||||||
' - ' + Rule.INDENTATION + ': checks correct '
|
|
||||||
'indentation of code.\n'
|
|
||||||
' - ' + Rule.WELL_FORMED_AUTHOR + ': validates the '
|
|
||||||
'@author JsDoc tags.\n'
|
|
||||||
' - ' + Rule.NO_BRACES_AROUND_INHERIT_DOC + ': '
|
|
||||||
'forbids braces around @inheritdoc JsDoc tags.\n'
|
|
||||||
' - ' + Rule.BRACES_AROUND_TYPE + ': enforces braces '
|
|
||||||
'around types in JsDoc tags.\n'
|
|
||||||
' - ' + Rule.OPTIONAL_TYPE_MARKER + ': checks correct '
|
|
||||||
'use of optional marker = in param types.\n'
|
|
||||||
' - ' + Rule.UNUSED_PRIVATE_MEMBERS + ': checks for '
|
|
||||||
'unused private variables.\n'
|
|
||||||
' - ' + Rule.UNUSED_LOCAL_VARIABLES + ': checks for '
|
|
||||||
'unused local variables.\n')
|
|
||||||
|
|
||||||
|
|
||||||
def ShouldCheck(rule):
|
|
||||||
"""Returns whether the optional rule should be checked.
|
|
||||||
|
|
||||||
Computes different flags (strict, jslint_error, jslint_noerror) to find out if
|
|
||||||
this specific rule should be checked.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
rule: Name of the rule (see Rule).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the rule should be checked according to the flags, otherwise False.
|
|
||||||
"""
|
|
||||||
if rule in FLAGS.jslint_error or Rule.ALL in FLAGS.jslint_error:
|
|
||||||
return True
|
|
||||||
# Checks strict rules.
|
|
||||||
return FLAGS.strict and rule in Rule.CLOSURE_RULES
|
|
||||||
@@ -1,618 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Main class responsible for automatically fixing simple style violations."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import javascriptstatetracker
|
|
||||||
from closure_linter import javascripttokens
|
|
||||||
from closure_linter import requireprovidesorter
|
|
||||||
from closure_linter import tokenutil
|
|
||||||
from closure_linter.common import errorhandler
|
|
||||||
|
|
||||||
# Shorthand
|
|
||||||
Token = javascripttokens.JavaScriptToken
|
|
||||||
Type = javascripttokens.JavaScriptTokenType
|
|
||||||
|
|
||||||
END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$')
|
|
||||||
|
|
||||||
# Regex to represent common mistake inverting author name and email as
|
|
||||||
# @author User Name (user@company)
|
|
||||||
INVERTED_AUTHOR_SPEC = re.compile(r'(?P<leading_whitespace>\s*)'
|
|
||||||
r'(?P<name>[^(]+)'
|
|
||||||
r'(?P<whitespace_after_name>\s+)'
|
|
||||||
r'\('
|
|
||||||
r'(?P<email>[^\s]+@[^)\s]+)'
|
|
||||||
r'\)'
|
|
||||||
r'(?P<trailing_characters>.*)')
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('disable_indentation_fixing', False,
|
|
||||||
'Whether to disable automatic fixing of indentation.')
|
|
||||||
flags.DEFINE_list('fix_error_codes', [], 'A list of specific error codes to '
|
|
||||||
'fix. Defaults to all supported error codes when empty. '
|
|
||||||
'See errors.py for a list of error codes.')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorFixer(errorhandler.ErrorHandler):
|
|
||||||
"""Object that fixes simple style errors."""
|
|
||||||
|
|
||||||
def __init__(self, external_file=None):
|
|
||||||
"""Initialize the error fixer.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
external_file: If included, all output will be directed to this file
|
|
||||||
instead of overwriting the files the errors are found in.
|
|
||||||
"""
|
|
||||||
errorhandler.ErrorHandler.__init__(self)
|
|
||||||
|
|
||||||
self._file_name = None
|
|
||||||
self._file_token = None
|
|
||||||
self._external_file = external_file
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._fix_error_codes = set([errors.ByName(error.upper()) for error in
|
|
||||||
FLAGS.fix_error_codes])
|
|
||||||
except KeyError as ke:
|
|
||||||
raise ValueError('Unknown error code ' + ke.args[0])
|
|
||||||
|
|
||||||
def HandleFile(self, filename, first_token):
|
|
||||||
"""Notifies this ErrorPrinter that subsequent errors are in filename.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: The name of the file about to be checked.
|
|
||||||
first_token: The first token in the file.
|
|
||||||
"""
|
|
||||||
self._file_name = filename
|
|
||||||
self._file_is_html = filename.endswith('.html') or filename.endswith('.htm')
|
|
||||||
self._file_token = first_token
|
|
||||||
self._file_fix_count = 0
|
|
||||||
self._file_changed_lines = set()
|
|
||||||
|
|
||||||
def _AddFix(self, tokens):
|
|
||||||
"""Adds the fix to the internal count.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
tokens: The token or sequence of tokens changed to fix an error.
|
|
||||||
"""
|
|
||||||
self._file_fix_count += 1
|
|
||||||
if hasattr(tokens, 'line_number'):
|
|
||||||
self._file_changed_lines.add(tokens.line_number)
|
|
||||||
else:
|
|
||||||
for token in tokens:
|
|
||||||
self._file_changed_lines.add(token.line_number)
|
|
||||||
|
|
||||||
def _FixJsDocPipeNull(self, js_type):
|
|
||||||
"""Change number|null or null|number to ?number.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
js_type: The typeannotation.TypeAnnotation instance to fix.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Recurse into all sub_types if the error was at a deeper level.
|
|
||||||
map(self._FixJsDocPipeNull, js_type.IterTypes())
|
|
||||||
|
|
||||||
if js_type.type_group and len(js_type.sub_types) == 2:
|
|
||||||
# Find and remove the null sub_type:
|
|
||||||
sub_type = None
|
|
||||||
for sub_type in js_type.sub_types:
|
|
||||||
if sub_type.identifier == 'null':
|
|
||||||
map(tokenutil.DeleteToken, sub_type.tokens)
|
|
||||||
self._AddFix(sub_type.tokens)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
first_token = js_type.FirstToken()
|
|
||||||
question_mark = Token('?', Type.DOC_TYPE_MODIFIER, first_token.line,
|
|
||||||
first_token.line_number)
|
|
||||||
tokenutil.InsertTokenBefore(question_mark, first_token)
|
|
||||||
js_type.tokens.insert(0, question_mark)
|
|
||||||
js_type.tokens.remove(sub_type)
|
|
||||||
js_type.or_null = True
|
|
||||||
|
|
||||||
# Now also remove the separator, which is in the parent's token list,
|
|
||||||
# either before or after the sub_type, there is exactly one. Scan for it.
|
|
||||||
for token in js_type.tokens:
|
|
||||||
if (token and isinstance(token, Token) and
|
|
||||||
token.type == Type.DOC_TYPE_MODIFIER and token.string == '|'):
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
break
|
|
||||||
|
|
||||||
def HandleError(self, error):
|
|
||||||
"""Attempts to fix the error.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
error: The error object
|
|
||||||
"""
|
|
||||||
code = error.code
|
|
||||||
token = error.token
|
|
||||||
|
|
||||||
if self._fix_error_codes and code not in self._fix_error_codes:
|
|
||||||
return
|
|
||||||
|
|
||||||
if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL:
|
|
||||||
self._FixJsDocPipeNull(token.attached_object.jstype)
|
|
||||||
|
|
||||||
elif code == errors.JSDOC_MISSING_OPTIONAL_TYPE:
|
|
||||||
iterator = token.attached_object.type_end_token
|
|
||||||
if iterator.type == Type.DOC_END_BRACE or iterator.string.isspace():
|
|
||||||
iterator = iterator.previous
|
|
||||||
|
|
||||||
ending_space = len(iterator.string) - len(iterator.string.rstrip())
|
|
||||||
iterator.string = '%s=%s' % (iterator.string.rstrip(),
|
|
||||||
' ' * ending_space)
|
|
||||||
|
|
||||||
# Create a new flag object with updated type info.
|
|
||||||
token.attached_object = javascriptstatetracker.JsDocFlag(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.JSDOC_MISSING_VAR_ARGS_TYPE:
|
|
||||||
iterator = token.attached_object.type_start_token
|
|
||||||
if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace():
|
|
||||||
iterator = iterator.next
|
|
||||||
|
|
||||||
starting_space = len(iterator.string) - len(iterator.string.lstrip())
|
|
||||||
iterator.string = '%s...%s' % (' ' * starting_space,
|
|
||||||
iterator.string.lstrip())
|
|
||||||
|
|
||||||
# Create a new flag object with updated type info.
|
|
||||||
token.attached_object = javascriptstatetracker.JsDocFlag(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
errors.MISSING_SEMICOLON):
|
|
||||||
semicolon_token = Token(';', Type.SEMICOLON, token.line,
|
|
||||||
token.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(semicolon_token, token)
|
|
||||||
token.metadata.is_implied_semicolon = False
|
|
||||||
semicolon_token.metadata.is_implied_semicolon = False
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
|
|
||||||
errors.REDUNDANT_SEMICOLON,
|
|
||||||
errors.COMMA_AT_END_OF_LITERAL):
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.INVALID_JSDOC_TAG:
|
|
||||||
if token.string == '@returns':
|
|
||||||
token.string = '@return'
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.FILE_MISSING_NEWLINE:
|
|
||||||
# This error is fixed implicitly by the way we restore the file
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.MISSING_SPACE:
|
|
||||||
if error.fix_data:
|
|
||||||
token.string = error.fix_data
|
|
||||||
self._AddFix(token)
|
|
||||||
elif error.position:
|
|
||||||
if error.position.IsAtBeginning():
|
|
||||||
tokenutil.InsertSpaceTokenAfter(token.previous)
|
|
||||||
elif error.position.IsAtEnd(token.string):
|
|
||||||
tokenutil.InsertSpaceTokenAfter(token)
|
|
||||||
else:
|
|
||||||
token.string = error.position.Set(token.string, ' ')
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.EXTRA_SPACE:
|
|
||||||
if error.position:
|
|
||||||
token.string = error.position.Set(token.string, '')
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.MISSING_LINE:
|
|
||||||
if error.position.IsAtBeginning():
|
|
||||||
tokenutil.InsertBlankLineAfter(token.previous)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertBlankLineAfter(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.EXTRA_LINE:
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.WRONG_BLANK_LINE_COUNT:
|
|
||||||
if not token.previous:
|
|
||||||
# TODO(user): Add an insertBefore method to tokenutil.
|
|
||||||
return
|
|
||||||
|
|
||||||
num_lines = error.fix_data
|
|
||||||
should_delete = False
|
|
||||||
|
|
||||||
if num_lines < 0:
|
|
||||||
num_lines *= -1
|
|
||||||
should_delete = True
|
|
||||||
|
|
||||||
for unused_i in xrange(1, num_lines + 1):
|
|
||||||
if should_delete:
|
|
||||||
# TODO(user): DeleteToken should update line numbers.
|
|
||||||
self._DeleteToken(token.previous)
|
|
||||||
else:
|
|
||||||
tokenutil.InsertBlankLineAfter(token.previous)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING:
|
|
||||||
end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END)
|
|
||||||
if end_quote:
|
|
||||||
single_quote_start = Token(
|
|
||||||
"'", Type.SINGLE_QUOTE_STRING_START, token.line, token.line_number)
|
|
||||||
single_quote_end = Token(
|
|
||||||
"'", Type.SINGLE_QUOTE_STRING_START, end_quote.line,
|
|
||||||
token.line_number)
|
|
||||||
|
|
||||||
tokenutil.InsertTokenAfter(single_quote_start, token)
|
|
||||||
tokenutil.InsertTokenAfter(single_quote_end, end_quote)
|
|
||||||
self._DeleteToken(token)
|
|
||||||
self._DeleteToken(end_quote)
|
|
||||||
self._AddFix([token, end_quote])
|
|
||||||
|
|
||||||
elif code == errors.MISSING_BRACES_AROUND_TYPE:
|
|
||||||
fixed_tokens = []
|
|
||||||
start_token = token.attached_object.type_start_token
|
|
||||||
|
|
||||||
if start_token.type != Type.DOC_START_BRACE:
|
|
||||||
leading_space = (
|
|
||||||
len(start_token.string) - len(start_token.string.lstrip()))
|
|
||||||
if leading_space:
|
|
||||||
start_token = tokenutil.SplitToken(start_token, leading_space)
|
|
||||||
# Fix case where start and end token were the same.
|
|
||||||
if token.attached_object.type_end_token == start_token.previous:
|
|
||||||
token.attached_object.type_end_token = start_token
|
|
||||||
|
|
||||||
new_token = Token('{', Type.DOC_START_BRACE, start_token.line,
|
|
||||||
start_token.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, start_token.previous)
|
|
||||||
token.attached_object.type_start_token = new_token
|
|
||||||
fixed_tokens.append(new_token)
|
|
||||||
|
|
||||||
end_token = token.attached_object.type_end_token
|
|
||||||
if end_token.type != Type.DOC_END_BRACE:
|
|
||||||
# If the start token was a brace, the end token will be a
|
|
||||||
# FLAG_ENDING_TYPE token, if there wasn't a starting brace then
|
|
||||||
# the end token is the last token of the actual type.
|
|
||||||
last_type = end_token
|
|
||||||
if not fixed_tokens:
|
|
||||||
last_type = end_token.previous
|
|
||||||
|
|
||||||
while last_type.string.isspace():
|
|
||||||
last_type = last_type.previous
|
|
||||||
|
|
||||||
# If there was no starting brace then a lone end brace wouldn't have
|
|
||||||
# been type end token. Now that we've added any missing start brace,
|
|
||||||
# see if the last effective type token was an end brace.
|
|
||||||
if last_type.type != Type.DOC_END_BRACE:
|
|
||||||
trailing_space = (len(last_type.string) -
|
|
||||||
len(last_type.string.rstrip()))
|
|
||||||
if trailing_space:
|
|
||||||
tokenutil.SplitToken(last_type,
|
|
||||||
len(last_type.string) - trailing_space)
|
|
||||||
|
|
||||||
new_token = Token('}', Type.DOC_END_BRACE, last_type.line,
|
|
||||||
last_type.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, last_type)
|
|
||||||
token.attached_object.type_end_token = new_token
|
|
||||||
fixed_tokens.append(new_token)
|
|
||||||
|
|
||||||
self._AddFix(fixed_tokens)
|
|
||||||
|
|
||||||
elif code == errors.LINE_STARTS_WITH_OPERATOR:
|
|
||||||
# Remove whitespace following the operator so the line starts clean.
|
|
||||||
self._StripSpace(token, before=False)
|
|
||||||
|
|
||||||
# Remove the operator.
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
insertion_point = tokenutil.GetPreviousCodeToken(token)
|
|
||||||
|
|
||||||
# Insert a space between the previous token and the new operator.
|
|
||||||
space = Token(' ', Type.WHITESPACE, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(space, insertion_point)
|
|
||||||
|
|
||||||
# Insert the operator on the end of the previous line.
|
|
||||||
new_token = Token(token.string, token.type, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenAfter(new_token, space)
|
|
||||||
self._AddFix(new_token)
|
|
||||||
|
|
||||||
elif code == errors.LINE_ENDS_WITH_DOT:
|
|
||||||
# Remove whitespace preceding the operator to remove trailing whitespace.
|
|
||||||
self._StripSpace(token, before=True)
|
|
||||||
|
|
||||||
# Remove the dot.
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
insertion_point = tokenutil.GetNextCodeToken(token)
|
|
||||||
|
|
||||||
# Insert the dot at the beginning of the next line of code.
|
|
||||||
new_token = Token(token.string, token.type, insertion_point.line,
|
|
||||||
insertion_point.line_number)
|
|
||||||
tokenutil.InsertTokenBefore(new_token, insertion_point)
|
|
||||||
self._AddFix(new_token)
|
|
||||||
|
|
||||||
elif code == errors.GOOG_REQUIRES_NOT_ALPHABETIZED:
|
|
||||||
require_start_token = error.fix_data
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixRequires(require_start_token)
|
|
||||||
|
|
||||||
self._AddFix(require_start_token)
|
|
||||||
|
|
||||||
elif code == errors.GOOG_PROVIDES_NOT_ALPHABETIZED:
|
|
||||||
provide_start_token = error.fix_data
|
|
||||||
sorter = requireprovidesorter.RequireProvideSorter()
|
|
||||||
sorter.FixProvides(provide_start_token)
|
|
||||||
|
|
||||||
self._AddFix(provide_start_token)
|
|
||||||
|
|
||||||
elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC:
|
|
||||||
if token.previous.string == '{' and token.next.string == '}':
|
|
||||||
self._DeleteToken(token.previous)
|
|
||||||
self._DeleteToken(token.next)
|
|
||||||
self._AddFix([token])
|
|
||||||
|
|
||||||
elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION:
|
|
||||||
match = INVERTED_AUTHOR_SPEC.match(token.string)
|
|
||||||
if match:
|
|
||||||
token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'),
|
|
||||||
match.group('email'),
|
|
||||||
match.group('whitespace_after_name'),
|
|
||||||
match.group('name'),
|
|
||||||
match.group('trailing_characters'))
|
|
||||||
self._AddFix(token)
|
|
||||||
|
|
||||||
elif (code == errors.WRONG_INDENTATION and
|
|
||||||
not FLAGS.disable_indentation_fixing):
|
|
||||||
token = tokenutil.GetFirstTokenInSameLine(token)
|
|
||||||
actual = error.position.start
|
|
||||||
expected = error.position.length
|
|
||||||
|
|
||||||
# Cases where first token is param but with leading spaces.
|
|
||||||
if (len(token.string.lstrip()) == len(token.string) - actual and
|
|
||||||
token.string.lstrip()):
|
|
||||||
token.string = token.string.lstrip()
|
|
||||||
actual = 0
|
|
||||||
|
|
||||||
if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0:
|
|
||||||
token.string = token.string.lstrip() + (' ' * expected)
|
|
||||||
self._AddFix([token])
|
|
||||||
else:
|
|
||||||
# We need to add indentation.
|
|
||||||
new_token = Token(' ' * expected, Type.WHITESPACE,
|
|
||||||
token.line, token.line_number)
|
|
||||||
# Note that we'll never need to add indentation at the first line,
|
|
||||||
# since it will always not be indented. Therefore it's safe to assume
|
|
||||||
# token.previous exists.
|
|
||||||
tokenutil.InsertTokenAfter(new_token, token.previous)
|
|
||||||
self._AddFix([token])
|
|
||||||
|
|
||||||
elif code in [errors.MALFORMED_END_OF_SCOPE_COMMENT,
|
|
||||||
errors.MISSING_END_OF_SCOPE_COMMENT]:
|
|
||||||
# Only fix cases where }); is found with no trailing content on the line
|
|
||||||
# other than a comment. Value of 'token' is set to } for this error.
|
|
||||||
if (token.type == Type.END_BLOCK and
|
|
||||||
token.next.type == Type.END_PAREN and
|
|
||||||
token.next.next.type == Type.SEMICOLON):
|
|
||||||
current_token = token.next.next.next
|
|
||||||
removed_tokens = []
|
|
||||||
while current_token and current_token.line_number == token.line_number:
|
|
||||||
if current_token.IsAnyType(Type.WHITESPACE,
|
|
||||||
Type.START_SINGLE_LINE_COMMENT,
|
|
||||||
Type.COMMENT):
|
|
||||||
removed_tokens.append(current_token)
|
|
||||||
current_token = current_token.next
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
if removed_tokens:
|
|
||||||
self._DeleteTokens(removed_tokens[0], len(removed_tokens))
|
|
||||||
|
|
||||||
whitespace_token = Token(' ', Type.WHITESPACE, token.line,
|
|
||||||
token.line_number)
|
|
||||||
start_comment_token = Token('//', Type.START_SINGLE_LINE_COMMENT,
|
|
||||||
token.line, token.line_number)
|
|
||||||
comment_token = Token(' goog.scope', Type.COMMENT, token.line,
|
|
||||||
token.line_number)
|
|
||||||
insertion_tokens = [whitespace_token, start_comment_token,
|
|
||||||
comment_token]
|
|
||||||
|
|
||||||
tokenutil.InsertTokensAfter(insertion_tokens, token.next.next)
|
|
||||||
self._AddFix(removed_tokens + insertion_tokens)
|
|
||||||
|
|
||||||
elif code in [errors.EXTRA_GOOG_PROVIDE, errors.EXTRA_GOOG_REQUIRE]:
|
|
||||||
tokens_in_line = tokenutil.GetAllTokensInSameLine(token)
|
|
||||||
num_delete_tokens = len(tokens_in_line)
|
|
||||||
# If line being deleted is preceded and succeed with blank lines then
|
|
||||||
# delete one blank line also.
|
|
||||||
if (tokens_in_line[0].previous and tokens_in_line[-1].next
|
|
||||||
and tokens_in_line[0].previous.type == Type.BLANK_LINE
|
|
||||||
and tokens_in_line[-1].next.type == Type.BLANK_LINE):
|
|
||||||
num_delete_tokens += 1
|
|
||||||
self._DeleteTokens(tokens_in_line[0], num_delete_tokens)
|
|
||||||
self._AddFix(tokens_in_line)
|
|
||||||
|
|
||||||
elif code in [errors.MISSING_GOOG_PROVIDE, errors.MISSING_GOOG_REQUIRE]:
|
|
||||||
missing_namespaces = error.fix_data[0]
|
|
||||||
need_blank_line = error.fix_data[1] or (not token.previous)
|
|
||||||
|
|
||||||
insert_location = Token('', Type.NORMAL, '', token.line_number - 1)
|
|
||||||
dummy_first_token = insert_location
|
|
||||||
tokenutil.InsertTokenBefore(insert_location, token)
|
|
||||||
|
|
||||||
# If inserting a blank line check blank line does not exist before
|
|
||||||
# token to avoid extra blank lines.
|
|
||||||
if (need_blank_line and insert_location.previous
|
|
||||||
and insert_location.previous.type != Type.BLANK_LINE):
|
|
||||||
tokenutil.InsertBlankLineAfter(insert_location)
|
|
||||||
insert_location = insert_location.next
|
|
||||||
|
|
||||||
for missing_namespace in missing_namespaces:
|
|
||||||
new_tokens = self._GetNewRequireOrProvideTokens(
|
|
||||||
code == errors.MISSING_GOOG_PROVIDE,
|
|
||||||
missing_namespace, insert_location.line_number + 1)
|
|
||||||
tokenutil.InsertLineAfter(insert_location, new_tokens)
|
|
||||||
insert_location = new_tokens[-1]
|
|
||||||
self._AddFix(new_tokens)
|
|
||||||
|
|
||||||
# If inserting a blank line check blank line does not exist after
|
|
||||||
# token to avoid extra blank lines.
|
|
||||||
if (need_blank_line and insert_location.next
|
|
||||||
and insert_location.next.type != Type.BLANK_LINE):
|
|
||||||
tokenutil.InsertBlankLineAfter(insert_location)
|
|
||||||
|
|
||||||
tokenutil.DeleteToken(dummy_first_token)
|
|
||||||
|
|
||||||
def _StripSpace(self, token, before):
|
|
||||||
"""Strip whitespace tokens either preceding or following the given token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token.
|
|
||||||
before: If true, strip space before the token, if false, after it.
|
|
||||||
"""
|
|
||||||
token = token.previous if before else token.next
|
|
||||||
while token and token.type == Type.WHITESPACE:
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
token = token.previous if before else token.next
|
|
||||||
|
|
||||||
def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number):
|
|
||||||
"""Returns a list of tokens to create a goog.require/provide statement.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
is_provide: True if getting tokens for a provide, False for require.
|
|
||||||
namespace: The required or provided namespaces to get tokens for.
|
|
||||||
line_number: The line number the new require or provide statement will be
|
|
||||||
on.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tokens to create a new goog.require or goog.provide statement.
|
|
||||||
"""
|
|
||||||
string = 'goog.require'
|
|
||||||
if is_provide:
|
|
||||||
string = 'goog.provide'
|
|
||||||
line_text = string + '(\'' + namespace + '\');\n'
|
|
||||||
return [
|
|
||||||
Token(string, Type.IDENTIFIER, line_text, line_number),
|
|
||||||
Token('(', Type.START_PAREN, line_text, line_number),
|
|
||||||
Token('\'', Type.SINGLE_QUOTE_STRING_START, line_text, line_number),
|
|
||||||
Token(namespace, Type.STRING_TEXT, line_text, line_number),
|
|
||||||
Token('\'', Type.SINGLE_QUOTE_STRING_END, line_text, line_number),
|
|
||||||
Token(')', Type.END_PAREN, line_text, line_number),
|
|
||||||
Token(';', Type.SEMICOLON, line_text, line_number)
|
|
||||||
]
|
|
||||||
|
|
||||||
def _DeleteToken(self, token):
|
|
||||||
"""Deletes the specified token from the linked list of tokens.
|
|
||||||
|
|
||||||
Updates instance variables pointing to tokens such as _file_token if
|
|
||||||
they reference the deleted token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The token to delete.
|
|
||||||
"""
|
|
||||||
if token == self._file_token:
|
|
||||||
self._file_token = token.next
|
|
||||||
|
|
||||||
tokenutil.DeleteToken(token)
|
|
||||||
|
|
||||||
def _DeleteTokens(self, token, token_count):
|
|
||||||
"""Deletes the given number of tokens starting with the given token.
|
|
||||||
|
|
||||||
Updates instance variables pointing to tokens such as _file_token if
|
|
||||||
they reference the deleted token.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
token: The first token to delete.
|
|
||||||
token_count: The total number of tokens to delete.
|
|
||||||
"""
|
|
||||||
if token == self._file_token:
|
|
||||||
for unused_i in xrange(token_count):
|
|
||||||
self._file_token = self._file_token.next
|
|
||||||
|
|
||||||
tokenutil.DeleteTokens(token, token_count)
|
|
||||||
|
|
||||||
def FinishFile(self):
|
|
||||||
"""Called when the current file has finished style checking.
|
|
||||||
|
|
||||||
Used to go back and fix any errors in the file. It currently supports both
|
|
||||||
js and html files. For js files it does a simple dump of all tokens, but in
|
|
||||||
order to support html file, we need to merge the original file with the new
|
|
||||||
token set back together. This works because the tokenized html file is the
|
|
||||||
original html file with all non js lines kept but blanked out with one blank
|
|
||||||
line token per line of html.
|
|
||||||
"""
|
|
||||||
if self._file_fix_count:
|
|
||||||
# Get the original file content for html.
|
|
||||||
if self._file_is_html:
|
|
||||||
f = open(self._file_name, 'r')
|
|
||||||
original_lines = f.readlines()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
f = self._external_file
|
|
||||||
if not f:
|
|
||||||
error_noun = 'error' if self._file_fix_count == 1 else 'errors'
|
|
||||||
print 'Fixed %d %s in %s' % (
|
|
||||||
self._file_fix_count, error_noun, self._file_name)
|
|
||||||
f = open(self._file_name, 'w')
|
|
||||||
|
|
||||||
token = self._file_token
|
|
||||||
# Finding the first not deleted token.
|
|
||||||
while token.is_deleted:
|
|
||||||
token = token.next
|
|
||||||
# If something got inserted before first token (e.g. due to sorting)
|
|
||||||
# then move to start. Bug 8398202.
|
|
||||||
while token.previous:
|
|
||||||
token = token.previous
|
|
||||||
char_count = 0
|
|
||||||
line = ''
|
|
||||||
while token:
|
|
||||||
line += token.string
|
|
||||||
char_count += len(token.string)
|
|
||||||
|
|
||||||
if token.IsLastInLine():
|
|
||||||
# We distinguish if a blank line in html was from stripped original
|
|
||||||
# file or newly added error fix by looking at the "org_line_number"
|
|
||||||
# field on the token. It is only set in the tokenizer, so for all
|
|
||||||
# error fixes, the value should be None.
|
|
||||||
if (line or not self._file_is_html or
|
|
||||||
token.orig_line_number is None):
|
|
||||||
f.write(line)
|
|
||||||
f.write('\n')
|
|
||||||
else:
|
|
||||||
f.write(original_lines[token.orig_line_number - 1])
|
|
||||||
line = ''
|
|
||||||
if char_count > 80 and token.line_number in self._file_changed_lines:
|
|
||||||
print 'WARNING: Line %d of %s is now longer than 80 characters.' % (
|
|
||||||
token.line_number, self._file_name)
|
|
||||||
|
|
||||||
char_count = 0
|
|
||||||
|
|
||||||
token = token.next
|
|
||||||
|
|
||||||
if not self._external_file:
|
|
||||||
# Close the file if we created it
|
|
||||||
f.close()
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
"""Unit tests for the error_fixer module."""
|
|
||||||
|
|
||||||
# Allow non-Google copyright
|
|
||||||
# pylint: disable=g-bad-file-header
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import testutil
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorFixerTest(googletest.TestCase):
|
|
||||||
"""Unit tests for error_fixer."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.error_fixer = error_fixer.ErrorFixer()
|
|
||||||
|
|
||||||
def testDeleteToken(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)
|
|
||||||
second_token = start_token.next
|
|
||||||
self.error_fixer.HandleFile('test_file', start_token)
|
|
||||||
|
|
||||||
self.error_fixer._DeleteToken(start_token)
|
|
||||||
|
|
||||||
self.assertEqual(second_token, self.error_fixer._file_token)
|
|
||||||
|
|
||||||
def testDeleteTokens(self):
|
|
||||||
start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT)
|
|
||||||
fourth_token = start_token.next.next.next
|
|
||||||
self.error_fixer.HandleFile('test_file', start_token)
|
|
||||||
|
|
||||||
self.error_fixer._DeleteTokens(start_token, 3)
|
|
||||||
|
|
||||||
self.assertEqual(fourth_token, self.error_fixer._file_token)
|
|
||||||
|
|
||||||
_TEST_SCRIPT = """\
|
|
||||||
var x = 3;
|
|
||||||
"""
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/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.
|
|
||||||
|
|
||||||
|
|
||||||
"""A simple, pickle-serializable class to represent a lint error."""
|
|
||||||
|
|
||||||
__author__ = 'nnaze@google.com (Nathan Naze)'
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter.common import erroroutput
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorRecord(object):
|
|
||||||
"""Record-keeping struct that can be serialized back from a process.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
path: Path to the file.
|
|
||||||
error_string: Error string for the user.
|
|
||||||
new_error: Whether this is a "new error" (see errors.NEW_ERRORS).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, path, error_string, new_error):
|
|
||||||
self.path = path
|
|
||||||
self.error_string = error_string
|
|
||||||
self.new_error = new_error
|
|
||||||
|
|
||||||
|
|
||||||
def MakeErrorRecord(path, error):
|
|
||||||
"""Make an error record with correctly formatted error string.
|
|
||||||
|
|
||||||
Errors are not able to be serialized (pickled) over processes because of
|
|
||||||
their pointers to the complex token/context graph. We use an intermediary
|
|
||||||
serializable class to pass back just the relevant information.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: Path of file the error was found in.
|
|
||||||
error: An error.Error instance.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
_ErrorRecord instance.
|
|
||||||
"""
|
|
||||||
new_error = error.code in errors.NEW_ERRORS
|
|
||||||
|
|
||||||
if FLAGS.unix_mode:
|
|
||||||
error_string = erroroutput.GetUnixErrorOutput(
|
|
||||||
path, error, new_error=new_error)
|
|
||||||
else:
|
|
||||||
error_string = erroroutput.GetErrorOutput(error, new_error=new_error)
|
|
||||||
|
|
||||||
return ErrorRecord(path, error_string, new_error)
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2010 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.
|
|
||||||
|
|
||||||
"""Linter error rules class for Closure Linter."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
from closure_linter import errors
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('jsdoc', True,
|
|
||||||
'Whether to report errors for missing JsDoc.')
|
|
||||||
flags.DEFINE_list('disable', None,
|
|
||||||
'Disable specific error. Usage Ex.: gjslint --disable 1,'
|
|
||||||
'0011 foo.js.')
|
|
||||||
flags.DEFINE_integer('max_line_length', 80, 'Maximum line length allowed '
|
|
||||||
'without warning.', lower_bound=1)
|
|
||||||
|
|
||||||
disabled_error_nums = None
|
|
||||||
|
|
||||||
|
|
||||||
def GetMaxLineLength():
|
|
||||||
"""Returns allowed maximum length of line.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Length of line allowed without any warning.
|
|
||||||
"""
|
|
||||||
return FLAGS.max_line_length
|
|
||||||
|
|
||||||
|
|
||||||
def ShouldReportError(error):
|
|
||||||
"""Whether the given error should be reported.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
True for all errors except missing documentation errors and disabled
|
|
||||||
errors. For missing documentation, it returns the value of the
|
|
||||||
jsdoc flag.
|
|
||||||
"""
|
|
||||||
global disabled_error_nums
|
|
||||||
if disabled_error_nums is None:
|
|
||||||
disabled_error_nums = []
|
|
||||||
if FLAGS.disable:
|
|
||||||
for error_str in FLAGS.disable:
|
|
||||||
error_num = 0
|
|
||||||
try:
|
|
||||||
error_num = int(error_str)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
disabled_error_nums.append(error_num)
|
|
||||||
|
|
||||||
return ((FLAGS.jsdoc or error not in (
|
|
||||||
errors.MISSING_PARAMETER_DOCUMENTATION,
|
|
||||||
errors.MISSING_RETURN_DOCUMENTATION,
|
|
||||||
errors.MISSING_MEMBER_DOCUMENTATION,
|
|
||||||
errors.MISSING_PRIVATE,
|
|
||||||
errors.MISSING_JSDOC_TAG_THIS)) and
|
|
||||||
(not FLAGS.disable or error not in disabled_error_nums))
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2013 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.
|
|
||||||
|
|
||||||
"""Medium tests for the gjslint errorrules.
|
|
||||||
|
|
||||||
Currently its just verifying that warnings can't be disabled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorRulesTest(googletest.TestCase):
|
|
||||||
"""Test case to for gjslint errorrules."""
|
|
||||||
|
|
||||||
def testNoMaxLineLengthFlagExists(self):
|
|
||||||
"""Tests that --max_line_length flag does not exists."""
|
|
||||||
self.assertTrue('max_line_length' not in flags.FLAGS.FlagDict())
|
|
||||||
|
|
||||||
def testGetMaxLineLength(self):
|
|
||||||
"""Tests warning are reported for line greater than 80.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# One line > 100 and one line > 80 and < 100. So should produce two
|
|
||||||
# line too long error.
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13'
|
|
||||||
' + 14 + 15 + 16 + 17 + 18 + 19 + 20;',
|
|
||||||
' dummy.aa.j = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13'
|
|
||||||
' + 14 + 15 + 16 + 17 + 18;',
|
|
||||||
'}',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
# Expect line too long.
|
|
||||||
expected = [errors.LINE_TOO_LONG, errors.LINE_TOO_LONG]
|
|
||||||
|
|
||||||
self._AssertErrors(original, expected)
|
|
||||||
|
|
||||||
def testNoDisableFlagExists(self):
|
|
||||||
"""Tests that --disable flag does not exists."""
|
|
||||||
self.assertTrue('disable' not in flags.FLAGS.FlagDict())
|
|
||||||
|
|
||||||
def testWarningsNotDisabled(self):
|
|
||||||
"""Tests warnings are reported when nothing is disabled.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [errors.GOOG_REQUIRES_NOT_ALPHABETIZED,
|
|
||||||
errors.FILE_MISSING_NEWLINE]
|
|
||||||
|
|
||||||
self._AssertErrors(original, expected)
|
|
||||||
|
|
||||||
def _AssertErrors(self, original, expected_errors, include_header=True):
|
|
||||||
"""Asserts that the error fixer corrects original to expected."""
|
|
||||||
if include_header:
|
|
||||||
original = self._GetHeader() + original
|
|
||||||
|
|
||||||
# Trap gjslint's output parse it to get messages added.
|
|
||||||
error_accumulator = erroraccumulator.ErrorAccumulator()
|
|
||||||
runner.Run('testing.js', error_accumulator, source=original)
|
|
||||||
error_nums = [e.code for e in error_accumulator.GetErrors()]
|
|
||||||
|
|
||||||
error_nums.sort()
|
|
||||||
expected_errors.sort()
|
|
||||||
self.assertListEqual(error_nums, expected_errors)
|
|
||||||
|
|
||||||
def _GetHeader(self):
|
|
||||||
"""Returns a fake header for a JavaScript file."""
|
|
||||||
return [
|
|
||||||
'// Copyright 2011 Google Inc. All Rights Reserved.',
|
|
||||||
'',
|
|
||||||
'/**',
|
|
||||||
' * @fileoverview Fake file overview.',
|
|
||||||
' * @author fake@google.com (Fake Person)',
|
|
||||||
' */',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Error codes for JavaScript style checker."""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
|
|
||||||
def ByName(name):
|
|
||||||
"""Get the error code for the given error name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: The name of the error
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The error code
|
|
||||||
"""
|
|
||||||
return globals()[name]
|
|
||||||
|
|
||||||
|
|
||||||
# "File-fatal" errors - these errors stop further parsing of a single file
|
|
||||||
FILE_NOT_FOUND = -1
|
|
||||||
FILE_DOES_NOT_PARSE = -2
|
|
||||||
|
|
||||||
# Spacing
|
|
||||||
EXTRA_SPACE = 1
|
|
||||||
MISSING_SPACE = 2
|
|
||||||
EXTRA_LINE = 3
|
|
||||||
MISSING_LINE = 4
|
|
||||||
ILLEGAL_TAB = 5
|
|
||||||
WRONG_INDENTATION = 6
|
|
||||||
WRONG_BLANK_LINE_COUNT = 7
|
|
||||||
|
|
||||||
# Semicolons
|
|
||||||
MISSING_SEMICOLON = 10
|
|
||||||
MISSING_SEMICOLON_AFTER_FUNCTION = 11
|
|
||||||
ILLEGAL_SEMICOLON_AFTER_FUNCTION = 12
|
|
||||||
REDUNDANT_SEMICOLON = 13
|
|
||||||
|
|
||||||
# Miscellaneous
|
|
||||||
ILLEGAL_PROTOTYPE_MEMBER_VALUE = 100
|
|
||||||
LINE_TOO_LONG = 110
|
|
||||||
LINE_STARTS_WITH_OPERATOR = 120
|
|
||||||
COMMA_AT_END_OF_LITERAL = 121
|
|
||||||
LINE_ENDS_WITH_DOT = 122
|
|
||||||
MULTI_LINE_STRING = 130
|
|
||||||
UNNECESSARY_DOUBLE_QUOTED_STRING = 131
|
|
||||||
UNUSED_PRIVATE_MEMBER = 132
|
|
||||||
UNUSED_LOCAL_VARIABLE = 133
|
|
||||||
|
|
||||||
# Requires, provides
|
|
||||||
GOOG_REQUIRES_NOT_ALPHABETIZED = 140
|
|
||||||
GOOG_PROVIDES_NOT_ALPHABETIZED = 141
|
|
||||||
MISSING_GOOG_REQUIRE = 142
|
|
||||||
MISSING_GOOG_PROVIDE = 143
|
|
||||||
EXTRA_GOOG_REQUIRE = 144
|
|
||||||
EXTRA_GOOG_PROVIDE = 145
|
|
||||||
ALIAS_STMT_NEEDS_GOOG_REQUIRE = 146
|
|
||||||
|
|
||||||
# JsDoc
|
|
||||||
INVALID_JSDOC_TAG = 200
|
|
||||||
INVALID_USE_OF_DESC_TAG = 201
|
|
||||||
NO_BUG_NUMBER_AFTER_BUG_TAG = 202
|
|
||||||
MISSING_PARAMETER_DOCUMENTATION = 210
|
|
||||||
EXTRA_PARAMETER_DOCUMENTATION = 211
|
|
||||||
WRONG_PARAMETER_DOCUMENTATION = 212
|
|
||||||
MISSING_JSDOC_TAG_TYPE = 213
|
|
||||||
MISSING_JSDOC_TAG_DESCRIPTION = 214
|
|
||||||
MISSING_JSDOC_PARAM_NAME = 215
|
|
||||||
OUT_OF_ORDER_JSDOC_TAG_TYPE = 216
|
|
||||||
MISSING_RETURN_DOCUMENTATION = 217
|
|
||||||
UNNECESSARY_RETURN_DOCUMENTATION = 218
|
|
||||||
MISSING_BRACES_AROUND_TYPE = 219
|
|
||||||
MISSING_MEMBER_DOCUMENTATION = 220
|
|
||||||
MISSING_PRIVATE = 221
|
|
||||||
EXTRA_PRIVATE = 222
|
|
||||||
INVALID_OVERRIDE_PRIVATE = 223
|
|
||||||
INVALID_INHERIT_DOC_PRIVATE = 224
|
|
||||||
MISSING_JSDOC_TAG_THIS = 225
|
|
||||||
UNNECESSARY_BRACES_AROUND_INHERIT_DOC = 226
|
|
||||||
INVALID_AUTHOR_TAG_DESCRIPTION = 227
|
|
||||||
JSDOC_PREFER_QUESTION_TO_PIPE_NULL = 230
|
|
||||||
JSDOC_ILLEGAL_QUESTION_WITH_PIPE = 231
|
|
||||||
JSDOC_MISSING_OPTIONAL_TYPE = 232
|
|
||||||
JSDOC_MISSING_OPTIONAL_PREFIX = 233
|
|
||||||
JSDOC_MISSING_VAR_ARGS_TYPE = 234
|
|
||||||
JSDOC_MISSING_VAR_ARGS_NAME = 235
|
|
||||||
JSDOC_DOES_NOT_PARSE = 236
|
|
||||||
# TODO(robbyw): Split this in to more specific syntax problems.
|
|
||||||
INCORRECT_SUPPRESS_SYNTAX = 250
|
|
||||||
INVALID_SUPPRESS_TYPE = 251
|
|
||||||
UNNECESSARY_SUPPRESS = 252
|
|
||||||
|
|
||||||
# File ending
|
|
||||||
FILE_MISSING_NEWLINE = 300
|
|
||||||
FILE_IN_BLOCK = 301
|
|
||||||
|
|
||||||
# Interfaces
|
|
||||||
INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS = 400
|
|
||||||
INTERFACE_METHOD_CANNOT_HAVE_CODE = 401
|
|
||||||
|
|
||||||
# Comments
|
|
||||||
MISSING_END_OF_SCOPE_COMMENT = 500
|
|
||||||
MALFORMED_END_OF_SCOPE_COMMENT = 501
|
|
||||||
|
|
||||||
# goog.scope - Namespace aliasing
|
|
||||||
# TODO(nnaze) Add additional errors here and in aliaspass.py
|
|
||||||
INVALID_USE_OF_GOOG_SCOPE = 600
|
|
||||||
EXTRA_GOOG_SCOPE_USAGE = 601
|
|
||||||
|
|
||||||
# ActionScript specific errors:
|
|
||||||
# TODO(user): move these errors to their own file and move all JavaScript
|
|
||||||
# specific errors to their own file as well.
|
|
||||||
# All ActionScript specific errors should have error number at least 1000.
|
|
||||||
FUNCTION_MISSING_RETURN_TYPE = 1132
|
|
||||||
PARAMETER_MISSING_TYPE = 1133
|
|
||||||
VAR_MISSING_TYPE = 1134
|
|
||||||
PARAMETER_MISSING_DEFAULT_VALUE = 1135
|
|
||||||
IMPORTS_NOT_ALPHABETIZED = 1140
|
|
||||||
IMPORT_CONTAINS_WILDCARD = 1141
|
|
||||||
UNUSED_IMPORT = 1142
|
|
||||||
INVALID_TRACE_SEVERITY_LEVEL = 1250
|
|
||||||
MISSING_TRACE_SEVERITY_LEVEL = 1251
|
|
||||||
MISSING_TRACE_MESSAGE = 1252
|
|
||||||
REMOVE_TRACE_BEFORE_SUBMIT = 1253
|
|
||||||
REMOVE_COMMENT_BEFORE_SUBMIT = 1254
|
|
||||||
# End of list of ActionScript specific errors.
|
|
||||||
|
|
||||||
NEW_ERRORS = frozenset([
|
|
||||||
# Errors added after 2.0.2:
|
|
||||||
WRONG_INDENTATION,
|
|
||||||
MISSING_SEMICOLON,
|
|
||||||
# Errors added after 2.3.9:
|
|
||||||
JSDOC_MISSING_VAR_ARGS_TYPE,
|
|
||||||
JSDOC_MISSING_VAR_ARGS_NAME,
|
|
||||||
# Errors added after 2.3.15:
|
|
||||||
ALIAS_STMT_NEEDS_GOOG_REQUIRE,
|
|
||||||
JSDOC_DOES_NOT_PARSE,
|
|
||||||
LINE_ENDS_WITH_DOT,
|
|
||||||
# Errors added after 2.3.17:
|
|
||||||
])
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Automatically fix simple style guide violations."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robert Walker)'
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import simplefileflags as fileflags
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_list('additional_extensions', None, 'List of additional file '
|
|
||||||
'extensions (not js) that should be treated as '
|
|
||||||
'JavaScript files.')
|
|
||||||
flags.DEFINE_boolean('dry_run', False, 'Do not modify the file, only print it.')
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
"""Main function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
"""
|
|
||||||
if argv is None:
|
|
||||||
argv = flags.FLAGS(sys.argv)
|
|
||||||
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
|
|
||||||
files = fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
output_buffer = None
|
|
||||||
if FLAGS.dry_run:
|
|
||||||
output_buffer = StringIO.StringIO()
|
|
||||||
|
|
||||||
fixer = error_fixer.ErrorFixer(output_buffer)
|
|
||||||
|
|
||||||
# Check the list of files.
|
|
||||||
for filename in files:
|
|
||||||
runner.Run(filename, fixer)
|
|
||||||
if FLAGS.dry_run:
|
|
||||||
print output_buffer.getvalue()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,615 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2008 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.
|
|
||||||
|
|
||||||
"""Medium tests for the gpylint auto-fixer."""
|
|
||||||
|
|
||||||
__author__ = 'robbyw@google.com (Robby Walker)'
|
|
||||||
|
|
||||||
import StringIO
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
from closure_linter import error_fixer
|
|
||||||
from closure_linter import runner
|
|
||||||
|
|
||||||
|
|
||||||
_RESOURCE_PREFIX = 'closure_linter/testdata'
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
|
|
||||||
|
|
||||||
class FixJsStyleTest(googletest.TestCase):
|
|
||||||
"""Test case to for gjslint auto-fixing."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = True
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
flags.FLAGS.dot_on_next_line = False
|
|
||||||
|
|
||||||
def testFixJsStyle(self):
|
|
||||||
test_cases = [
|
|
||||||
['fixjsstyle.in.js', 'fixjsstyle.out.js'],
|
|
||||||
['indentation.js', 'fixjsstyle.indentation.out.js'],
|
|
||||||
['fixjsstyle.html.in.html', 'fixjsstyle.html.out.html'],
|
|
||||||
['fixjsstyle.oplineend.in.js', 'fixjsstyle.oplineend.out.js']]
|
|
||||||
for [running_input_file, running_output_file] in test_cases:
|
|
||||||
print 'Checking %s vs %s' % (running_input_file, running_output_file)
|
|
||||||
input_filename = None
|
|
||||||
golden_filename = None
|
|
||||||
current_filename = None
|
|
||||||
try:
|
|
||||||
input_filename = '%s/%s' % (_RESOURCE_PREFIX, running_input_file)
|
|
||||||
current_filename = input_filename
|
|
||||||
|
|
||||||
golden_filename = '%s/%s' % (_RESOURCE_PREFIX, running_output_file)
|
|
||||||
current_filename = golden_filename
|
|
||||||
except IOError as ex:
|
|
||||||
raise IOError('Could not find testdata resource for %s: %s' %
|
|
||||||
(current_filename, ex))
|
|
||||||
|
|
||||||
if running_input_file == 'fixjsstyle.in.js':
|
|
||||||
with open(input_filename) as f:
|
|
||||||
for line in f:
|
|
||||||
# Go to last line.
|
|
||||||
pass
|
|
||||||
self.assertTrue(line == line.rstrip(), '%s file should not end '
|
|
||||||
'with a new line.' % (input_filename))
|
|
||||||
|
|
||||||
# Autofix the file, sending output to a fake file.
|
|
||||||
actual = StringIO.StringIO()
|
|
||||||
runner.Run(input_filename, error_fixer.ErrorFixer(actual))
|
|
||||||
|
|
||||||
# Now compare the files.
|
|
||||||
actual.seek(0)
|
|
||||||
expected = open(golden_filename, 'r')
|
|
||||||
|
|
||||||
# Uncomment to generate new golden files and run
|
|
||||||
# open('/'.join(golden_filename.split('/')[4:]), 'w').write(actual.read())
|
|
||||||
# actual.seek(0)
|
|
||||||
|
|
||||||
self.assertEqual(actual.readlines(), expected.readlines())
|
|
||||||
|
|
||||||
def testAddProvideFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.provide is added."""
|
|
||||||
original = [
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testAddRequireFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.require is added."""
|
|
||||||
original = [
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteProvideAndAddProvideFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.provide is deleted and added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteProvideAndAddRequireFirstLine(self):
|
|
||||||
"""Tests handling where goog.provide is deleted and goog.require added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteRequireAndAddRequireFirstLine(self):
|
|
||||||
"""Tests handling of case where goog.require is deleted and added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'a = dummy.bb.cc;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testDeleteRequireAndAddProvideFirstLine(self):
|
|
||||||
"""Tests handling where goog.require is deleted and goog.provide added.
|
|
||||||
|
|
||||||
Bug 14832597.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'dummy.bb.cc = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMultipleProvideInsert(self):
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'goog.provide(\'dummy.dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa.ff = 1;',
|
|
||||||
'dummy.bb.ff = 1;',
|
|
||||||
'dummy.cc.ff = 1;',
|
|
||||||
'dummy.dd.ff = 1;',
|
|
||||||
'dummy.ee.ff = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.bb\');',
|
|
||||||
'goog.provide(\'dummy.cc\');',
|
|
||||||
'goog.provide(\'dummy.dd\');',
|
|
||||||
'goog.provide(\'dummy.ee\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa.ff = 1;',
|
|
||||||
'dummy.bb.ff = 1;',
|
|
||||||
'dummy.cc.ff = 1;',
|
|
||||||
'dummy.dd.ff = 1;',
|
|
||||||
'dummy.ee.ff = 1;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMultipleRequireInsert(self):
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.dd\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.aa.ff;',
|
|
||||||
'b = dummy.bb.ff;',
|
|
||||||
'c = dummy.cc.ff;',
|
|
||||||
'd = dummy.dd.ff;',
|
|
||||||
'e = dummy.ee.ff;',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'goog.require(\'dummy.dd\');',
|
|
||||||
'goog.require(\'dummy.ee\');',
|
|
||||||
'',
|
|
||||||
'a = dummy.aa.ff;',
|
|
||||||
'b = dummy.bb.ff;',
|
|
||||||
'c = dummy.cc.ff;',
|
|
||||||
'd = dummy.dd.ff;',
|
|
||||||
'e = dummy.ee.ff;',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testUnsortedRequires(self):
|
|
||||||
"""Tests handling of unsorted goog.require statements without header.
|
|
||||||
|
|
||||||
Bug 8398202.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'function a() {',
|
|
||||||
' dummy.aa.i = 1;',
|
|
||||||
' dummy.Cc.i = 1;',
|
|
||||||
' dummy.Dd.i = 1;',
|
|
||||||
'}',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMissingExtraAndUnsortedRequires(self):
|
|
||||||
"""Tests handling of missing extra and unsorted goog.require statements."""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
'dummy.Cc.someMethod();',
|
|
||||||
'dummy.aa.someMethod();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'goog.require(\'dummy.Cc\');',
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
'dummy.Cc.someMethod();',
|
|
||||||
'dummy.aa.someMethod();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testExtraRequireOnFirstLine(self):
|
|
||||||
"""Tests handling of extra goog.require statement on the first line.
|
|
||||||
|
|
||||||
There was a bug when fixjsstyle quits with an exception. It happened if
|
|
||||||
- the first line of the file is an extra goog.require() statement,
|
|
||||||
- goog.require() statements are not sorted.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.aa\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.bb();',
|
|
||||||
'var y = new dummy.cc();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.require(\'dummy.bb\');',
|
|
||||||
'goog.require(\'dummy.cc\');',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.bb();',
|
|
||||||
'var y = new dummy.cc();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testUnsortedProvides(self):
|
|
||||||
"""Tests handling of unsorted goog.provide statements without header.
|
|
||||||
|
|
||||||
Bug 8398202.
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa = function() {};'
|
|
||||||
'dummy.Cc = function() {};'
|
|
||||||
'dummy.Dd = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.aa = function() {};'
|
|
||||||
'dummy.Cc = function() {};'
|
|
||||||
'dummy.Dd = function() {};'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testMissingExtraAndUnsortedProvides(self):
|
|
||||||
"""Tests handling of missing extra and unsorted goog.provide statements."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.Dd\');',
|
|
||||||
'',
|
|
||||||
'dummy.Cc = function() {};',
|
|
||||||
'dummy.Bb = function() {};',
|
|
||||||
'dummy.aa.someMethod = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Bb\');',
|
|
||||||
'goog.provide(\'dummy.Cc\');',
|
|
||||||
'goog.provide(\'dummy.aa\');',
|
|
||||||
'',
|
|
||||||
'dummy.Cc = function() {};',
|
|
||||||
'dummy.Bb = function() {};',
|
|
||||||
'dummy.aa.someMethod = function();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testNoRequires(self):
|
|
||||||
"""Tests positioning of missing requires without existing requires."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testNoProvides(self):
|
|
||||||
"""Tests positioning of missing provides without existing provides."""
|
|
||||||
original = [
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'dummy.Something\');',
|
|
||||||
'',
|
|
||||||
'goog.require(\'dummy.Bb\');',
|
|
||||||
'',
|
|
||||||
'dummy.Something = function() {};',
|
|
||||||
'',
|
|
||||||
'var x = new dummy.Bb();',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testOutputOkayWhenFirstTokenIsDeleted(self):
|
|
||||||
"""Tests that autofix output is is correct when first token is deleted.
|
|
||||||
|
|
||||||
Regression test for bug 4581567
|
|
||||||
"""
|
|
||||||
original = ['"use strict";']
|
|
||||||
expected = ["'use strict';"]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testGoogScopeIndentation(self):
|
|
||||||
"""Tests Handling a typical end-of-scope indentation fix."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
' // TODO(brain): Take over the world.',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'// TODO(brain): Take over the world.',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMissingEndOfScopeComment(self):
|
|
||||||
"""Tests Handling a missing comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'});',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMissingEndOfScopeCommentWithOtherComment(self):
|
|
||||||
"""Tests handling an irrelevant comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
"}); // I don't belong here!",
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testMalformedEndOfScopeComment(self):
|
|
||||||
"""Tests Handling a malformed comment at end of goog.scope."""
|
|
||||||
original = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope FTW',
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.scope(function() {',
|
|
||||||
'}); // goog.scope',
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testEndsWithIdentifier(self):
|
|
||||||
"""Tests Handling case where script ends with identifier. Bug 7643404."""
|
|
||||||
original = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def testFileStartsWithSemicolon(self):
|
|
||||||
"""Tests handling files starting with semicolon.
|
|
||||||
|
|
||||||
b/10062516
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
';goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected, include_header=False)
|
|
||||||
|
|
||||||
def testCodeStartsWithSemicolon(self):
|
|
||||||
"""Tests handling code in starting with semicolon after comments.
|
|
||||||
|
|
||||||
b/10062516
|
|
||||||
"""
|
|
||||||
original = [
|
|
||||||
';goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
expected = [
|
|
||||||
'goog.provide(\'xyz\');',
|
|
||||||
'',
|
|
||||||
'abc;'
|
|
||||||
]
|
|
||||||
|
|
||||||
self._AssertFixes(original, expected)
|
|
||||||
|
|
||||||
def _AssertFixes(self, original, expected, include_header=True):
|
|
||||||
"""Asserts that the error fixer corrects original to expected."""
|
|
||||||
if include_header:
|
|
||||||
original = self._GetHeader() + original
|
|
||||||
expected = self._GetHeader() + expected
|
|
||||||
|
|
||||||
actual = StringIO.StringIO()
|
|
||||||
runner.Run('testing.js', error_fixer.ErrorFixer(actual), original)
|
|
||||||
actual.seek(0)
|
|
||||||
|
|
||||||
expected = [x + '\n' for x in expected]
|
|
||||||
|
|
||||||
self.assertListEqual(actual.readlines(), expected)
|
|
||||||
|
|
||||||
def _GetHeader(self):
|
|
||||||
"""Returns a fake header for a JavaScript file."""
|
|
||||||
return [
|
|
||||||
'// Copyright 2011 Google Inc. All Rights Reserved.',
|
|
||||||
'',
|
|
||||||
'/**',
|
|
||||||
' * @fileoverview Fake file overview.',
|
|
||||||
' * @author fake@google.com (Fake Person)',
|
|
||||||
' */',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
googletest.main()
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
#
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Full regression-type (Medium) tests for gjslint.
|
|
||||||
|
|
||||||
Tests every error that can be thrown by gjslint. Based heavily on
|
|
||||||
devtools/javascript/gpylint/full_test.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)')
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
import unittest as googletest
|
|
||||||
|
|
||||||
from closure_linter import error_check
|
|
||||||
from closure_linter import errors
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import filetestcase
|
|
||||||
|
|
||||||
_RESOURCE_PREFIX = 'closure_linter/testdata'
|
|
||||||
|
|
||||||
flags.FLAGS.strict = True
|
|
||||||
flags.FLAGS.custom_jsdoc_tags = ('customtag', 'requires')
|
|
||||||
flags.FLAGS.closurized_namespaces = ('goog', 'dummy')
|
|
||||||
flags.FLAGS.limited_doc_files = ('externs.js', 'dummy.js',
|
|
||||||
'limited_doc_checks.js')
|
|
||||||
flags.FLAGS.jslint_error = error_check.Rule.ALL
|
|
||||||
|
|
||||||
# List of files under testdata to test.
|
|
||||||
# We need to list files explicitly since pyglib can't list directories.
|
|
||||||
# TODO(user): Figure out how to list the directory.
|
|
||||||
_TEST_FILES = [
|
|
||||||
'all_js_wrapped.js',
|
|
||||||
'blank_lines.js',
|
|
||||||
'ends_with_block.js',
|
|
||||||
'empty_file.js',
|
|
||||||
'externs.js',
|
|
||||||
'externs_jsdoc.js',
|
|
||||||
'goog_scope.js',
|
|
||||||
'html_parse_error.html',
|
|
||||||
'indentation.js',
|
|
||||||
'interface.js',
|
|
||||||
'jsdoc.js',
|
|
||||||
'limited_doc_checks.js',
|
|
||||||
'minimal.js',
|
|
||||||
'other.js',
|
|
||||||
'provide_blank.js',
|
|
||||||
'provide_extra.js',
|
|
||||||
'provide_missing.js',
|
|
||||||
'require_alias.js',
|
|
||||||
'require_all_caps.js',
|
|
||||||
'require_blank.js',
|
|
||||||
'require_extra.js',
|
|
||||||
'require_function.js',
|
|
||||||
'require_function_missing.js',
|
|
||||||
'require_function_through_both.js',
|
|
||||||
'require_function_through_namespace.js',
|
|
||||||
'require_interface.js',
|
|
||||||
'require_interface_alias.js',
|
|
||||||
'require_interface_base.js',
|
|
||||||
'require_lower_case.js',
|
|
||||||
'require_missing.js',
|
|
||||||
'require_numeric.js',
|
|
||||||
'require_provide_blank.js',
|
|
||||||
'require_provide_missing.js',
|
|
||||||
'require_provide_ok.js',
|
|
||||||
'semicolon_missing.js',
|
|
||||||
'simple.html',
|
|
||||||
'spaces.js',
|
|
||||||
'tokenizer.js',
|
|
||||||
'unparseable.js',
|
|
||||||
'unused_local_variables.js',
|
|
||||||
'unused_private_members.js',
|
|
||||||
'utf8.html',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class GJsLintTestSuite(unittest.TestSuite):
|
|
||||||
"""Test suite to run a GJsLintTest for each of several files.
|
|
||||||
|
|
||||||
If sys.argv[1:] is non-empty, it is interpreted as a list of filenames in
|
|
||||||
testdata to test. Otherwise, _TEST_FILES is used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tests=()):
|
|
||||||
unittest.TestSuite.__init__(self, tests)
|
|
||||||
|
|
||||||
argv = sys.argv and sys.argv[1:] or []
|
|
||||||
if argv:
|
|
||||||
test_files = argv
|
|
||||||
else:
|
|
||||||
test_files = _TEST_FILES
|
|
||||||
for test_file in test_files:
|
|
||||||
resource_path = os.path.join(_RESOURCE_PREFIX, test_file)
|
|
||||||
self.addTest(
|
|
||||||
filetestcase.AnnotatedFileTestCase(
|
|
||||||
resource_path,
|
|
||||||
runner.Run,
|
|
||||||
errors.ByName))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Don't let main parse args; it happens in the TestSuite.
|
|
||||||
googletest.main(argv=sys.argv[0:1], defaultTest='GJsLintTestSuite')
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2007 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.
|
|
||||||
|
|
||||||
"""Checks JavaScript files for common style guide violations.
|
|
||||||
|
|
||||||
gjslint.py is designed to be used as a PRESUBMIT script to check for javascript
|
|
||||||
style guide violations. As of now, it checks for the following violations:
|
|
||||||
|
|
||||||
* Missing and extra spaces
|
|
||||||
* Lines longer than 80 characters
|
|
||||||
* Missing newline at end of file
|
|
||||||
* Missing semicolon after function declaration
|
|
||||||
* Valid JsDoc including parameter matching
|
|
||||||
|
|
||||||
Someday it will validate to the best of its ability against the entirety of the
|
|
||||||
JavaScript style guide.
|
|
||||||
|
|
||||||
This file is a front end that parses arguments and flags. The core of the code
|
|
||||||
is in tokenizer.py and checker.py.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = ('robbyw@google.com (Robert Walker)',
|
|
||||||
'ajp@google.com (Andy Perelson)',
|
|
||||||
'nnaze@google.com (Nathan Naze)',)
|
|
||||||
|
|
||||||
import errno
|
|
||||||
import itertools
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
import gflags as flags
|
|
||||||
|
|
||||||
from closure_linter import errorrecord
|
|
||||||
from closure_linter import runner
|
|
||||||
from closure_linter.common import erroraccumulator
|
|
||||||
from closure_linter.common import simplefileflags as fileflags
|
|
||||||
|
|
||||||
# Attempt import of multiprocessing (should be available in Python 2.6 and up).
|
|
||||||
try:
|
|
||||||
# pylint: disable=g-import-not-at-top
|
|
||||||
import multiprocessing
|
|
||||||
except ImportError:
|
|
||||||
multiprocessing = None
|
|
||||||
|
|
||||||
FLAGS = flags.FLAGS
|
|
||||||
flags.DEFINE_boolean('unix_mode', False,
|
|
||||||
'Whether to emit warnings in standard unix format.')
|
|
||||||
flags.DEFINE_boolean('beep', True, 'Whether to beep when errors are found.')
|
|
||||||
flags.DEFINE_boolean('time', False, 'Whether to emit timing statistics.')
|
|
||||||
flags.DEFINE_boolean('quiet', False, 'Whether to minimize logged messages. '
|
|
||||||
'Most useful for per-file linting, such as that performed '
|
|
||||||
'by the presubmit linter service.')
|
|
||||||
flags.DEFINE_boolean('check_html', False,
|
|
||||||
'Whether to check javascript in html files.')
|
|
||||||
flags.DEFINE_boolean('summary', False,
|
|
||||||
'Whether to show an error count summary.')
|
|
||||||
flags.DEFINE_list('additional_extensions', None, 'List of additional file '
|
|
||||||
'extensions (not js) that should be treated as '
|
|
||||||
'JavaScript files.')
|
|
||||||
flags.DEFINE_boolean('multiprocess',
|
|
||||||
platform.system() is 'Linux' and bool(multiprocessing),
|
|
||||||
'Whether to attempt parallelized linting using the '
|
|
||||||
'multiprocessing module. Enabled by default on Linux '
|
|
||||||
'if the multiprocessing module is present (Python 2.6+). '
|
|
||||||
'Otherwise disabled by default. '
|
|
||||||
'Disabling may make debugging easier.')
|
|
||||||
flags.ADOPT_module_key_flags(fileflags)
|
|
||||||
flags.ADOPT_module_key_flags(runner)
|
|
||||||
|
|
||||||
|
|
||||||
GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time',
|
|
||||||
'--check_html', '--summary', '--quiet']
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _MultiprocessCheckPaths(paths):
|
|
||||||
"""Run _CheckPath over mutltiple processes.
|
|
||||||
|
|
||||||
Tokenization, passes, and checks are expensive operations. Running in a
|
|
||||||
single process, they can only run on one CPU/core. Instead,
|
|
||||||
shard out linting over all CPUs with multiprocessing to parallelize.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paths: paths to check.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pool = multiprocessing.Pool()
|
|
||||||
|
|
||||||
path_results = pool.imap(_CheckPath, paths)
|
|
||||||
for results in path_results:
|
|
||||||
for result in results:
|
|
||||||
yield result
|
|
||||||
|
|
||||||
# Force destruct before returning, as this can sometimes raise spurious
|
|
||||||
# "interrupted system call" (EINTR), which we can ignore.
|
|
||||||
try:
|
|
||||||
pool.close()
|
|
||||||
pool.join()
|
|
||||||
del pool
|
|
||||||
except OSError as err:
|
|
||||||
if err.errno is not errno.EINTR:
|
|
||||||
raise err
|
|
||||||
|
|
||||||
|
|
||||||
def _CheckPaths(paths):
|
|
||||||
"""Run _CheckPath on all paths in one thread.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
paths: paths to check.
|
|
||||||
|
|
||||||
Yields:
|
|
||||||
errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
results = _CheckPath(path)
|
|
||||||
for record in results:
|
|
||||||
yield record
|
|
||||||
|
|
||||||
|
|
||||||
def _CheckPath(path):
|
|
||||||
"""Check a path and return any errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
path: paths to check.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of errorrecord.ErrorRecords for any found errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
error_handler = erroraccumulator.ErrorAccumulator()
|
|
||||||
runner.Run(path, error_handler)
|
|
||||||
|
|
||||||
make_error_record = lambda err: errorrecord.MakeErrorRecord(path, err)
|
|
||||||
return map(make_error_record, error_handler.GetErrors())
|
|
||||||
|
|
||||||
|
|
||||||
def _GetFilePaths(argv):
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
if FLAGS.check_html:
|
|
||||||
suffixes += ['.html', '.htm']
|
|
||||||
return fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
|
|
||||||
# Error printing functions
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintFileSummary(paths, records):
|
|
||||||
"""Print a detailed summary of the number of errors in each file."""
|
|
||||||
|
|
||||||
paths = list(paths)
|
|
||||||
paths.sort()
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
path_errors = [e for e in records if e.path == path]
|
|
||||||
print '%s: %d' % (path, len(path_errors))
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintFileSeparator(path):
|
|
||||||
print '----- FILE : %s -----' % path
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintSummary(paths, error_records):
|
|
||||||
"""Print a summary of the number of errors and files."""
|
|
||||||
|
|
||||||
error_count = len(error_records)
|
|
||||||
all_paths = set(paths)
|
|
||||||
all_paths_count = len(all_paths)
|
|
||||||
|
|
||||||
if error_count is 0:
|
|
||||||
print '%d files checked, no errors found.' % all_paths_count
|
|
||||||
|
|
||||||
new_error_count = len([e for e in error_records if e.new_error])
|
|
||||||
|
|
||||||
error_paths = set([e.path for e in error_records])
|
|
||||||
error_paths_count = len(error_paths)
|
|
||||||
no_error_paths_count = all_paths_count - error_paths_count
|
|
||||||
|
|
||||||
if (error_count or new_error_count) and not FLAGS.quiet:
|
|
||||||
error_noun = 'error' if error_count == 1 else 'errors'
|
|
||||||
new_error_noun = 'error' if new_error_count == 1 else 'errors'
|
|
||||||
error_file_noun = 'file' if error_paths_count == 1 else 'files'
|
|
||||||
ok_file_noun = 'file' if no_error_paths_count == 1 else 'files'
|
|
||||||
print ('Found %d %s, including %d new %s, in %d %s (%d %s OK).' %
|
|
||||||
(error_count,
|
|
||||||
error_noun,
|
|
||||||
new_error_count,
|
|
||||||
new_error_noun,
|
|
||||||
error_paths_count,
|
|
||||||
error_file_noun,
|
|
||||||
no_error_paths_count,
|
|
||||||
ok_file_noun))
|
|
||||||
|
|
||||||
|
|
||||||
def _PrintErrorRecords(error_records):
|
|
||||||
"""Print error records strings in the expected format."""
|
|
||||||
|
|
||||||
current_path = None
|
|
||||||
for record in error_records:
|
|
||||||
|
|
||||||
if current_path != record.path:
|
|
||||||
current_path = record.path
|
|
||||||
if not FLAGS.unix_mode:
|
|
||||||
_PrintFileSeparator(current_path)
|
|
||||||
|
|
||||||
print record.error_string
|
|
||||||
|
|
||||||
|
|
||||||
def _FormatTime(t):
|
|
||||||
"""Formats a duration as a human-readable string.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
t: A duration in seconds.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A formatted duration string.
|
|
||||||
"""
|
|
||||||
if t < 1:
|
|
||||||
return '%dms' % round(t * 1000)
|
|
||||||
else:
|
|
||||||
return '%.2fs' % t
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
"""Main function.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
argv: Sequence of command line arguments.
|
|
||||||
"""
|
|
||||||
if argv is None:
|
|
||||||
argv = flags.FLAGS(sys.argv)
|
|
||||||
|
|
||||||
if FLAGS.time:
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
suffixes = ['.js']
|
|
||||||
if FLAGS.additional_extensions:
|
|
||||||
suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions]
|
|
||||||
if FLAGS.check_html:
|
|
||||||
suffixes += ['.html', '.htm']
|
|
||||||
paths = fileflags.GetFileList(argv, 'JavaScript', suffixes)
|
|
||||||
|
|
||||||
if FLAGS.multiprocess:
|
|
||||||
records_iter = _MultiprocessCheckPaths(paths)
|
|
||||||
else:
|
|
||||||
records_iter = _CheckPaths(paths)
|
|
||||||
|
|
||||||
records_iter, records_iter_copy = itertools.tee(records_iter, 2)
|
|
||||||
_PrintErrorRecords(records_iter_copy)
|
|
||||||
|
|
||||||
error_records = list(records_iter)
|
|
||||||
_PrintSummary(paths, error_records)
|
|
||||||
|
|
||||||
exit_code = 0
|
|
||||||
|
|
||||||
# If there are any errors
|
|
||||||
if error_records:
|
|
||||||
exit_code += 1
|
|
||||||
|
|
||||||
# If there are any new errors
|
|
||||||
if [r for r in error_records if r.new_error]:
|
|
||||||
exit_code += 2
|
|
||||||
|
|
||||||
if exit_code:
|
|
||||||
if FLAGS.summary:
|
|
||||||
_PrintFileSummary(paths, error_records)
|
|
||||||
|
|
||||||
if FLAGS.beep:
|
|
||||||
# Make a beep noise.
|
|
||||||
sys.stdout.write(chr(7))
|
|
||||||
|
|
||||||
# Write out instructions for using fixjsstyle script to fix some of the
|
|
||||||
# reported errors.
|
|
||||||
fix_args = []
|
|
||||||
for flag in sys.argv[1:]:
|
|
||||||
for f in GJSLINT_ONLY_FLAGS:
|
|
||||||
if flag.startswith(f):
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
fix_args.append(flag)
|
|
||||||
|
|
||||||
if not FLAGS.quiet:
|
|
||||||
print """
|
|
||||||
Some of the errors reported by GJsLint may be auto-fixable using the script
|
|
||||||
fixjsstyle. Please double check any changes it makes and report any bugs. The
|
|
||||||
script can be run by executing:
|
|
||||||
|
|
||||||
fixjsstyle %s """ % ' '.join(fix_args)
|
|
||||||
|
|
||||||
if FLAGS.time:
|
|
||||||
print 'Done in %s.' % _FormatTime(time.time() - start_time)
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
Loaded 100 of 3016 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user