mirror of https://github.com/lukechilds/node.git
Ryan Dahl
14 years ago
46 changed files with 9435 additions and 1 deletions
@ -0,0 +1,10 @@ |
|||||
|
Metadata-Version: 1.0 |
||||
|
Name: closure_linter |
||||
|
Version: 2.2.6 |
||||
|
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 |
@ -0,0 +1,9 @@ |
|||||
|
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 |
@ -0,0 +1,10 @@ |
|||||
|
Metadata-Version: 1.0 |
||||
|
Name: closure-linter |
||||
|
Version: 2.2.6 |
||||
|
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 |
@ -0,0 +1,41 @@ |
|||||
|
README |
||||
|
setup.py |
||||
|
closure_linter/__init__.py |
||||
|
closure_linter/checker.py |
||||
|
closure_linter/checkerbase.py |
||||
|
closure_linter/ecmalintrules.py |
||||
|
closure_linter/ecmametadatapass.py |
||||
|
closure_linter/error_fixer.py |
||||
|
closure_linter/errorrules.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/statetracker.py |
||||
|
closure_linter/tokenutil.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/errorprinter.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 |
@ -0,0 +1 @@ |
|||||
|
|
@ -0,0 +1,4 @@ |
|||||
|
[console_scripts] |
||||
|
fixjsstyle = closure_linter.fixjsstyle:main |
||||
|
gjslint = closure_linter.gjslint:main |
||||
|
|
@ -0,0 +1 @@ |
|||||
|
python-gflags |
@ -0,0 +1 @@ |
|||||
|
closure_linter |
@ -0,0 +1 @@ |
|||||
|
#!/usr/bin/env python |
@ -0,0 +1,82 @@ |
|||||
|
#!/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 checkerbase |
||||
|
from closure_linter import ecmametadatapass |
||||
|
from closure_linter import errors |
||||
|
from closure_linter import javascriptlintrules |
||||
|
from closure_linter import javascriptstatetracker |
||||
|
from closure_linter.common import errorprinter |
||||
|
from closure_linter.common import lintrunner |
||||
|
|
||||
|
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.') |
||||
|
|
||||
|
|
||||
|
class JavaScriptStyleChecker(checkerbase.CheckerBase): |
||||
|
"""Checker that applies JavaScriptLintRules.""" |
||||
|
|
||||
|
def __init__(self, error_handler): |
||||
|
"""Initialize an JavaScriptStyleChecker object. |
||||
|
|
||||
|
Args: |
||||
|
error_handler: Error handler to pass all errors to |
||||
|
""" |
||||
|
checkerbase.CheckerBase.__init__( |
||||
|
self, |
||||
|
error_handler=error_handler, |
||||
|
lint_rules=javascriptlintrules.JavaScriptLintRules(), |
||||
|
state_tracker=javascriptstatetracker.JavaScriptStateTracker( |
||||
|
closurized_namespaces=flags.FLAGS.closurized_namespaces), |
||||
|
metadata_pass=ecmametadatapass.EcmaMetaDataPass(), |
||||
|
limited_doc_files=flags.FLAGS.limited_doc_files) |
||||
|
|
||||
|
|
||||
|
class GJsLintRunner(lintrunner.LintRunner): |
||||
|
"""Wrapper class to run GJsLint.""" |
||||
|
|
||||
|
def Run(self, filenames, error_handler=None): |
||||
|
"""Run GJsLint on the given filenames. |
||||
|
|
||||
|
Args: |
||||
|
filenames: The filenames to check |
||||
|
error_handler: An optional ErrorHandler object, an ErrorPrinter is used if |
||||
|
none is specified. |
||||
|
|
||||
|
Returns: |
||||
|
error_count, file_count: The number of errors and the number of files that |
||||
|
contain errors. |
||||
|
""" |
||||
|
if not error_handler: |
||||
|
error_handler = errorprinter.ErrorPrinter(errors.NEW_ERRORS) |
||||
|
|
||||
|
checker = JavaScriptStyleChecker(error_handler) |
||||
|
|
||||
|
# Check the list of files. |
||||
|
for filename in filenames: |
||||
|
checker.Check(filename) |
||||
|
|
||||
|
return error_handler |
@ -0,0 +1,237 @@ |
|||||
|
#!/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.""" |
||||
|
|
||||
|
__author__ = ('robbyw@google.com (Robert Walker)', |
||||
|
'ajp@google.com (Andy Perelson)', |
||||
|
'jacobr@google.com (Jacob Richman)') |
||||
|
|
||||
|
import traceback |
||||
|
|
||||
|
import gflags as flags |
||||
|
from closure_linter import ecmametadatapass |
||||
|
from closure_linter import errorrules |
||||
|
from closure_linter import errors |
||||
|
from closure_linter import javascripttokenizer |
||||
|
from closure_linter.common import error |
||||
|
from closure_linter.common import htmlutil |
||||
|
|
||||
|
FLAGS = flags.FLAGS |
||||
|
flags.DEFINE_boolean('debug_tokens', False, |
||||
|
'Whether to print all tokens for debugging.') |
||||
|
|
||||
|
flags.DEFINE_boolean('error_trace', False, |
||||
|
'Whether to show error exceptions.') |
||||
|
|
||||
|
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 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, tokenizer_mode): |
||||
|
"""Perform all checks that need to occur after all lines are processed. |
||||
|
|
||||
|
Args: |
||||
|
parser_state: State of the parser after parsing all tokens |
||||
|
tokenizer_mode: Mode of the tokenizer after parsing the entire page |
||||
|
|
||||
|
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, |
||||
|
limited_doc_files=None, metadata_pass=None): |
||||
|
"""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. |
||||
|
limited_doc_files: List of filenames that are not required to have |
||||
|
documentation comments. |
||||
|
metadata_pass: Object that builds metadata about the token stream. |
||||
|
""" |
||||
|
self.__error_handler = error_handler |
||||
|
self.__lint_rules = lint_rules |
||||
|
self.__state_tracker = state_tracker |
||||
|
self.__metadata_pass = metadata_pass |
||||
|
self.__limited_doc_files = limited_doc_files |
||||
|
self.__tokenizer = javascripttokenizer.JavaScriptTokenizer() |
||||
|
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, filename): |
||||
|
"""Checks the file, printing warnings and errors as they are found. |
||||
|
|
||||
|
Args: |
||||
|
filename: The name of the file to check. |
||||
|
""" |
||||
|
try: |
||||
|
f = open(filename) |
||||
|
except IOError: |
||||
|
self.__error_handler.HandleFile(filename, None) |
||||
|
self.HandleError(errors.FILE_NOT_FOUND, 'File not found', None) |
||||
|
self.__error_handler.FinishFile() |
||||
|
return |
||||
|
|
||||
|
try: |
||||
|
if filename.endswith('.html') or filename.endswith('.htm'): |
||||
|
self.CheckLines(filename, htmlutil.GetScriptLines(f), True) |
||||
|
else: |
||||
|
self.CheckLines(filename, f, False) |
||||
|
finally: |
||||
|
f.close() |
||||
|
|
||||
|
def CheckLines(self, filename, lines_iter, is_html): |
||||
|
"""Checks a file, given as an iterable of lines, for warnings and errors. |
||||
|
|
||||
|
Args: |
||||
|
filename: The name of the file to check. |
||||
|
lines_iter: An iterator that yields one line of the file at a time. |
||||
|
is_html: Whether the file being checked is an HTML file with extracted |
||||
|
contents. |
||||
|
|
||||
|
Returns: |
||||
|
A boolean indicating whether the full file could be checked or if checking |
||||
|
failed prematurely. |
||||
|
""" |
||||
|
limited_doc_checks = False |
||||
|
if self.__limited_doc_files: |
||||
|
for limited_doc_filename in self.__limited_doc_files: |
||||
|
if filename.endswith(limited_doc_filename): |
||||
|
limited_doc_checks = True |
||||
|
break |
||||
|
|
||||
|
state_tracker = self.__state_tracker |
||||
|
lint_rules = self.__lint_rules |
||||
|
state_tracker.Reset() |
||||
|
lint_rules.Initialize(self, limited_doc_checks, is_html) |
||||
|
|
||||
|
token = self.__tokenizer.TokenizeFile(lines_iter) |
||||
|
|
||||
|
parse_error = None |
||||
|
if self.__metadata_pass: |
||||
|
try: |
||||
|
self.__metadata_pass.Reset() |
||||
|
self.__metadata_pass.Process(token) |
||||
|
except ecmametadatapass.ParseError, caught_parse_error: |
||||
|
if FLAGS.error_trace: |
||||
|
traceback.print_exc() |
||||
|
parse_error = caught_parse_error |
||||
|
except Exception: |
||||
|
print 'Internal error in %s' % filename |
||||
|
traceback.print_exc() |
||||
|
return False |
||||
|
|
||||
|
self.__error_handler.HandleFile(filename, token) |
||||
|
|
||||
|
while token: |
||||
|
if FLAGS.debug_tokens: |
||||
|
print token |
||||
|
|
||||
|
if parse_error and parse_error.token == token: |
||||
|
# Report any parse errors from above once we find the token. |
||||
|
message = ('Error parsing file at token "%s". Unable to ' |
||||
|
'check the rest of file.' % token.string) |
||||
|
self.HandleError(errors.FILE_DOES_NOT_PARSE, message, token) |
||||
|
self.__error_handler.FinishFile() |
||||
|
return False |
||||
|
|
||||
|
if FLAGS.error_trace: |
||||
|
state_tracker.HandleToken(token, state_tracker.GetLastNonSpaceToken()) |
||||
|
else: |
||||
|
try: |
||||
|
state_tracker.HandleToken(token, state_tracker.GetLastNonSpaceToken()) |
||||
|
except: |
||||
|
self.HandleError(errors.FILE_DOES_NOT_PARSE, |
||||
|
('Error parsing file at token "%s". Unable to ' |
||||
|
'check the rest of file.' % token.string), |
||||
|
token) |
||||
|
self.__error_handler.FinishFile() |
||||
|
return False |
||||
|
|
||||
|
# Check the token for style guide violations. |
||||
|
lint_rules.CheckToken(token, state_tracker) |
||||
|
|
||||
|
state_tracker.HandleAfterToken(token) |
||||
|
|
||||
|
# Move to the next token. |
||||
|
token = token.next |
||||
|
|
||||
|
lint_rules.Finalize(state_tracker, self.__tokenizer.mode) |
||||
|
self.__error_handler.FinishFile() |
||||
|
return True |
@ -0,0 +1 @@ |
|||||
|
#!/usr/bin/env python |
@ -0,0 +1,65 @@ |
|||||
|
#!/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, position, fix_data): |
||||
|
"""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) |
@ -0,0 +1,46 @@ |
|||||
|
#!/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.token.line_number, error.code)) |
||||
|
|
||||
|
def GetErrors(self): |
||||
|
"""Returns the accumulated errors. |
||||
|
|
||||
|
Returns: |
||||
|
A sequence of errors. |
||||
|
""" |
||||
|
return self._errors |
@ -0,0 +1,61 @@ |
|||||
|
#!/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. |
||||
|
""" |
@ -0,0 +1,203 @@ |
|||||
|
#!/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 prints errors to stdout.""" |
||||
|
|
||||
|
__author__ = ('robbyw@google.com (Robert Walker)', |
||||
|
'ajp@google.com (Andy Perelson)') |
||||
|
|
||||
|
from closure_linter.common import error |
||||
|
from closure_linter.common import errorhandler |
||||
|
|
||||
|
Error = error.Error |
||||
|
|
||||
|
|
||||
|
# The error message is of the format: |
||||
|
# Line <number>, E:<code>: message |
||||
|
DEFAULT_FORMAT = 1 |
||||
|
|
||||
|
# The error message is of the format: |
||||
|
# filename:[line number]:message |
||||
|
UNIX_FORMAT = 2 |
||||
|
|
||||
|
|
||||
|
class ErrorPrinter(errorhandler.ErrorHandler): |
||||
|
"""ErrorHandler that prints errors to stdout.""" |
||||
|
|
||||
|
def __init__(self, new_errors=None): |
||||
|
"""Initializes this error printer. |
||||
|
|
||||
|
Args: |
||||
|
new_errors: A sequence of error codes representing recently introduced |
||||
|
errors, defaults to None. |
||||
|
""" |
||||
|
# Number of errors |
||||
|
self._error_count = 0 |
||||
|
|
||||
|
# Number of new errors |
||||
|
self._new_error_count = 0 |
||||
|
|
||||
|
# Number of files checked |
||||
|
self._total_file_count = 0 |
||||
|
|
||||
|
# Number of files with errors |
||||
|
self._error_file_count = 0 |
||||
|
|
||||
|
# Dict of file name to number of errors |
||||
|
self._file_table = {} |
||||
|
|
||||
|
# List of errors for each file |
||||
|
self._file_errors = None |
||||
|
|
||||
|
# Current file |
||||
|
self._filename = None |
||||
|
|
||||
|
self._format = DEFAULT_FORMAT |
||||
|
|
||||
|
if new_errors: |
||||
|
self._new_errors = frozenset(new_errors) |
||||
|
else: |
||||
|
self._new_errors = frozenset(set()) |
||||
|
|
||||
|
def SetFormat(self, format): |
||||
|
"""Sets the print format of errors. |
||||
|
|
||||
|
Args: |
||||
|
format: One of {DEFAULT_FORMAT, UNIX_FORMAT}. |
||||
|
""" |
||||
|
self._format = format |
||||
|
|
||||
|
def HandleFile(self, filename, first_token): |
||||
|
"""Notifies this ErrorPrinter that subsequent errors are in filename. |
||||
|
|
||||
|
Sets the current file name, and sets a flag stating the header for this file |
||||
|
has not been printed yet. |
||||
|
|
||||
|
Should be called by a linter before a file is style checked. |
||||
|
|
||||
|
Args: |
||||
|
filename: The name of the file about to be checked. |
||||
|
first_token: The first token in the file, or None if there was an error |
||||
|
opening the file |
||||
|
""" |
||||
|
if self._filename and self._file_table[self._filename]: |
||||
|
print |
||||
|
|
||||
|
self._filename = filename |
||||
|
self._file_table[filename] = 0 |
||||
|
self._total_file_count += 1 |
||||
|
self._file_errors = [] |
||||
|
|
||||
|
def HandleError(self, error): |
||||
|
"""Prints a formatted error message about the specified error. |
||||
|
|
||||
|
The error message is of the format: |
||||
|
Error #<code>, line #<number>: message |
||||
|
|
||||
|
Args: |
||||
|
error: The error object |
||||
|
""" |
||||
|
self._file_errors.append(error) |
||||
|
self._file_table[self._filename] += 1 |
||||
|
self._error_count += 1 |
||||
|
|
||||
|
if self._new_errors and error.code in self._new_errors: |
||||
|
self._new_error_count += 1 |
||||
|
|
||||
|
def _PrintError(self, error): |
||||
|
"""Prints a formatted error message about the specified error. |
||||
|
|
||||
|
Args: |
||||
|
error: The error object |
||||
|
""" |
||||
|
new_error = self._new_errors and error.code in self._new_errors |
||||
|
if self._format == DEFAULT_FORMAT: |
||||
|
line = '' |
||||
|
if error.token: |
||||
|
line = 'Line %d, ' % error.token.line_number |
||||
|
|
||||
|
code = 'E:%04d' % error.code |
||||
|
if new_error: |
||||
|
print '%s%s: (New error) %s' % (line, code, error.message) |
||||
|
else: |
||||
|
print '%s%s: %s' % (line, code, error.message) |
||||
|
else: |
||||
|
# UNIX format |
||||
|
filename = self._filename |
||||
|
line = '' |
||||
|
if error.token: |
||||
|
line = '%d' % error.token.line_number |
||||
|
|
||||
|
error_code = '%04d' % error.code |
||||
|
if new_error: |
||||
|
error_code = 'New Error ' + error_code |
||||
|
print '%s:%s:(%s) %s' % (filename, line, error_code, error.message) |
||||
|
|
||||
|
def FinishFile(self): |
||||
|
"""Finishes handling the current file.""" |
||||
|
if self._file_errors: |
||||
|
self._error_file_count += 1 |
||||
|
|
||||
|
if self._format != UNIX_FORMAT: |
||||
|
print '----- FILE : %s -----' % (self._filename) |
||||
|
|
||||
|
self._file_errors.sort(Error.Compare) |
||||
|
|
||||
|
for error in self._file_errors: |
||||
|
self._PrintError(error) |
||||
|
|
||||
|
def HasErrors(self): |
||||
|
"""Whether this error printer encountered any errors. |
||||
|
|
||||
|
Returns: |
||||
|
True if the error printer encountered any errors. |
||||
|
""" |
||||
|
return self._error_count |
||||
|
|
||||
|
def HasNewErrors(self): |
||||
|
"""Whether this error printer encountered any new errors. |
||||
|
|
||||
|
Returns: |
||||
|
True if the error printer encountered any new errors. |
||||
|
""" |
||||
|
return self._new_error_count |
||||
|
|
||||
|
def HasOldErrors(self): |
||||
|
"""Whether this error printer encountered any old errors. |
||||
|
|
||||
|
Returns: |
||||
|
True if the error printer encountered any old errors. |
||||
|
""" |
||||
|
return self._error_count - self._new_error_count |
||||
|
|
||||
|
def PrintSummary(self): |
||||
|
"""Print a summary of the number of errors and files.""" |
||||
|
if self.HasErrors() or self.HasNewErrors(): |
||||
|
print ('Found %d errors, including %d new errors, in %d files ' |
||||
|
'(%d files OK).' % ( |
||||
|
self._error_count, |
||||
|
self._new_error_count, |
||||
|
self._error_file_count, |
||||
|
self._total_file_count - self._error_file_count)) |
||||
|
else: |
||||
|
print '%d files checked, no errors found.' % self._total_file_count |
||||
|
|
||||
|
def PrintFileSummary(self): |
||||
|
"""Print a detailed summary of the number of errors in each file.""" |
||||
|
keys = self._file_table.keys() |
||||
|
keys.sort() |
||||
|
for filename in keys: |
||||
|
print '%s: %d' % (filename, self._file_table[filename]) |
@ -0,0 +1,105 @@ |
|||||
|
#!/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 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, runner, converter): |
||||
|
"""Create a single file lint test case. |
||||
|
|
||||
|
Args: |
||||
|
filename: Filename to test. |
||||
|
runner: Object implementing the LintRunner interface that lints a file. |
||||
|
converter: Function taking an error string and returning an error code. |
||||
|
""" |
||||
|
|
||||
|
googletest.TestCase.__init__(self, 'runTest') |
||||
|
self._filename = filename |
||||
|
self._messages = [] |
||||
|
self._runner = runner |
||||
|
self._converter = converter |
||||
|
|
||||
|
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, 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 gpylint's output parse it to get messages added.""" |
||||
|
errors = erroraccumulator.ErrorAccumulator() |
||||
|
self._runner.Run([filename], errors) |
||||
|
|
||||
|
errors = errors.GetErrors() |
||||
|
errors.sort() |
||||
|
return errors |
@ -0,0 +1,170 @@ |
|||||
|
#!/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() |
@ -0,0 +1,39 @@ |
|||||
|
#!/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. |
||||
|
""" |
@ -0,0 +1,60 @@ |
|||||
|
#!/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 |
@ -0,0 +1,126 @@ |
|||||
|
#!/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) |
@ -0,0 +1,190 @@ |
|||||
|
#!/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) |
@ -0,0 +1,184 @@ |
|||||
|
#!/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) |
||||
|
|
||||
|
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 |
@ -0,0 +1,125 @@ |
|||||
|
#!/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): |
||||
|
"""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. |
||||
|
""" |
||||
|
self.type = token_type |
||||
|
self.string = string |
||||
|
self.length = len(string) |
||||
|
self.line = line |
||||
|
self.line_number = line_number |
||||
|
self.values = values |
||||
|
|
||||
|
# 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) |
@ -0,0 +1,752 @@ |
|||||
|
#!/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 |
||||
|
|
||||
|
from closure_linter import checkerbase |
||||
|
from closure_linter import ecmametadatapass |
||||
|
from closure_linter import errors |
||||
|
from closure_linter import indentation |
||||
|
from closure_linter import javascripttokens |
||||
|
from closure_linter import javascripttokenizer |
||||
|
from closure_linter import statetracker |
||||
|
from closure_linter import tokenutil |
||||
|
from closure_linter.common import error |
||||
|
from closure_linter.common import htmlutil |
||||
|
from closure_linter.common import lintrunner |
||||
|
from closure_linter.common import position |
||||
|
from closure_linter.common import tokens |
||||
|
import gflags as flags |
||||
|
|
||||
|
FLAGS = flags.FLAGS |
||||
|
flags.DEFINE_boolean('strict', False, |
||||
|
'Whether to validate against the stricter Closure style.') |
||||
|
flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow') |
||||
|
|
||||
|
# 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 |
||||
|
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. |
||||
|
""" |
||||
|
|
||||
|
# Static constants. |
||||
|
MAX_LINE_LENGTH = 80 |
||||
|
|
||||
|
MISSING_PARAMETER_SPACE = re.compile(r',\S') |
||||
|
|
||||
|
EXTRA_SPACE = re.compile('(\(\s|\s\))') |
||||
|
|
||||
|
ENDS_WITH_SPACE = re.compile('\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]) |
||||
|
|
||||
|
def __init__(self): |
||||
|
"""Initialize this lint rule object.""" |
||||
|
checkerbase.LintRulesBase.__init__(self) |
||||
|
|
||||
|
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. |
||||
|
""" |
||||
|
# 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.NORMAL): |
||||
|
# Dots are acceptable places to wrap. |
||||
|
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: |
||||
|
# 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 > self.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 = 1 |
||||
|
if '@param' in parts: |
||||
|
max = 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): |
||||
|
self._HandleError(errors.LINE_TOO_LONG, |
||||
|
'Line too long (%d characters).' % len(line), last_token) |
||||
|
|
||||
|
def _CheckJsDocType(self, token): |
||||
|
"""Checks the given type for style errors. |
||||
|
|
||||
|
Args: |
||||
|
token: The DOC_FLAG token for the flag whose type to check. |
||||
|
""" |
||||
|
flag = token.attached_object |
||||
|
type = flag.type |
||||
|
if type and type is not None and not type.isspace(): |
||||
|
pieces = self.TYPE_SPLIT.split(type) |
||||
|
if len(pieces) == 1 and type.count('|') == 1 and ( |
||||
|
type.endswith('|null') or type.startswith('null|')): |
||||
|
self._HandleError(errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL, |
||||
|
'Prefer "?Type" to "Type|null": "%s"' % type, token) |
||||
|
|
||||
|
for p in pieces: |
||||
|
if p.count('|') and p.count('?'): |
||||
|
# TODO(robbyw): We should do actual parsing of JsDoc types. As is, |
||||
|
# this won't report an error for {number|Array.<string>?}, etc. |
||||
|
self._HandleError(errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE, |
||||
|
'JsDoc types cannot contain both "?" and "|": "%s"' % p, token) |
||||
|
|
||||
|
if FLAGS.strict 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) |
||||
|
|
||||
|
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.AtBeginning()) |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
# 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 (token.string == ':' and |
||||
|
token.metadata.context.type in (Context.LITERAL_ELEMENT, |
||||
|
Context.CASE_BLOCK, |
||||
|
Context.STATEMENT)): |
||||
|
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() |
||||
|
|
||||
|
type = token.type |
||||
|
|
||||
|
# Process the line change. |
||||
|
if not self._is_html and FLAGS.strict: |
||||
|
# 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 type == Type.PARAMETERS: |
||||
|
# Find missing spaces in parameter lists. |
||||
|
if self.MISSING_PARAMETER_SPACE.search(token.string): |
||||
|
self._HandleError(errors.MISSING_SPACE, 'Missing space after ","', |
||||
|
token) |
||||
|
|
||||
|
# 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(0, space_count)) |
||||
|
|
||||
|
elif (type == Type.START_BLOCK and |
||||
|
token.metadata.context.type == Context.BLOCK): |
||||
|
self._CheckForMissingSpaceBeforeToken(token) |
||||
|
|
||||
|
elif type == Type.END_BLOCK: |
||||
|
# This check is for object literal end block tokens, but there is no need |
||||
|
# to test that condition since a comma at the end of any other kind of |
||||
|
# block is undoubtedly a parse error. |
||||
|
last_code = token.metadata.last_code |
||||
|
if last_code.IsOperator(','): |
||||
|
self._HandleError(errors.COMMA_AT_END_OF_LITERAL, |
||||
|
'Illegal comma at end of object literal', last_code, |
||||
|
Position.All(last_code.string)) |
||||
|
|
||||
|
if state.InFunction() and state.IsFunctionClose(): |
||||
|
is_immediately_called = (token.next and |
||||
|
token.next.type == Type.START_PAREN) |
||||
|
if state.InTopLevelFunction(): |
||||
|
# When the function was top-level and not immediately called, check |
||||
|
# that it's terminated by a semi-colon. |
||||
|
if state.InAssignedFunction(): |
||||
|
if not is_immediately_called and (last_in_line or |
||||
|
not token.next.type == Type.SEMICOLON): |
||||
|
self._HandleError(errors.MISSING_SEMICOLON_AFTER_FUNCTION, |
||||
|
'Missing semicolon after function assigned to a variable', |
||||
|
token, Position.AtEnd(token.string)) |
||||
|
else: |
||||
|
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.All(token.next.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): |
||||
|
self._HandleError(errors.REDUNDANT_SEMICOLON, |
||||
|
'No semicolon is required to end a code block', |
||||
|
token.next, Position.All(token.next.string)) |
||||
|
|
||||
|
elif type == Type.SEMICOLON: |
||||
|
if token.previous and token.previous.type == Type.WHITESPACE: |
||||
|
self._HandleError(errors.EXTRA_SPACE, 'Extra space before ";"', |
||||
|
token.previous, 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.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.All(token.string)) |
||||
|
|
||||
|
elif type == Type.START_PAREN: |
||||
|
if token.previous and token.previous.type == Type.KEYWORD: |
||||
|
self._HandleError(errors.MISSING_SPACE, 'Missing space before "("', |
||||
|
token, Position.AtBeginning()) |
||||
|
elif token.previous and token.previous.type == Type.WHITESPACE: |
||||
|
before_space = token.previous.previous |
||||
|
if (before_space and before_space.line_number == token.line_number and |
||||
|
before_space.type == Type.IDENTIFIER): |
||||
|
self._HandleError(errors.EXTRA_SPACE, 'Extra space before "("', |
||||
|
token.previous, Position.All(token.previous.string)) |
||||
|
|
||||
|
elif type == Type.START_BRACKET: |
||||
|
if (not first_in_line 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.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 first_in_line and token.previous and |
||||
|
not token.previous.type in ( |
||||
|
[Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] + |
||||
|
Type.EXPRESSION_ENDER_TYPES)): |
||||
|
self._HandleError(errors.MISSING_SPACE, 'Missing space before "["', |
||||
|
token, Position.AtBeginning()) |
||||
|
|
||||
|
elif 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.All(token.previous.string)) |
||||
|
|
||||
|
if token.type == Type.END_BRACKET: |
||||
|
last_code = token.metadata.last_code |
||||
|
if last_code.IsOperator(','): |
||||
|
self._HandleError(errors.COMMA_AT_END_OF_LITERAL, |
||||
|
'Illegal comma at end of array literal', last_code, |
||||
|
Position.All(last_code.string)) |
||||
|
|
||||
|
elif type == Type.WHITESPACE: |
||||
|
if self.ILLEGAL_TAB.search(token.string): |
||||
|
if token.IsFirstInLine(): |
||||
|
self._HandleError(errors.ILLEGAL_TAB, |
||||
|
'Illegal tab in whitespace before "%s"' % token.next.string, |
||||
|
token, Position.All(token.string)) |
||||
|
else: |
||||
|
self._HandleError(errors.ILLEGAL_TAB, |
||||
|
'Illegal tab in whitespace after "%s"' % token.previous.string, |
||||
|
token, 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.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(1, len(token.string) - 1)) |
||||
|
|
||||
|
elif type == Type.OPERATOR: |
||||
|
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)): |
||||
|
self._HandleError(errors.EXTRA_SPACE, |
||||
|
'Extra space before "%s"' % token.string, token.previous, |
||||
|
Position.All(token.previous.string)) |
||||
|
|
||||
|
elif (token.previous and |
||||
|
not token.previous.IsComment() and |
||||
|
token.previous.type in Type.EXPRESSION_ENDER_TYPES): |
||||
|
self._HandleError(errors.MISSING_SPACE, |
||||
|
'Missing space before "%s"' % token.string, token, |
||||
|
Position.AtBeginning()) |
||||
|
|
||||
|
# Check that binary operators are not used to start lines. |
||||
|
if ((not last_code or last_code.line_number != token.line_number) and |
||||
|
not token.metadata.IsUnaryOperator()): |
||||
|
self._HandleError(errors.LINE_STARTS_WITH_OPERATOR, |
||||
|
'Binary operator should go on previous line "%s"' % token.string, |
||||
|
token) |
||||
|
|
||||
|
elif 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) |
||||
|
elif flag.type not in state.GetDocFlag().SUPPRESS_TYPES: |
||||
|
self._HandleError(errors.INVALID_SUPPRESS_TYPE, |
||||
|
'Invalid suppression type: %s' % flag.type, |
||||
|
token) |
||||
|
|
||||
|
elif FLAGS.strict 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(result.start(2), 0)) |
||||
|
elif num_spaces > 1: |
||||
|
self._HandleError(errors.EXTRA_SPACE, |
||||
|
'Extra space after email address', |
||||
|
token.next, |
||||
|
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(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'] |
||||
|
self._HandleError(errors.MISSING_JSDOC_TAG_DESCRIPTION, |
||||
|
'Missing description in %s tag' % flag_name, token) |
||||
|
else: |
||||
|
self._CheckForMissingSpaceBeforeToken(flag.description_start_token) |
||||
|
|
||||
|
# We want punctuation to be inside of any tags ending a description, |
||||
|
# so strip tags before checking description. See bug 1127192. Note |
||||
|
# that depending on how lines break, the real description end token |
||||
|
# may consist only of stripped html and the effective end token can |
||||
|
# be different. |
||||
|
end_token = flag.description_end_token |
||||
|
end_string = htmlutil.StripTags(end_token.string).strip() |
||||
|
while (end_string == '' and not |
||||
|
end_token.type in Type.FLAG_ENDING_TYPES): |
||||
|
end_token = end_token.previous |
||||
|
if end_token.type in Type.FLAG_DESCRIPTION_TYPES: |
||||
|
end_string = htmlutil.StripTags(end_token.string).rstrip() |
||||
|
|
||||
|
if not (end_string.endswith('.') or end_string.endswith('?') or |
||||
|
end_string.endswith('!')): |
||||
|
# Find the position for the missing punctuation, inside of any html |
||||
|
# tags. |
||||
|
desc_str = end_token.string.rstrip() |
||||
|
while desc_str.endswith('>'): |
||||
|
start_tag_index = desc_str.rfind('<') |
||||
|
if start_tag_index < 0: |
||||
|
break |
||||
|
desc_str = desc_str[:start_tag_index].rstrip() |
||||
|
end_position = Position(len(desc_str), 0) |
||||
|
|
||||
|
self._HandleError( |
||||
|
errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER, |
||||
|
('%s descriptions must end with valid punctuation such as a ' |
||||
|
'period.' % token.string), |
||||
|
end_token, end_position) |
||||
|
|
||||
|
if flag.flag_type in state.GetDocFlag().HAS_TYPE: |
||||
|
if flag.type_start_token is not None: |
||||
|
self._CheckForMissingSpaceBeforeToken( |
||||
|
token.attached_object.type_start_token) |
||||
|
|
||||
|
if flag.type and flag.type != '' and not flag.type.isspace(): |
||||
|
self._CheckJsDocType(token) |
||||
|
|
||||
|
if 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 (FLAGS.strict and token.values['name'] == 'inheritDoc' and |
||||
|
type == Type.DOC_INLINE_FLAG): |
||||
|
self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC, |
||||
|
'Unnecessary braces around @inheritDoc', |
||||
|
token) |
||||
|
|
||||
|
elif type == Type.SIMPLE_LVALUE: |
||||
|
identifier = token.values['identifier'] |
||||
|
|
||||
|
if ((not state.InFunction() or state.InConstructor()) and |
||||
|
not state.InParentheses() 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('__'): |
||||
|
if jsdoc.HasFlag('override'): |
||||
|
self._HandleError(errors.INVALID_OVERRIDE_PRIVATE, |
||||
|
'%s should not override a private member.' % identifier, |
||||
|
jsdoc.GetFlag('override').flag_token) |
||||
|
# Can have a private class which inherits documentation from a |
||||
|
# public superclass. |
||||
|
if jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor'): |
||||
|
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 |
||||
|
not ('underscore' 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'): |
||||
|
self._HandleError(errors.EXTRA_PRIVATE, |
||||
|
'Member "%s" must not have @private JsDoc' % |
||||
|
identifier, token) |
||||
|
|
||||
|
if ((jsdoc.HasFlag('desc') or jsdoc.HasFlag('hidden')) |
||||
|
and not identifier.startswith('MSG_') |
||||
|
and identifier.find('.MSG_') == -1): |
||||
|
# TODO(user): Update error message to show the actual invalid |
||||
|
# tag, either @desc or @hidden. |
||||
|
self._HandleError(errors.INVALID_USE_OF_DESC_TAG, |
||||
|
'Member "%s" should not have @desc JsDoc' % identifier, |
||||
|
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 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. |
||||
|
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 |
||||
|
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 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(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 Finalize(self, state, tokenizer_mode): |
||||
|
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) |
||||
|
|
||||
|
# Check that the mode is not mid comment, argument list, etc. |
||||
|
if not tokenizer_mode == Modes.TEXT_MODE: |
||||
|
self._HandleError( |
||||
|
errors.FILE_IN_BLOCK, |
||||
|
'File ended in mode "%s".' % tokenizer_mode, |
||||
|
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.""" |
||||
|
return [] |
@ -0,0 +1,521 @@ |
|||||
|
#!/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, type, start_token, parent): |
||||
|
"""Initializes the context object. |
||||
|
|
||||
|
Args: |
||||
|
type: The context type. |
||||
|
start_token: The token where this context starts. |
||||
|
parent: The parent context. |
||||
|
""" |
||||
|
self.type = type |
||||
|
self.start_token = start_token |
||||
|
self.end_token = None |
||||
|
self.parent = parent |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
|
||||
|
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. |
||||
|
""" |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
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;') |
||||
|
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, type): |
||||
|
"""Overridable by subclasses to create the appropriate context type.""" |
||||
|
return EcmaContext(type, self._token, self._context) |
||||
|
|
||||
|
def _CreateMetaData(self): |
||||
|
"""Overridable by subclasses to create the appropriate metadata type.""" |
||||
|
return EcmaMetaData() |
||||
|
|
||||
|
def _AddContext(self, type): |
||||
|
"""Adds a context of the given type to the context stack. |
||||
|
|
||||
|
Args: |
||||
|
type: The type of context to create |
||||
|
""" |
||||
|
self._context = self._CreateContext(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')): |
||||
|
# Pop up to but not including the switch block. |
||||
|
while self._context.parent.type != EcmaContext.SWITCH: |
||||
|
self._PopContext() |
||||
|
|
||||
|
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_identifier = (token.type == TokenType.IDENTIFIER and |
||||
|
token.string.endswith('.')) |
||||
|
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 |
||||
|
next_code_is_dot = next_code and next_code.string == '.' |
||||
|
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 |
||||
|
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_identifier and |
||||
|
not is_continued_operator and |
||||
|
not is_continued_dot and |
||||
|
not next_code_is_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 whether 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 |
@ -0,0 +1,336 @@ |
|||||
|
#!/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.""" |
||||
|
|
||||
|
__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 tokenutil |
||||
|
from closure_linter.common import errorhandler |
||||
|
|
||||
|
# Shorthand |
||||
|
Token = javascripttokens.JavaScriptToken |
||||
|
Type = javascripttokens.JavaScriptTokenType |
||||
|
|
||||
|
END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$') |
||||
|
|
||||
|
FLAGS = flags.FLAGS |
||||
|
flags.DEFINE_boolean('disable_indentation_fixing', False, |
||||
|
'Whether to disable automatic fixing of indentation.') |
||||
|
|
||||
|
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. |
||||
|
""" |
||||
|
self._file_name = None |
||||
|
self._file_token = None |
||||
|
self._external_file = external_file |
||||
|
|
||||
|
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_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 HandleError(self, error): |
||||
|
"""Attempts to fix the error. |
||||
|
|
||||
|
Args: |
||||
|
error: The error object |
||||
|
""" |
||||
|
code = error.code |
||||
|
token = error.token |
||||
|
|
||||
|
if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL: |
||||
|
iterator = token.attached_object.type_start_token |
||||
|
if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace(): |
||||
|
iterator = iterator.next |
||||
|
|
||||
|
leading_space = len(iterator.string) - len(iterator.string.lstrip()) |
||||
|
iterator.string = '%s?%s' % (' ' * leading_space, |
||||
|
iterator.string.lstrip()) |
||||
|
|
||||
|
# Cover the no outer brace case where the end token is part of the type. |
||||
|
while iterator and iterator != token.attached_object.type_end_token.next: |
||||
|
iterator.string = iterator.string.replace( |
||||
|
'null|', '').replace('|null', '') |
||||
|
iterator = iterator.next |
||||
|
|
||||
|
# 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): |
||||
|
tokenutil.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.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.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER: |
||||
|
token.string = error.position.Set(token.string, '.') |
||||
|
self._AddFix(token) |
||||
|
|
||||
|
elif code == errors.MISSING_LINE: |
||||
|
if error.position.IsAtBeginning(): |
||||
|
tokenutil.InsertLineAfter(token.previous) |
||||
|
else: |
||||
|
tokenutil.InsertLineAfter(token) |
||||
|
self._AddFix(token) |
||||
|
|
||||
|
elif code == errors.EXTRA_LINE: |
||||
|
tokenutil.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 = num_lines * -1 |
||||
|
should_delete = True |
||||
|
|
||||
|
for i in xrange(1, num_lines + 1): |
||||
|
if should_delete: |
||||
|
# TODO(user): DeleteToken should update line numbers. |
||||
|
tokenutil.DeleteToken(token.previous) |
||||
|
else: |
||||
|
tokenutil.InsertLineAfter(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) |
||||
|
tokenutil.DeleteToken(token) |
||||
|
tokenutil.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 len(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 in (errors.GOOG_REQUIRES_NOT_ALPHABETIZED, |
||||
|
errors.GOOG_PROVIDES_NOT_ALPHABETIZED): |
||||
|
tokens = error.fix_data |
||||
|
strings = map(lambda x: x.string, tokens) |
||||
|
sorted_strings = sorted(strings) |
||||
|
|
||||
|
index = 0 |
||||
|
changed_tokens = [] |
||||
|
for token in tokens: |
||||
|
if token.string != sorted_strings[index]: |
||||
|
token.string = sorted_strings[index] |
||||
|
changed_tokens.append(token) |
||||
|
index += 1 |
||||
|
|
||||
|
self._AddFix(changed_tokens) |
||||
|
|
||||
|
elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC: |
||||
|
if token.previous.string == '{' and token.next.string == '}': |
||||
|
tokenutil.DeleteToken(token.previous) |
||||
|
tokenutil.DeleteToken(token.next) |
||||
|
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 |
||||
|
|
||||
|
if token.type in (Type.WHITESPACE, Type.PARAMETERS): |
||||
|
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 == errors.EXTRA_GOOG_REQUIRE: |
||||
|
fixed_tokens = [] |
||||
|
while token: |
||||
|
if token.type == Type.IDENTIFIER: |
||||
|
if token.string not in ['goog.require', 'goog.provide']: |
||||
|
# Stop iterating over tokens once we're out of the requires and |
||||
|
# provides. |
||||
|
break |
||||
|
if token.string == 'goog.require': |
||||
|
# Text of form: goog.require('required'), skipping past open paren |
||||
|
# and open quote to the string text. |
||||
|
required = token.next.next.next.string |
||||
|
if required in error.fix_data: |
||||
|
fixed_tokens.append(token) |
||||
|
# Want to delete: goog.require + open paren + open single-quote + |
||||
|
# text + close single-quote + close paren + semi-colon = 7. |
||||
|
tokenutil.DeleteTokens(token, 7) |
||||
|
token = token.next |
||||
|
|
||||
|
self._AddFix(fixed_tokens) |
||||
|
|
||||
|
def FinishFile(self): |
||||
|
"""Called when the current file has finished style checking. |
||||
|
|
||||
|
Used to go back and fix any errors in the file. |
||||
|
""" |
||||
|
if self._file_fix_count: |
||||
|
f = self._external_file |
||||
|
if not f: |
||||
|
print "Fixed %d errors in %s" % (self._file_fix_count, self._file_name) |
||||
|
f = open(self._file_name, 'w') |
||||
|
|
||||
|
token = self._file_token |
||||
|
char_count = 0 |
||||
|
while token: |
||||
|
f.write(token.string) |
||||
|
char_count += len(token.string) |
||||
|
|
||||
|
if token.IsLastInLine(): |
||||
|
f.write('\n') |
||||
|
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 |
||||
|
self._file_changed_lines |
||||
|
|
||||
|
token = token.next |
||||
|
|
||||
|
if not self._external_file: |
||||
|
# Close the file if we created it |
||||
|
f.close() |
@ -0,0 +1,42 @@ |
|||||
|
#!/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.') |
||||
|
|
||||
|
|
||||
|
def ShouldReportError(error): |
||||
|
"""Whether the given error should be reported. |
||||
|
|
||||
|
Returns: |
||||
|
True for all errors except missing documentation errors. For these, |
||||
|
it returns the value of the jsdoc flag. |
||||
|
""" |
||||
|
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) |
@ -0,0 +1,131 @@ |
|||||
|
#!/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 |
||||
|
MULTI_LINE_STRING = 130 |
||||
|
UNNECESSARY_DOUBLE_QUOTED_STRING = 131 |
||||
|
|
||||
|
# Requires, provides |
||||
|
GOOG_REQUIRES_NOT_ALPHABETIZED = 140 |
||||
|
GOOG_PROVIDES_NOT_ALPHABETIZED = 141 |
||||
|
MISSING_GOOG_REQUIRE = 142 |
||||
|
MISSING_GOOG_PROVIDE = 143 |
||||
|
EXTRA_GOOG_REQUIRE = 144 |
||||
|
|
||||
|
# 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_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER = 240 |
||||
|
# 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 |
||||
|
|
||||
|
# 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.2.5: |
||||
|
WRONG_BLANK_LINE_COUNT, |
||||
|
EXTRA_GOOG_REQUIRE, |
||||
|
]) |
@ -0,0 +1,47 @@ |
|||||
|
#!/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 sys |
||||
|
|
||||
|
import gflags as flags |
||||
|
from closure_linter import checker |
||||
|
from closure_linter import error_fixer |
||||
|
from closure_linter.common import simplefileflags as fileflags |
||||
|
|
||||
|
|
||||
|
def main(argv = None): |
||||
|
"""Main function. |
||||
|
|
||||
|
Args: |
||||
|
argv: Sequence of command line arguments. |
||||
|
""" |
||||
|
if argv is None: |
||||
|
argv = flags.FLAGS(sys.argv) |
||||
|
|
||||
|
files = fileflags.GetFileList(argv, 'JavaScript', ['.js']) |
||||
|
|
||||
|
style_checker = checker.JavaScriptStyleChecker(error_fixer.ErrorFixer()) |
||||
|
|
||||
|
# Check the list of files. |
||||
|
for filename in files: |
||||
|
style_checker.Check(filename) |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
main() |
@ -0,0 +1,61 @@ |
|||||
|
#!/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 checker |
||||
|
from closure_linter import error_fixer |
||||
|
|
||||
|
_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 testFixJsStyle(self): |
||||
|
input_filename = None |
||||
|
try: |
||||
|
input_filename = '%s/fixjsstyle.in.js' % (_RESOURCE_PREFIX) |
||||
|
|
||||
|
golden_filename = '%s/fixjsstyle.out.js' % (_RESOURCE_PREFIX) |
||||
|
except IOError, ex: |
||||
|
raise IOError('Could not find testdata resource for %s: %s' % |
||||
|
(self._filename, ex)) |
||||
|
|
||||
|
# Autofix the file, sending output to a fake file. |
||||
|
actual = StringIO.StringIO() |
||||
|
style_checker = checker.JavaScriptStyleChecker( |
||||
|
error_fixer.ErrorFixer(actual)) |
||||
|
style_checker.Check(input_filename) |
||||
|
|
||||
|
# Now compare the files. |
||||
|
actual.seek(0) |
||||
|
expected = open(golden_filename, 'r') |
||||
|
|
||||
|
self.assertEqual(actual.readlines(), expected.readlines()) |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
googletest.main() |
@ -0,0 +1,99 @@ |
|||||
|
#!/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 re |
||||
|
import os |
||||
|
import sys |
||||
|
import unittest |
||||
|
|
||||
|
import gflags as flags |
||||
|
import unittest as googletest |
||||
|
|
||||
|
from closure_linter import checker |
||||
|
from closure_linter import errors |
||||
|
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') |
||||
|
|
||||
|
# List of files under testdata to test. |
||||
|
# We need to list files explicitly since pyglib can't list directories. |
||||
|
_TEST_FILES = [ |
||||
|
'all_js_wrapped.js', |
||||
|
'blank_lines.js', |
||||
|
'ends_with_block.js', |
||||
|
'externs.js', |
||||
|
'html_parse_error.html', |
||||
|
'indentation.js', |
||||
|
'interface.js', |
||||
|
'jsdoc.js', |
||||
|
'minimal.js', |
||||
|
'other.js', |
||||
|
'require_all_caps.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_lower_case.js', |
||||
|
'require_numeric.js', |
||||
|
'require_provide_ok.js', |
||||
|
'require_provide_missing.js', |
||||
|
'simple.html', |
||||
|
'spaces.js', |
||||
|
'tokenizer.js', |
||||
|
'unparseable.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, |
||||
|
checker.GJsLintRunner(), 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') |
@ -0,0 +1,142 @@ |
|||||
|
#!/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)') |
||||
|
|
||||
|
import sys |
||||
|
import time |
||||
|
|
||||
|
from closure_linter import checker |
||||
|
from closure_linter import errors |
||||
|
from closure_linter.common import errorprinter |
||||
|
from closure_linter.common import simplefileflags as fileflags |
||||
|
import gflags as flags |
||||
|
|
||||
|
|
||||
|
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('check_html', False, |
||||
|
'Whether to check javascript in html files.') |
||||
|
flags.DEFINE_boolean('summary', False, |
||||
|
'Whether to show an error count summary.') |
||||
|
|
||||
|
GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time', |
||||
|
'--check_html', '--summary'] |
||||
|
|
||||
|
|
||||
|
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.check_html: |
||||
|
suffixes += ['.html', '.htm'] |
||||
|
files = fileflags.GetFileList(argv, 'JavaScript', suffixes) |
||||
|
|
||||
|
error_handler = None |
||||
|
if FLAGS.unix_mode: |
||||
|
error_handler = errorprinter.ErrorPrinter(errors.NEW_ERRORS) |
||||
|
error_handler.SetFormat(errorprinter.UNIX_FORMAT) |
||||
|
|
||||
|
runner = checker.GJsLintRunner() |
||||
|
result = runner.Run(files, error_handler) |
||||
|
result.PrintSummary() |
||||
|
|
||||
|
exit_code = 0 |
||||
|
if result.HasOldErrors(): |
||||
|
exit_code += 1 |
||||
|
if result.HasNewErrors(): |
||||
|
exit_code += 2 |
||||
|
|
||||
|
if exit_code: |
||||
|
if FLAGS.summary: |
||||
|
result.PrintFileSummary() |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
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() |
@ -0,0 +1,543 @@ |
|||||
|
#!/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)') |
||||
|
|
||||
|
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 |
||||
|
|
||||
|
import gflags as flags |
||||
|
|
||||
|
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 not token.type 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: |
||||
|
self._PopTo(Type.START_BLOCK) |
||||
|
|
||||
|
elif token_type == Type.KEYWORD and token.string in ('case', 'default'): |
||||
|
self._Add(self._PopTo(Type.START_BLOCK)) |
||||
|
|
||||
|
elif is_first and token.string == '.': |
||||
|
# This token should have been on the previous line, so treat it as if it |
||||
|
# was there. |
||||
|
info = TokenInfo(token) |
||||
|
info.line_number = token.line_number - 1 |
||||
|
self._Add(info) |
||||
|
|
||||
|
elif token_type == Type.SEMICOLON: |
||||
|
self._PopTransient() |
||||
|
|
||||
|
not_binary_operator = (token_type != Type.OPERATOR or |
||||
|
token.metadata.IsUnaryOperator()) |
||||
|
not_dot = token.string != '.' |
||||
|
if is_first and not_binary_operator and not_dot 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]), 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: |
||||
|
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)) |
||||
|
pass |
||||
|
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() |
||||
|
|
||||
|
elif (token.string.endswith('.') |
||||
|
and token_type in (Type.IDENTIFIER, Type.NORMAL)): |
||||
|
self._Add(TokenInfo(token)) |
||||
|
elif token_type == Type.PARAMETERS and token.string.endswith(','): |
||||
|
# Parameter lists. |
||||
|
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. |
||||
|
|
||||
|
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 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 _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: |
||||
|
index = 1 |
||||
|
while index <= len(self._stack): |
||||
|
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); |
||||
|
close_block = token_info.token.metadata.context.end_token |
||||
|
stack_info.is_permanent_override = \ |
||||
|
close_block.line_number != token_info.token.line_number |
||||
|
elif (token_info.token.type == Type.START_BLOCK and |
||||
|
token_info.token.metadata.context.type == Context.BLOCK and |
||||
|
(stack_token.IsAssignment() or |
||||
|
stack_token.type == Type.IDENTIFIER)): |
||||
|
# When starting a function block, the override can transcend lines. |
||||
|
# For example |
||||
|
# long.long.name = function( |
||||
|
# a) { |
||||
|
# In this case the { and the = are on different lines. But the |
||||
|
# override should still apply. |
||||
|
stack_info.overridden_by = token_info |
||||
|
stack_info.is_permanent_override = True |
||||
|
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() |
@ -0,0 +1,395 @@ |
|||||
|
#!/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. |
||||
|
|
||||
|
"""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 gflags as flags |
||||
|
from closure_linter import ecmalintrules |
||||
|
from closure_linter import errors |
||||
|
from closure_linter import javascripttokenizer |
||||
|
from closure_linter import javascripttokens |
||||
|
from closure_linter import tokenutil |
||||
|
from closure_linter.common import error |
||||
|
from closure_linter.common import position |
||||
|
|
||||
|
FLAGS = flags.FLAGS |
||||
|
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.') |
||||
|
|
||||
|
# Shorthand |
||||
|
Error = error.Error |
||||
|
Position = position.Position |
||||
|
Type = javascripttokens.JavaScriptTokenType |
||||
|
|
||||
|
|
||||
|
class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules): |
||||
|
"""JavaScript lint rules that catch JavaScript specific style errors.""" |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
def __ContainsRecordType(self, token): |
||||
|
"""Check whether the given token contains a record type. |
||||
|
|
||||
|
Args: |
||||
|
token: The token being checked |
||||
|
""" |
||||
|
# If we see more than one left-brace in the string of an annotation token, |
||||
|
# then there's a record type in there. |
||||
|
return (token and token.type == Type.DOC_FLAG and |
||||
|
token.attached_object.type is not None and |
||||
|
token.attached_object.type.find('{') != token.string.rfind('{')) |
||||
|
|
||||
|
|
||||
|
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 |
||||
|
""" |
||||
|
if self.__ContainsRecordType(token): |
||||
|
# We should bail out and not emit any warnings for this annotation. |
||||
|
# TODO(nicksantos): Support record types for real. |
||||
|
state.GetDocComment().Invalidate() |
||||
|
return |
||||
|
|
||||
|
# Call the base class's CheckToken function. |
||||
|
super(JavaScriptLintRules, self).CheckToken(token, state) |
||||
|
|
||||
|
# Store some convenience variables |
||||
|
first_in_line = token.IsFirstInLine() |
||||
|
last_in_line = token.IsLastInLine() |
||||
|
type = token.type |
||||
|
|
||||
|
if 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.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 |
||||
|
# without types. |
||||
|
if (flag.flag_type not in ('suppress', 'enum') and |
||||
|
(flag.type == None or flag.type == '' or flag.type.isspace())): |
||||
|
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 type == Type.DOUBLE_QUOTE_STRING_START: |
||||
|
next = token.next |
||||
|
while next.type == Type.STRING_TEXT: |
||||
|
if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search( |
||||
|
next.string): |
||||
|
break |
||||
|
next = next.next |
||||
|
else: |
||||
|
self._HandleError( |
||||
|
errors.UNNECESSARY_DOUBLE_QUOTED_STRING, |
||||
|
'Single-quoted string preferred over double-quoted string.', |
||||
|
token, |
||||
|
Position.All(token.string)) |
||||
|
|
||||
|
elif type == Type.END_DOC_COMMENT: |
||||
|
if (FLAGS.strict and not self._is_html and state.InTopLevel() and |
||||
|
not state.InBlock()): |
||||
|
|
||||
|
# Check if we're in a fileoverview or constructor JsDoc. |
||||
|
doc_comment = state.GetDocComment() |
||||
|
is_constructor = (doc_comment.HasFlag('constructor') or |
||||
|
doc_comment.HasFlag('interface')) |
||||
|
is_file_overview = doc_comment.HasFlag('fileoverview') |
||||
|
|
||||
|
# 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.next |
||||
|
if (not next or |
||||
|
(not is_file_overview and next.type in Type.NON_CODE_TYPES)): |
||||
|
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_overview: |
||||
|
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 |
||||
|
|
||||
|
if is_file_overview and blank_lines == 0: |
||||
|
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_overview 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.AtBeginning(), |
||||
|
expected_blank_lines - blank_lines) |
||||
|
|
||||
|
elif 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.AtBeginning()) |
||||
|
elif (not function.has_return and function.doc and |
||||
|
function.doc.HasFlag('return') and |
||||
|
not state.InInterfaceMethod()): |
||||
|
return_flag = function.doc.GetFlag('return') |
||||
|
if (return_flag.type is None or ( |
||||
|
'undefined' not in return_flag.type and |
||||
|
'void' not in return_flag.type and |
||||
|
'*' not in return_flag.type)): |
||||
|
self._HandleError( |
||||
|
errors.UNNECESSARY_RETURN_DOCUMENTATION, |
||||
|
'Found @return JsDoc on function that returns nothing', |
||||
|
return_flag.flag_token, Position.AtBeginning()) |
||||
|
|
||||
|
if state.InFunction() and state.IsFunctionClose(): |
||||
|
is_immediately_called = (token.next and |
||||
|
token.next.type == Type.START_PAREN) |
||||
|
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): |
||||
|
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.AtBeginning()) |
||||
|
|
||||
|
elif 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.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 type == Type.OPERATOR: |
||||
|
# 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 token.next.type 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.AtEnd(token.string)) |
||||
|
elif type == Type.WHITESPACE: |
||||
|
# 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.All(token.string)) |
||||
|
|
||||
|
def Finalize(self, state, tokenizer_mode): |
||||
|
"""Perform all checks that need to occur after all lines are processed.""" |
||||
|
# Call the base class's Finalize function. |
||||
|
super(JavaScriptLintRules, self).Finalize(state, tokenizer_mode) |
||||
|
|
||||
|
# Check for sorted requires statements. |
||||
|
goog_require_tokens = state.GetGoogRequireTokens() |
||||
|
requires = [require_token.string for require_token in goog_require_tokens] |
||||
|
sorted_requires = sorted(requires) |
||||
|
index = 0 |
||||
|
bad = False |
||||
|
for item in requires: |
||||
|
if item != sorted_requires[index]: |
||||
|
bad = True |
||||
|
break |
||||
|
index += 1 |
||||
|
|
||||
|
if bad: |
||||
|
self._HandleError( |
||||
|
errors.GOOG_REQUIRES_NOT_ALPHABETIZED, |
||||
|
'goog.require classes must be alphabetized. The correct code is:\n' + |
||||
|
'\n'.join(map(lambda x: 'goog.require(\'%s\');' % x, |
||||
|
sorted_requires)), |
||||
|
goog_require_tokens[index], |
||||
|
position=Position.AtBeginning(), |
||||
|
fix_data=goog_require_tokens) |
||||
|
|
||||
|
# Check for sorted provides statements. |
||||
|
goog_provide_tokens = state.GetGoogProvideTokens() |
||||
|
provides = [provide_token.string for provide_token in goog_provide_tokens] |
||||
|
sorted_provides = sorted(provides) |
||||
|
index = 0 |
||||
|
bad = False |
||||
|
for item in provides: |
||||
|
if item != sorted_provides[index]: |
||||
|
bad = True |
||||
|
break |
||||
|
index += 1 |
||||
|
|
||||
|
if bad: |
||||
|
self._HandleError( |
||||
|
errors.GOOG_PROVIDES_NOT_ALPHABETIZED, |
||||
|
'goog.provide classes must be alphabetized. The correct code is:\n' + |
||||
|
'\n'.join(map(lambda x: 'goog.provide(\'%s\');' % x, |
||||
|
sorted_provides)), |
||||
|
goog_provide_tokens[index], |
||||
|
position=Position.AtBeginning(), |
||||
|
fix_data=goog_provide_tokens) |
||||
|
|
||||
|
if FLAGS.closurized_namespaces: |
||||
|
# Check that we provide everything we need. |
||||
|
provided_namespaces = state.GetProvidedNamespaces() |
||||
|
missing_provides = provided_namespaces - set(provides) |
||||
|
if missing_provides: |
||||
|
self._HandleError( |
||||
|
errors.MISSING_GOOG_PROVIDE, |
||||
|
'Missing the following goog.provide statements:\n' + |
||||
|
'\n'.join(map(lambda x: 'goog.provide(\'%s\');' % x, |
||||
|
sorted(missing_provides))), |
||||
|
state.GetFirstToken(), position=Position.AtBeginning(), |
||||
|
fix_data=missing_provides) |
||||
|
|
||||
|
# Compose a set of all available namespaces. Explicitly omit goog |
||||
|
# because if you can call goog.require, you already have goog. |
||||
|
available_namespaces = (set(requires) | set(provides) | set(['goog']) | |
||||
|
provided_namespaces) |
||||
|
|
||||
|
# Check that we require everything we need. |
||||
|
missing_requires = set() |
||||
|
for namespace_variants in state.GetUsedNamespaces(): |
||||
|
# Namespace variants is a list of potential things to require. If we |
||||
|
# find we're missing one, we are lazy and choose to require the first |
||||
|
# in the sequence - which should be the namespace. |
||||
|
if not set(namespace_variants) & available_namespaces: |
||||
|
missing_requires.add(namespace_variants[0]) |
||||
|
|
||||
|
if missing_requires: |
||||
|
self._HandleError( |
||||
|
errors.MISSING_GOOG_REQUIRE, |
||||
|
'Missing the following goog.require statements:\n' + |
||||
|
'\n'.join(map(lambda x: 'goog.require(\'%s\');' % x, |
||||
|
sorted(missing_requires))), |
||||
|
state.GetFirstToken(), position=Position.AtBeginning(), |
||||
|
fix_data=missing_requires) |
||||
|
|
||||
|
# Check that we don't require things we don't actually use. |
||||
|
namespace_variants = state.GetUsedNamespaces() |
||||
|
used_namespaces = set() |
||||
|
for a, b in namespace_variants: |
||||
|
used_namespaces.add(a) |
||||
|
used_namespaces.add(b) |
||||
|
|
||||
|
extra_requires = set() |
||||
|
for i in requires: |
||||
|
baseNamespace = i.split('.')[0] |
||||
|
if (i not in used_namespaces and |
||||
|
baseNamespace in FLAGS.closurized_namespaces and |
||||
|
i not in FLAGS.ignored_extra_namespaces): |
||||
|
extra_requires.add(i) |
||||
|
|
||||
|
if extra_requires: |
||||
|
self._HandleError( |
||||
|
errors.EXTRA_GOOG_REQUIRE, |
||||
|
'The following goog.require statements appear unnecessary:\n' + |
||||
|
'\n'.join(map(lambda x: 'goog.require(\'%s\');' % x, |
||||
|
sorted(extra_requires))), |
||||
|
state.GetFirstToken(), position=Position.AtBeginning(), |
||||
|
fix_data=extra_requires) |
||||
|
|
@ -0,0 +1,238 @@ |
|||||
|
#!/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 JavaScript type spec. |
||||
|
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', |
||||
|
'protected', 'notypecheck', 'throws']) |
||||
|
|
||||
|
LEGAL_DOC = EXTENDED_DOC | statetracker.DocFlag.LEGAL_DOC |
||||
|
|
||||
|
def __init__(self, flag_token): |
||||
|
"""Creates the JsDocFlag object and attaches it to the given start token. |
||||
|
|
||||
|
Args: |
||||
|
flag_token: The starting token of the flag. |
||||
|
""" |
||||
|
statetracker.DocFlag.__init__(self, flag_token) |
||||
|
|
||||
|
|
||||
|
class JavaScriptStateTracker(statetracker.StateTracker): |
||||
|
"""JavaScript state tracker. |
||||
|
|
||||
|
Inherits from the core EcmaScript StateTracker adding extra state tracking |
||||
|
functionality needed for JavaScript. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, closurized_namespaces=''): |
||||
|
"""Initializes a JavaScript token stream state tracker. |
||||
|
|
||||
|
Args: |
||||
|
closurized_namespaces: An optional list of namespace prefixes used for |
||||
|
testing of goog.provide/require. |
||||
|
""" |
||||
|
statetracker.StateTracker.__init__(self, JsDocFlag) |
||||
|
self.__closurized_namespaces = closurized_namespaces |
||||
|
|
||||
|
def Reset(self): |
||||
|
"""Resets the state tracker to prepare for processing a new page.""" |
||||
|
super(JavaScriptStateTracker, self).Reset() |
||||
|
|
||||
|
self.__goog_require_tokens = [] |
||||
|
self.__goog_provide_tokens = [] |
||||
|
self.__provided_namespaces = set() |
||||
|
self.__used_namespaces = [] |
||||
|
|
||||
|
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 not self.InParentheses() |
||||
|
|
||||
|
def GetGoogRequireTokens(self): |
||||
|
"""Returns list of require tokens.""" |
||||
|
return self.__goog_require_tokens |
||||
|
|
||||
|
def GetGoogProvideTokens(self): |
||||
|
"""Returns list of provide tokens.""" |
||||
|
return self.__goog_provide_tokens |
||||
|
|
||||
|
def GetProvidedNamespaces(self): |
||||
|
"""Returns list of provided namespaces.""" |
||||
|
return self.__provided_namespaces |
||||
|
|
||||
|
def GetUsedNamespaces(self): |
||||
|
"""Returns list of used namespaces, is a list of sequences.""" |
||||
|
return self.__used_namespaces |
||||
|
|
||||
|
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, None, |
||||
|
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 HandleToken(self, token, last_non_space_token): |
||||
|
"""Handles the given token and updates state. |
||||
|
|
||||
|
Args: |
||||
|
token: The token to handle. |
||||
|
last_non_space_token: |
||||
|
""" |
||||
|
super(JavaScriptStateTracker, self).HandleToken(token, |
||||
|
last_non_space_token) |
||||
|
|
||||
|
if token.IsType(Type.IDENTIFIER): |
||||
|
if token.string == 'goog.require': |
||||
|
class_token = tokenutil.Search(token, Type.STRING_TEXT) |
||||
|
self.__goog_require_tokens.append(class_token) |
||||
|
|
||||
|
elif token.string == 'goog.provide': |
||||
|
class_token = tokenutil.Search(token, Type.STRING_TEXT) |
||||
|
self.__goog_provide_tokens.append(class_token) |
||||
|
|
||||
|
elif self.__closurized_namespaces: |
||||
|
self.__AddUsedNamespace(token.string) |
||||
|
if token.IsType(Type.SIMPLE_LVALUE) and not self.InFunction(): |
||||
|
identifier = token.values['identifier'] |
||||
|
|
||||
|
if self.__closurized_namespaces: |
||||
|
namespace = self.GetClosurizedNamespace(identifier) |
||||
|
if namespace and identifier == namespace: |
||||
|
self.__provided_namespaces.add(namespace) |
||||
|
if (self.__closurized_namespaces and |
||||
|
token.IsType(Type.DOC_FLAG) and |
||||
|
token.attached_object.flag_type == 'implements'): |
||||
|
# Interfaces should be goog.require'd. |
||||
|
doc_start = tokenutil.Search(token, Type.DOC_START_BRACE) |
||||
|
interface = tokenutil.Search(doc_start, Type.COMMENT) |
||||
|
self.__AddUsedNamespace(interface.string) |
||||
|
|
||||
|
def __AddUsedNamespace(self, identifier): |
||||
|
"""Adds the namespace of an identifier to the list of used namespaces. |
||||
|
|
||||
|
Args: |
||||
|
identifier: An identifier which has been used. |
||||
|
""" |
||||
|
namespace = self.GetClosurizedNamespace(identifier) |
||||
|
|
||||
|
if namespace: |
||||
|
# We add token.string as a 'namespace' as it is something that could |
||||
|
# potentially be provided to satisfy this dependency. |
||||
|
self.__used_namespaces.append([namespace, identifier]) |
||||
|
|
||||
|
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. |
||||
|
""" |
||||
|
parts = identifier.split('.') |
||||
|
for part in parts: |
||||
|
if part.endswith('_'): |
||||
|
# Ignore private variables / inner classes. |
||||
|
return None |
||||
|
|
||||
|
if identifier.startswith('goog.global'): |
||||
|
# Ignore goog.global, since it is, by definition, global. |
||||
|
return None |
||||
|
|
||||
|
for namespace in self.__closurized_namespaces: |
||||
|
if identifier.startswith(namespace + '.'): |
||||
|
last_part = parts[-1] |
||||
|
if not last_part: |
||||
|
# TODO(robbyw): Handle this: it's a multi-line identifier. |
||||
|
return None |
||||
|
|
||||
|
if last_part in ('apply', 'inherits', 'call'): |
||||
|
# Calling one of Function's methods usually indicates use of a |
||||
|
# superclass. |
||||
|
parts.pop() |
||||
|
last_part = parts[-1] |
||||
|
|
||||
|
for i in xrange(1, len(parts)): |
||||
|
part = parts[i] |
||||
|
if part.isupper(): |
||||
|
# If an identifier is of the form foo.bar.BAZ.x or foo.bar.BAZ, |
||||
|
# the namespace is foo.bar. |
||||
|
return '.'.join(parts[:i]) |
||||
|
if part == 'prototype': |
||||
|
# If an identifier is of the form foo.bar.prototype.x, the |
||||
|
# namespace is foo.bar. |
||||
|
return '.'.join(parts[:i]) |
||||
|
|
||||
|
if last_part.isupper() or not last_part[0].isupper(): |
||||
|
# Strip off the last part of an enum or constant reference. |
||||
|
parts.pop() |
||||
|
|
||||
|
return '.'.join(parts) |
||||
|
|
||||
|
return None |
@ -0,0 +1,53 @@ |
|||||
|
#!/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 JavaScriptStateTracker.""" |
||||
|
|
||||
|
|
||||
|
|
||||
|
import unittest as googletest |
||||
|
from closure_linter import javascriptstatetracker |
||||
|
|
||||
|
class JavaScriptStateTrackerTest(googletest.TestCase): |
||||
|
|
||||
|
__test_cases = { |
||||
|
'package.CONSTANT' : 'package', |
||||
|
'package.methodName' : 'package', |
||||
|
'package.subpackage.methodName' : 'package.subpackage', |
||||
|
'package.ClassName.something' : 'package.ClassName', |
||||
|
'package.ClassName.Enum.VALUE.methodName' : 'package.ClassName.Enum', |
||||
|
'package.ClassName.CONSTANT' : 'package.ClassName', |
||||
|
'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_' : None, |
||||
|
'package.ClassName.prototype.methodName.apply' : 'package.ClassName' |
||||
|
} |
||||
|
|
||||
|
def testGetClosurizedNamespace(self): |
||||
|
stateTracker = javascriptstatetracker.JavaScriptStateTracker(['package']) |
||||
|
for identifier, expected_namespace in self.__test_cases.items(): |
||||
|
actual_namespace = stateTracker.GetClosurizedNamespace(identifier) |
||||
|
self.assertEqual(expected_namespace, actual_namespace, |
||||
|
'expected namespace "' + str(expected_namespace) + |
||||
|
'" for identifier "' + str(identifier) + '" but was "' + |
||||
|
str(actual_namespace) + '"') |
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
googletest.main() |
||||
|
|
@ -0,0 +1,365 @@ |
|||||
|
#!/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 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'] |
||||
|
# 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(KEYWORD_LIST), IDENTIFIER_CHAR)) |
||||
|
|
||||
|
# 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'\bdelete\b', r'\bin\b', r'\binstanceof\b', r'\bnew\b', |
||||
|
r'\btypeof\b', r'\bvoid\b'] |
||||
|
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. |
||||
|
NESTED_IDENTIFIER = r'[a-zA-Z_$][%s.]*' % 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, we need to tokenize whitespace into a |
||||
|
# token. |
||||
|
DOC_FLAG_LEX_SPACES = re.compile(r'(^|(?<=\s))@(?P<name>%s)\b' % |
||||
|
'|'.join(['param'])) |
||||
|
|
||||
|
DOC_INLINE_FLAG = re.compile(r'(?<={)@(?P<name>[a-zA-Z]+)') |
||||
|
|
||||
|
# 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), |
||||
|
Matcher(DOC_FLAG, Type.DOC_FLAG), |
||||
|
|
||||
|
# Tokenize braces so we can find types. |
||||
|
Matcher(START_BLOCK, Type.DOC_START_BRACE), |
||||
|
Matcher(END_BLOCK, Type.DOC_END_BRACE), |
||||
|
Matcher(DOC_PREFIX, Type.DOC_PREFIX, None, True)] |
||||
|
|
||||
|
|
||||
|
# The token matcher groups work as follows: it is an 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. |
||||
|
JAVASCRIPT_MATCHERS = { |
||||
|
# 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(START_DOC_COMMENT, Type.START_DOC_COMMENT, |
||||
|
JavaScriptModes.DOC_COMMENT_MODE), |
||||
|
Matcher(START_BLOCK_COMMENT, Type.START_BLOCK_COMMENT, |
||||
|
JavaScriptModes.BLOCK_COMMENT_MODE), |
||||
|
Matcher(END_OF_LINE_SINGLE_LINE_COMMENT, |
||||
|
Type.START_SINGLE_LINE_COMMENT), |
||||
|
Matcher(START_SINGLE_LINE_COMMENT, Type.START_SINGLE_LINE_COMMENT, |
||||
|
JavaScriptModes.LINE_COMMENT_MODE), |
||||
|
Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_START, |
||||
|
JavaScriptModes.SINGLE_QUOTE_STRING_MODE), |
||||
|
Matcher(DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_START, |
||||
|
JavaScriptModes.DOUBLE_QUOTE_STRING_MODE), |
||||
|
Matcher(REGEX, Type.REGEX), |
||||
|
|
||||
|
# Next we check for start blocks appearing outside any of the items above. |
||||
|
Matcher(START_BLOCK, Type.START_BLOCK), |
||||
|
Matcher(END_BLOCK, Type.END_BLOCK), |
||||
|
|
||||
|
# Then we search for function declarations. |
||||
|
Matcher(FUNCTION_DECLARATION, Type.FUNCTION_DECLARATION, |
||||
|
JavaScriptModes.FUNCTION_MODE), |
||||
|
|
||||
|
# Next, we convert non-function related parens to tokens. |
||||
|
Matcher(OPENING_PAREN, Type.START_PAREN), |
||||
|
Matcher(CLOSING_PAREN, Type.END_PAREN), |
||||
|
|
||||
|
# Next, we convert brackets to tokens. |
||||
|
Matcher(OPENING_BRACKET, Type.START_BRACKET), |
||||
|
Matcher(CLOSING_BRACKET, Type.END_BRACKET), |
||||
|
|
||||
|
# Find numbers. This has to happen before operators because scientific |
||||
|
# notation numbers can have + and - in them. |
||||
|
Matcher(NUMBER, Type.NUMBER), |
||||
|
|
||||
|
# Find operators and simple assignments |
||||
|
Matcher(SIMPLE_LVALUE, Type.SIMPLE_LVALUE), |
||||
|
Matcher(OPERATOR, Type.OPERATOR), |
||||
|
|
||||
|
# Find key words and whitespace |
||||
|
Matcher(KEYWORD, Type.KEYWORD), |
||||
|
Matcher(WHITESPACE, Type.WHITESPACE), |
||||
|
|
||||
|
# Find identifiers |
||||
|
Matcher(IDENTIFIER, Type.IDENTIFIER), |
||||
|
|
||||
|
# Finally, we convert semicolons to tokens. |
||||
|
Matcher(SEMICOLON, Type.SEMICOLON)], |
||||
|
|
||||
|
|
||||
|
# Matchers for single quote strings. |
||||
|
JavaScriptModes.SINGLE_QUOTE_STRING_MODE: [ |
||||
|
Matcher(SINGLE_QUOTE_TEXT, Type.STRING_TEXT), |
||||
|
Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_END, |
||||
|
JavaScriptModes.TEXT_MODE)], |
||||
|
|
||||
|
|
||||
|
# Matchers for double quote strings. |
||||
|
JavaScriptModes.DOUBLE_QUOTE_STRING_MODE: [ |
||||
|
Matcher(DOUBLE_QUOTE_TEXT, Type.STRING_TEXT), |
||||
|
Matcher(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(END_BLOCK_COMMENT, Type.END_BLOCK_COMMENT, |
||||
|
JavaScriptModes.TEXT_MODE), |
||||
|
|
||||
|
# Match non-comment-ending text.. |
||||
|
Matcher(BLOCK_COMMENT_TEXT, Type.COMMENT)], |
||||
|
|
||||
|
|
||||
|
# Matchers for doc comments. |
||||
|
JavaScriptModes.DOC_COMMENT_MODE: COMMON_DOC_MATCHERS + [ |
||||
|
Matcher(DOC_COMMENT_TEXT, Type.COMMENT)], |
||||
|
|
||||
|
JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: COMMON_DOC_MATCHERS + [ |
||||
|
Matcher(WHITESPACE, Type.COMMENT), |
||||
|
Matcher(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(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(OPENING_PAREN, Type.START_PARAMETERS, |
||||
|
JavaScriptModes.PARAMETER_MODE), |
||||
|
Matcher(WHITESPACE, Type.WHITESPACE), |
||||
|
Matcher(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(CLOSING_PAREN_WITH_SPACE, Type.END_PARAMETERS, |
||||
|
JavaScriptModes.TEXT_MODE), |
||||
|
Matcher(PARAMETERS, Type.PARAMETERS, JavaScriptModes.PARAMETER_MODE)]} |
||||
|
|
||||
|
|
||||
|
# 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 |
||||
|
} |
||||
|
|
||||
|
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.JAVASCRIPT_MATCHERS |
||||
|
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) |
@ -0,0 +1,147 @@ |
|||||
|
#!/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: * ' |
||||
|
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]) |
||||
|
|
||||
|
FLAG_DESCRIPTION_TYPES = frozenset([ |
||||
|
DOC_INLINE_FLAG, COMMENT, DOC_START_BRACE, DOC_END_BRACE]) |
||||
|
|
||||
|
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) |
@ -0,0 +1,964 @@ |
|||||
|
#!/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. |
||||
|
|
||||
|
"""Light weight EcmaScript state tracker that reads tokens and tracks state.""" |
||||
|
|
||||
|
__author__ = ('robbyw@google.com (Robert Walker)', |
||||
|
'ajp@google.com (Andy Perelson)') |
||||
|
|
||||
|
import re |
||||
|
|
||||
|
from closure_linter import javascripttokenizer |
||||
|
from closure_linter import javascripttokens |
||||
|
from closure_linter import tokenutil |
||||
|
|
||||
|
# Shorthand |
||||
|
Type = javascripttokens.JavaScriptTokenType |
||||
|
|
||||
|
|
||||
|
class DocFlag(object): |
||||
|
"""Generic 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 type, |
||||
|
including braces. |
||||
|
type_end_token: The last token specifying the flag type, |
||||
|
including braces. |
||||
|
type: The type spec. |
||||
|
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. |
||||
|
|
||||
|
# The list of standard jsdoc tags is from |
||||
|
STANDARD_DOC = frozenset([ |
||||
|
'author', |
||||
|
'bug', |
||||
|
'const', |
||||
|
'constructor', |
||||
|
'define', |
||||
|
'deprecated', |
||||
|
'enum', |
||||
|
'export', |
||||
|
'extends', |
||||
|
'externs', |
||||
|
'fileoverview', |
||||
|
'implements', |
||||
|
'implicitCast', |
||||
|
'interface', |
||||
|
'license', |
||||
|
'noalias', |
||||
|
'nocompile', |
||||
|
'nosideeffects', |
||||
|
'override', |
||||
|
'owner', |
||||
|
'param', |
||||
|
'preserve', |
||||
|
'private', |
||||
|
'return', |
||||
|
'see', |
||||
|
'supported', |
||||
|
'template', |
||||
|
'this', |
||||
|
'type', |
||||
|
'typedef', |
||||
|
]) |
||||
|
|
||||
|
ANNOTATION = frozenset(['preserveTry', 'suppress']) |
||||
|
|
||||
|
LEGAL_DOC = STANDARD_DOC | ANNOTATION |
||||
|
|
||||
|
# Includes all Closure Compiler @suppress types. |
||||
|
# Not all of these annotations are interpreted by Closure Linter. |
||||
|
SUPPRESS_TYPES = frozenset([ |
||||
|
'accessControls', |
||||
|
'checkRegExp', |
||||
|
'checkTypes', |
||||
|
'checkVars', |
||||
|
'deprecated', |
||||
|
'duplicate', |
||||
|
'fileoverviewTags', |
||||
|
'invalidCasts', |
||||
|
'missingProperties', |
||||
|
'nonStandardJsDocs', |
||||
|
'strictModuleDepCheck', |
||||
|
'undefinedVars', |
||||
|
'underscore', |
||||
|
'unknownDefines', |
||||
|
'uselessCode', |
||||
|
'visibility', |
||||
|
'with']) |
||||
|
|
||||
|
HAS_DESCRIPTION = frozenset([ |
||||
|
'define', 'deprecated', 'desc', 'fileoverview', 'license', 'param', |
||||
|
'preserve', 'return', 'supported']) |
||||
|
|
||||
|
HAS_TYPE = frozenset([ |
||||
|
'define', 'enum', 'extends', 'implements', 'param', 'return', 'type', |
||||
|
'suppress']) |
||||
|
|
||||
|
TYPE_ONLY = frozenset(['enum', 'extends', 'implements', 'suppress', 'type']) |
||||
|
|
||||
|
HAS_NAME = frozenset(['param']) |
||||
|
|
||||
|
EMPTY_COMMENT_LINE = re.compile(r'^\s*\*?\s*$') |
||||
|
EMPTY_STRING = re.compile(r'^\s*$') |
||||
|
|
||||
|
def __init__(self, flag_token): |
||||
|
"""Creates the DocFlag object and attaches it to the given start token. |
||||
|
|
||||
|
Args: |
||||
|
flag_token: The starting token of the flag. |
||||
|
""" |
||||
|
self.flag_token = flag_token |
||||
|
self.flag_type = flag_token.string.strip().lstrip('@') |
||||
|
|
||||
|
# Extract type, if applicable. |
||||
|
self.type = None |
||||
|
self.type_start_token = None |
||||
|
self.type_end_token = None |
||||
|
if self.flag_type in self.HAS_TYPE: |
||||
|
brace = tokenutil.SearchUntil(flag_token, [Type.DOC_START_BRACE], |
||||
|
Type.FLAG_ENDING_TYPES) |
||||
|
if brace: |
||||
|
end_token, contents = _GetMatchingEndBraceAndContents(brace) |
||||
|
self.type = contents |
||||
|
self.type_start_token = brace |
||||
|
self.type_end_token = end_token |
||||
|
elif (self.flag_type in self.TYPE_ONLY and |
||||
|
flag_token.next.type not in Type.FLAG_ENDING_TYPES): |
||||
|
self.type_start_token = flag_token.next |
||||
|
self.type_end_token, self.type = _GetEndTokenAndContents( |
||||
|
self.type_start_token) |
||||
|
if self.type is not None: |
||||
|
self.type = self.type.strip() |
||||
|
|
||||
|
# Extract name, if applicable. |
||||
|
self.name_token = None |
||||
|
self.name = None |
||||
|
if self.flag_type in self.HAS_NAME: |
||||
|
# Handle bad case, name could be immediately after flag token. |
||||
|
self.name_token = _GetNextIdentifierToken(flag_token) |
||||
|
|
||||
|
# Handle good case, if found token is after type start, look for |
||||
|
# identifier after type end, since types contain identifiers. |
||||
|
if (self.type and self.name_token and |
||||
|
tokenutil.Compare(self.name_token, self.type_start_token) > 0): |
||||
|
self.name_token = _GetNextIdentifierToken(self.type_end_token) |
||||
|
|
||||
|
if self.name_token: |
||||
|
self.name = self.name_token.string |
||||
|
|
||||
|
# Extract description, if applicable. |
||||
|
self.description_start_token = None |
||||
|
self.description_end_token = None |
||||
|
self.description = None |
||||
|
if self.flag_type in self.HAS_DESCRIPTION: |
||||
|
search_start_token = flag_token |
||||
|
if self.name_token and self.type_end_token: |
||||
|
if tokenutil.Compare(self.type_end_token, self.name_token) > 0: |
||||
|
search_start_token = self.type_end_token |
||||
|
else: |
||||
|
search_start_token = self.name_token |
||||
|
elif self.name_token: |
||||
|
search_start_token = self.name_token |
||||
|
elif self.type: |
||||
|
search_start_token = self.type_end_token |
||||
|
|
||||
|
interesting_token = tokenutil.Search(search_start_token, |
||||
|
Type.FLAG_DESCRIPTION_TYPES | Type.FLAG_ENDING_TYPES) |
||||
|
if interesting_token.type in Type.FLAG_DESCRIPTION_TYPES: |
||||
|
self.description_start_token = interesting_token |
||||
|
self.description_end_token, self.description = ( |
||||
|
_GetEndTokenAndContents(interesting_token)) |
||||
|
|
||||
|
|
||||
|
class DocComment(object): |
||||
|
"""JavaScript doc comment object. |
||||
|
|
||||
|
Attributes: |
||||
|
ordered_params: Ordered list of parameters documented. |
||||
|
start_token: The token that starts the doc comment. |
||||
|
end_token: The token that ends the doc comment. |
||||
|
suppressions: Map of suppression type to the token that added it. |
||||
|
""" |
||||
|
def __init__(self, start_token): |
||||
|
"""Create the doc comment object. |
||||
|
|
||||
|
Args: |
||||
|
start_token: The first token in the doc comment. |
||||
|
""" |
||||
|
self.__params = {} |
||||
|
self.ordered_params = [] |
||||
|
self.__flags = {} |
||||
|
self.start_token = start_token |
||||
|
self.end_token = None |
||||
|
self.suppressions = {} |
||||
|
self.invalidated = False |
||||
|
|
||||
|
def Invalidate(self): |
||||
|
"""Indicate that the JSDoc is well-formed but we had problems parsing it. |
||||
|
|
||||
|
This is a short-circuiting mechanism so that we don't emit false |
||||
|
positives about well-formed doc comments just because we don't support |
||||
|
hot new syntaxes. |
||||
|
""" |
||||
|
self.invalidated = True |
||||
|
|
||||
|
def IsInvalidated(self): |
||||
|
"""Test whether Invalidate() has been called.""" |
||||
|
return self.invalidated |
||||
|
|
||||
|
def AddParam(self, name, param_type): |
||||
|
"""Add a new documented parameter. |
||||
|
|
||||
|
Args: |
||||
|
name: The name of the parameter to document. |
||||
|
param_type: The parameter's declared JavaScript type. |
||||
|
""" |
||||
|
self.ordered_params.append(name) |
||||
|
self.__params[name] = param_type |
||||
|
|
||||
|
def AddSuppression(self, token): |
||||
|
"""Add a new error suppression flag. |
||||
|
|
||||
|
Args: |
||||
|
token: The suppression flag token. |
||||
|
""" |
||||
|
#TODO(user): Error if no braces |
||||
|
brace = tokenutil.SearchUntil(token, [Type.DOC_START_BRACE], |
||||
|
[Type.DOC_FLAG]) |
||||
|
if brace: |
||||
|
end_token, contents = _GetMatchingEndBraceAndContents(brace) |
||||
|
self.suppressions[contents] = token |
||||
|
|
||||
|
def AddFlag(self, flag): |
||||
|
"""Add a new document flag. |
||||
|
|
||||
|
Args: |
||||
|
flag: DocFlag object. |
||||
|
""" |
||||
|
self.__flags[flag.flag_type] = flag |
||||
|
|
||||
|
def InheritsDocumentation(self): |
||||
|
"""Test if the jsdoc implies documentation inheritance. |
||||
|
|
||||
|
Returns: |
||||
|
True if documentation may be pulled off the superclass. |
||||
|
""" |
||||
|
return (self.HasFlag('inheritDoc') or |
||||
|
(self.HasFlag('override') and |
||||
|
not self.HasFlag('return') and |
||||
|
not self.HasFlag('param'))) |
||||
|
|
||||
|
def HasFlag(self, flag_type): |
||||
|
"""Test if the given flag has been set. |
||||
|
|
||||
|
Args: |
||||
|
flag_type: The type of the flag to check. |
||||
|
|
||||
|
Returns: |
||||
|
True if the flag is set. |
||||
|
""" |
||||
|
return flag_type in self.__flags |
||||
|
|
||||
|
def GetFlag(self, flag_type): |
||||
|
"""Gets the last flag of the given type. |
||||
|
|
||||
|
Args: |
||||
|
flag_type: The type of the flag to get. |
||||
|
|
||||
|
Returns: |
||||
|
The last instance of the given flag type in this doc comment. |
||||
|
""" |
||||
|
return self.__flags[flag_type] |
||||
|
|
||||
|
def CompareParameters(self, params): |
||||
|
"""Computes the edit distance and list from the function params to the docs. |
||||
|
|
||||
|
Uses the Levenshtein edit distance algorithm, with code modified from |
||||
|
http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#Python |
||||
|
|
||||
|
Args: |
||||
|
params: The parameter list for the function declaration. |
||||
|
|
||||
|
Returns: |
||||
|
The edit distance, the edit list. |
||||
|
""" |
||||
|
source_len, target_len = len(self.ordered_params), len(params) |
||||
|
edit_lists = [[]] |
||||
|
distance = [[]] |
||||
|
for i in range(target_len+1): |
||||
|
edit_lists[0].append(['I'] * i) |
||||
|
distance[0].append(i) |
||||
|
|
||||
|
for j in range(1, source_len+1): |
||||
|
edit_lists.append([['D'] * j]) |
||||
|
distance.append([j]) |
||||
|
|
||||
|
for i in range(source_len): |
||||
|
for j in range(target_len): |
||||
|
cost = 1 |
||||
|
if self.ordered_params[i] == params[j]: |
||||
|
cost = 0 |
||||
|
|
||||
|
deletion = distance[i][j+1] + 1 |
||||
|
insertion = distance[i+1][j] + 1 |
||||
|
substitution = distance[i][j] + cost |
||||
|
|
||||
|
edit_list = None |
||||
|
best = None |
||||
|
if deletion <= insertion and deletion <= substitution: |
||||
|
# Deletion is best. |
||||
|
best = deletion |
||||
|
edit_list = list(edit_lists[i][j+1]) |
||||
|
edit_list.append('D') |
||||
|
|
||||
|
elif insertion <= substitution: |
||||
|
# Insertion is best. |
||||
|
best = insertion |
||||
|
edit_list = list(edit_lists[i+1][j]) |
||||
|
edit_list.append('I') |
||||
|
edit_lists[i+1].append(edit_list) |
||||
|
|
||||
|
else: |
||||
|
# Substitution is best. |
||||
|
best = substitution |
||||
|
edit_list = list(edit_lists[i][j]) |
||||
|
if cost: |
||||
|
edit_list.append('S') |
||||
|
else: |
||||
|
edit_list.append('=') |
||||
|
|
||||
|
edit_lists[i+1].append(edit_list) |
||||
|
distance[i+1].append(best) |
||||
|
|
||||
|
return distance[source_len][target_len], edit_lists[source_len][target_len] |
||||
|
|
||||
|
def __repr__(self): |
||||
|
"""Returns a string representation of this object. |
||||
|
|
||||
|
Returns: |
||||
|
A string representation of this object. |
||||
|
""" |
||||
|
return '<DocComment: %s, %s>' % (str(self.__params), str(self.__flags)) |
||||
|
|
||||
|
|
||||
|
# |
||||
|
# Helper methods used by DocFlag and DocComment to parse out flag information. |
||||
|
# |
||||
|
|
||||
|
|
||||
|
def _GetMatchingEndBraceAndContents(start_brace): |
||||
|
"""Returns the matching end brace and contents between the two braces. |
||||
|
|
||||
|
If any FLAG_ENDING_TYPE token is encountered before a matching end brace, then |
||||
|
that token is used as the matching ending token. Contents will have all |
||||
|
comment prefixes stripped out of them, and all comment prefixes in between the |
||||
|
start and end tokens will be split out into separate DOC_PREFIX tokens. |
||||
|
|
||||
|
Args: |
||||
|
start_brace: The DOC_START_BRACE token immediately before desired contents. |
||||
|
|
||||
|
Returns: |
||||
|
The matching ending token (DOC_END_BRACE or FLAG_ENDING_TYPE) and a string |
||||
|
of the contents between the matching tokens, minus any comment prefixes. |
||||
|
""" |
||||
|
open_count = 1 |
||||
|
close_count = 0 |
||||
|
contents = [] |
||||
|
|
||||
|
# We don't consider the start brace part of the type string. |
||||
|
token = start_brace.next |
||||
|
while open_count != close_count: |
||||
|
if token.type == Type.DOC_START_BRACE: |
||||
|
open_count += 1 |
||||
|
elif token.type == Type.DOC_END_BRACE: |
||||
|
close_count += 1 |
||||
|
|
||||
|
if token.type != Type.DOC_PREFIX: |
||||
|
contents.append(token.string) |
||||
|
|
||||
|
if token.type in Type.FLAG_ENDING_TYPES: |
||||
|
break |
||||
|
token = token.next |
||||
|
|
||||
|
#Don't include the end token (end brace, end doc comment, etc.) in type. |
||||
|
token = token.previous |
||||
|
contents = contents[:-1] |
||||
|
|
||||
|
return token, ''.join(contents) |
||||
|
|
||||
|
|
||||
|
def _GetNextIdentifierToken(start_token): |
||||
|
"""Searches for and returns the first identifier at the beginning of a token. |
||||
|
|
||||
|
Searches each token after the start to see if it starts with an identifier. |
||||
|
If found, will split the token into at most 3 piecies: leading whitespace, |
||||
|
identifier, rest of token, returning the identifier token. If no identifier is |
||||
|
found returns None and changes no tokens. Search is abandoned when a |
||||
|
FLAG_ENDING_TYPE token is found. |
||||
|
|
||||
|
Args: |
||||
|
start_token: The token to start searching after. |
||||
|
|
||||
|
Returns: |
||||
|
The identifier token is found, None otherwise. |
||||
|
""" |
||||
|
token = start_token.next |
||||
|
|
||||
|
while token and not token.type in Type.FLAG_ENDING_TYPES: |
||||
|
match = javascripttokenizer.JavaScriptTokenizer.IDENTIFIER.match( |
||||
|
token.string) |
||||
|
if (match is not None and token.type == Type.COMMENT and |
||||
|
len(token.string) == len(match.group(0))): |
||||
|
return token |
||||
|
|
||||
|
token = token.next |
||||
|
|
||||
|
return None |
||||
|
|
||||
|
|
||||
|
def _GetEndTokenAndContents(start_token): |
||||
|
"""Returns last content token and all contents before FLAG_ENDING_TYPE token. |
||||
|
|
||||
|
Comment prefixes are split into DOC_PREFIX tokens and stripped from the |
||||
|
returned contents. |
||||
|
|
||||
|
Args: |
||||
|
start_token: The token immediately before the first content token. |
||||
|
|
||||
|
Returns: |
||||
|
The last content token and a string of all contents including start and |
||||
|
end tokens, with comment prefixes stripped. |
||||
|
""" |
||||
|
iterator = start_token |
||||
|
last_line = iterator.line_number |
||||
|
last_token = None |
||||
|
contents = '' |
||||
|
while not iterator.type in Type.FLAG_ENDING_TYPES: |
||||
|
if (iterator.IsFirstInLine() and |
||||
|
DocFlag.EMPTY_COMMENT_LINE.match(iterator.line)): |
||||
|
# If we have a blank comment line, consider that an implicit |
||||
|
# ending of the description. This handles a case like: |
||||
|
# |
||||
|
# * @return {boolean} True |
||||
|
# * |
||||
|
# * Note: This is a sentence. |
||||
|
# |
||||
|
# The note is not part of the @return description, but there was |
||||
|
# no definitive ending token. Rather there was a line containing |
||||
|
# only a doc comment prefix or whitespace. |
||||
|
break |
||||
|
|
||||
|
if iterator.type in Type.FLAG_DESCRIPTION_TYPES: |
||||
|
contents += iterator.string |
||||
|
last_token = iterator |
||||
|
|
||||
|
iterator = iterator.next |
||||
|
if iterator.line_number != last_line: |
||||
|
contents += '\n' |
||||
|
last_line = iterator.line_number |
||||
|
|
||||
|
end_token = last_token |
||||
|
if DocFlag.EMPTY_STRING.match(contents): |
||||
|
contents = None |
||||
|
else: |
||||
|
# Strip trailing newline. |
||||
|
contents = contents[:-1] |
||||
|
|
||||
|
return end_token, contents |
||||
|
|
||||
|
|
||||
|
class Function(object): |
||||
|
"""Data about a JavaScript function. |
||||
|
|
||||
|
Attributes: |
||||
|
block_depth: Block depth the function began at. |
||||
|
doc: The DocComment associated with the function. |
||||
|
has_return: If the function has a return value. |
||||
|
has_this: If the function references the 'this' object. |
||||
|
is_assigned: If the function is part of an assignment. |
||||
|
is_constructor: If the function is a constructor. |
||||
|
name: The name of the function, whether given in the function keyword or |
||||
|
as the lvalue the function is assigned to. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, block_depth, is_assigned, doc, name): |
||||
|
self.block_depth = block_depth |
||||
|
self.is_assigned = is_assigned |
||||
|
self.is_constructor = doc and doc.HasFlag('constructor') |
||||
|
self.is_interface = doc and doc.HasFlag('interface') |
||||
|
self.has_return = False |
||||
|
self.has_this = False |
||||
|
self.name = name |
||||
|
self.doc = doc |
||||
|
|
||||
|
|
||||
|
class StateTracker(object): |
||||
|
"""EcmaScript state tracker. |
||||
|
|
||||
|
Tracks block depth, function names, etc. within an EcmaScript token stream. |
||||
|
""" |
||||
|
|
||||
|
OBJECT_LITERAL = 'o' |
||||
|
CODE = 'c' |
||||
|
|
||||
|
def __init__(self, doc_flag=DocFlag): |
||||
|
"""Initializes a JavaScript token stream state tracker. |
||||
|
|
||||
|
Args: |
||||
|
doc_flag: An optional custom DocFlag used for validating |
||||
|
documentation flags. |
||||
|
""" |
||||
|
self._doc_flag = doc_flag |
||||
|
self.Reset() |
||||
|
|
||||
|
def Reset(self): |
||||
|
"""Resets the state tracker to prepare for processing a new page.""" |
||||
|
self._block_depth = 0 |
||||
|
self._is_block_close = False |
||||
|
self._paren_depth = 0 |
||||
|
self._functions = [] |
||||
|
self._functions_by_name = {} |
||||
|
self._last_comment = None |
||||
|
self._doc_comment = None |
||||
|
self._cumulative_params = None |
||||
|
self._block_types = [] |
||||
|
self._last_non_space_token = None |
||||
|
self._last_line = None |
||||
|
self._first_token = None |
||||
|
self._documented_identifiers = set() |
||||
|
|
||||
|
def InFunction(self): |
||||
|
"""Returns true if the current token is within a function. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within a function. |
||||
|
""" |
||||
|
return bool(self._functions) |
||||
|
|
||||
|
def InConstructor(self): |
||||
|
"""Returns true if the current token is within a constructor. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within a constructor. |
||||
|
""" |
||||
|
return self.InFunction() and self._functions[-1].is_constructor |
||||
|
|
||||
|
def InInterfaceMethod(self): |
||||
|
"""Returns true if the current token is within an interface method. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within an interface method. |
||||
|
""" |
||||
|
if self.InFunction(): |
||||
|
if self._functions[-1].is_interface: |
||||
|
return True |
||||
|
else: |
||||
|
name = self._functions[-1].name |
||||
|
prototype_index = name.find('.prototype.') |
||||
|
if prototype_index != -1: |
||||
|
class_function_name = name[0:prototype_index] |
||||
|
if (class_function_name in self._functions_by_name and |
||||
|
self._functions_by_name[class_function_name].is_interface): |
||||
|
return True |
||||
|
|
||||
|
return False |
||||
|
|
||||
|
def InTopLevelFunction(self): |
||||
|
"""Returns true if the current token is within a top level function. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within a top level function. |
||||
|
""" |
||||
|
return len(self._functions) == 1 and self.InTopLevel() |
||||
|
|
||||
|
def InAssignedFunction(self): |
||||
|
"""Returns true if the current token is within a function variable. |
||||
|
|
||||
|
Returns: |
||||
|
True if if the current token is within a function variable |
||||
|
""" |
||||
|
return self.InFunction() and self._functions[-1].is_assigned |
||||
|
|
||||
|
def IsFunctionOpen(self): |
||||
|
"""Returns true if the current token is a function block open. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is a function block open. |
||||
|
""" |
||||
|
return (self._functions and |
||||
|
self._functions[-1].block_depth == self._block_depth - 1) |
||||
|
|
||||
|
def IsFunctionClose(self): |
||||
|
"""Returns true if the current token is a function block close. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is a function block close. |
||||
|
""" |
||||
|
return (self._functions and |
||||
|
self._functions[-1].block_depth == self._block_depth) |
||||
|
|
||||
|
def InBlock(self): |
||||
|
"""Returns true if the current token is within a block. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within a block. |
||||
|
""" |
||||
|
return bool(self._block_depth) |
||||
|
|
||||
|
def IsBlockClose(self): |
||||
|
"""Returns true if the current token is a block close. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is a block close. |
||||
|
""" |
||||
|
return self._is_block_close |
||||
|
|
||||
|
def InObjectLiteral(self): |
||||
|
"""Returns true if the current token is within an object literal. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within an object literal. |
||||
|
""" |
||||
|
return self._block_depth and self._block_types[-1] == self.OBJECT_LITERAL |
||||
|
|
||||
|
def InObjectLiteralDescendant(self): |
||||
|
"""Returns true if the current token has an object literal ancestor. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token has an object literal ancestor. |
||||
|
""" |
||||
|
return self.OBJECT_LITERAL in self._block_types |
||||
|
|
||||
|
def InParentheses(self): |
||||
|
"""Returns true if the current token is within parentheses. |
||||
|
|
||||
|
Returns: |
||||
|
True if the current token is within parentheses. |
||||
|
""" |
||||
|
return bool(self._paren_depth) |
||||
|
|
||||
|
def InTopLevel(self): |
||||
|
"""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. |
||||
|
""" |
||||
|
raise TypeError('Abstract method InTopLevel not implemented') |
||||
|
|
||||
|
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. |
||||
|
""" |
||||
|
raise TypeError('Abstract method GetBlockType not implemented') |
||||
|
|
||||
|
def GetParams(self): |
||||
|
"""Returns the accumulated input params as an array. |
||||
|
|
||||
|
In some EcmasSript languages, input params are specified like |
||||
|
(param:Type, param2:Type2, ...) |
||||
|
in other they are specified just as |
||||
|
(param, param2) |
||||
|
We handle both formats for specifying parameters here and leave |
||||
|
it to the compilers for each language to detect compile errors. |
||||
|
This allows more code to be reused between lint checkers for various |
||||
|
EcmaScript languages. |
||||
|
|
||||
|
Returns: |
||||
|
The accumulated input params as an array. |
||||
|
""" |
||||
|
params = [] |
||||
|
if self._cumulative_params: |
||||
|
params = re.compile(r'\s+').sub('', self._cumulative_params).split(',') |
||||
|
# Strip out the type from parameters of the form name:Type. |
||||
|
params = map(lambda param: param.split(':')[0], params) |
||||
|
|
||||
|
return params |
||||
|
|
||||
|
def GetLastComment(self): |
||||
|
"""Return the last plain comment that could be used as documentation. |
||||
|
|
||||
|
Returns: |
||||
|
The last plain comment that could be used as documentation. |
||||
|
""" |
||||
|
return self._last_comment |
||||
|
|
||||
|
def GetDocComment(self): |
||||
|
"""Return the most recent applicable documentation comment. |
||||
|
|
||||
|
Returns: |
||||
|
The last applicable documentation comment. |
||||
|
""" |
||||
|
return self._doc_comment |
||||
|
|
||||
|
def HasDocComment(self, identifier): |
||||
|
"""Returns whether the identifier has been documented yet. |
||||
|
|
||||
|
Args: |
||||
|
identifier: The identifier. |
||||
|
|
||||
|
Returns: |
||||
|
Whether the identifier has been documented yet. |
||||
|
""" |
||||
|
return identifier in self._documented_identifiers |
||||
|
|
||||
|
def InDocComment(self): |
||||
|
"""Returns whether the current token is in a doc comment. |
||||
|
|
||||
|
Returns: |
||||
|
Whether the current token is in a doc comment. |
||||
|
""" |
||||
|
return self._doc_comment and self._doc_comment.end_token is None |
||||
|
|
||||
|
def GetDocFlag(self): |
||||
|
"""Returns the current documentation flags. |
||||
|
|
||||
|
Returns: |
||||
|
The current documentation flags. |
||||
|
""" |
||||
|
return self._doc_flag |
||||
|
|
||||
|
def IsTypeToken(self, t): |
||||
|
if self.InDocComment() and t.type not in (Type.START_DOC_COMMENT, |
||||
|
Type.DOC_FLAG, Type.DOC_INLINE_FLAG, Type.DOC_PREFIX): |
||||
|
f = tokenutil.SearchUntil(t, [Type.DOC_FLAG], [Type.START_DOC_COMMENT], |
||||
|
None, True) |
||||
|
if f and f.attached_object.type_start_token is not None: |
||||
|
return (tokenutil.Compare(t, f.attached_object.type_start_token) > 0 and |
||||
|
tokenutil.Compare(t, f.attached_object.type_end_token) < 0) |
||||
|
return False |
||||
|
|
||||
|
def GetFunction(self): |
||||
|
"""Return the function the current code block is a part of. |
||||
|
|
||||
|
Returns: |
||||
|
The current Function object. |
||||
|
""" |
||||
|
if self._functions: |
||||
|
return self._functions[-1] |
||||
|
|
||||
|
def GetBlockDepth(self): |
||||
|
"""Return the block depth. |
||||
|
|
||||
|
Returns: |
||||
|
The current block depth. |
||||
|
""" |
||||
|
return self._block_depth |
||||
|
|
||||
|
def GetLastNonSpaceToken(self): |
||||
|
"""Return the last non whitespace token.""" |
||||
|
return self._last_non_space_token |
||||
|
|
||||
|
def GetLastLine(self): |
||||
|
"""Return the last line.""" |
||||
|
return self._last_line |
||||
|
|
||||
|
def GetFirstToken(self): |
||||
|
"""Return the very first token in the file.""" |
||||
|
return self._first_token |
||||
|
|
||||
|
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: |
||||
|
""" |
||||
|
self._is_block_close = False |
||||
|
|
||||
|
if not self._first_token: |
||||
|
self._first_token = token |
||||
|
|
||||
|
# Track block depth. |
||||
|
type = token.type |
||||
|
if type == Type.START_BLOCK: |
||||
|
self._block_depth += 1 |
||||
|
|
||||
|
# Subclasses need to handle block start very differently because |
||||
|
# whether a block is a CODE or OBJECT_LITERAL block varies significantly |
||||
|
# by language. |
||||
|
self._block_types.append(self.GetBlockType(token)) |
||||
|
|
||||
|
# Track block depth. |
||||
|
elif type == Type.END_BLOCK: |
||||
|
self._is_block_close = not self.InObjectLiteral() |
||||
|
self._block_depth -= 1 |
||||
|
self._block_types.pop() |
||||
|
|
||||
|
# Track parentheses depth. |
||||
|
elif type == Type.START_PAREN: |
||||
|
self._paren_depth += 1 |
||||
|
|
||||
|
# Track parentheses depth. |
||||
|
elif type == Type.END_PAREN: |
||||
|
self._paren_depth -= 1 |
||||
|
|
||||
|
elif type == Type.COMMENT: |
||||
|
self._last_comment = token.string |
||||
|
|
||||
|
elif type == Type.START_DOC_COMMENT: |
||||
|
self._last_comment = None |
||||
|
self._doc_comment = DocComment(token) |
||||
|
|
||||
|
elif type == Type.END_DOC_COMMENT: |
||||
|
self._doc_comment.end_token = token |
||||
|
|
||||
|
elif type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG): |
||||
|
flag = self._doc_flag(token) |
||||
|
token.attached_object = flag |
||||
|
self._doc_comment.AddFlag(flag) |
||||
|
|
||||
|
if flag.flag_type == 'param' and flag.name: |
||||
|
self._doc_comment.AddParam(flag.name, flag.type) |
||||
|
elif flag.flag_type == 'suppress': |
||||
|
self._doc_comment.AddSuppression(token) |
||||
|
|
||||
|
elif type == Type.FUNCTION_DECLARATION: |
||||
|
last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, None, |
||||
|
True) |
||||
|
doc = None |
||||
|
# Only functions outside of parens are eligible for documentation. |
||||
|
if not self._paren_depth: |
||||
|
doc = self._doc_comment |
||||
|
|
||||
|
name = '' |
||||
|
is_assigned = last_code and (last_code.IsOperator('=') or |
||||
|
last_code.IsOperator('||') or last_code.IsOperator('&&') or |
||||
|
(last_code.IsOperator(':') and not self.InObjectLiteral())) |
||||
|
if is_assigned: |
||||
|
# TODO(robbyw): This breaks for x[2] = ... |
||||
|
# Must use loop to find full function name in the case of line-wrapped |
||||
|
# declarations (bug 1220601) like: |
||||
|
# my.function.foo. |
||||
|
# bar = function() ... |
||||
|
identifier = tokenutil.Search(last_code, Type.SIMPLE_LVALUE, None, True) |
||||
|
while identifier and identifier.type in ( |
||||
|
Type.IDENTIFIER, Type.SIMPLE_LVALUE): |
||||
|
name = identifier.string + name |
||||
|
# Traverse behind us, skipping whitespace and comments. |
||||
|
while True: |
||||
|
identifier = identifier.previous |
||||
|
if not identifier or not identifier.type in Type.NON_CODE_TYPES: |
||||
|
break |
||||
|
|
||||
|
else: |
||||
|
next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) |
||||
|
while next_token and next_token.IsType(Type.FUNCTION_NAME): |
||||
|
name += next_token.string |
||||
|
next_token = tokenutil.Search(next_token, Type.FUNCTION_NAME, 2) |
||||
|
|
||||
|
function = Function(self._block_depth, is_assigned, doc, name) |
||||
|
self._functions.append(function) |
||||
|
self._functions_by_name[name] = function |
||||
|
|
||||
|
elif type == Type.START_PARAMETERS: |
||||
|
self._cumulative_params = '' |
||||
|
|
||||
|
elif type == Type.PARAMETERS: |
||||
|
self._cumulative_params += token.string |
||||
|
|
||||
|
elif type == Type.KEYWORD and token.string == 'return': |
||||
|
next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) |
||||
|
if not next_token.IsType(Type.SEMICOLON): |
||||
|
function = self.GetFunction() |
||||
|
if function: |
||||
|
function.has_return = True |
||||
|
|
||||
|
elif type == Type.SIMPLE_LVALUE: |
||||
|
identifier = token.values['identifier'] |
||||
|
jsdoc = self.GetDocComment() |
||||
|
if jsdoc: |
||||
|
self._documented_identifiers.add(identifier) |
||||
|
|
||||
|
self._HandleIdentifier(identifier, True) |
||||
|
|
||||
|
elif type == Type.IDENTIFIER: |
||||
|
self._HandleIdentifier(token.string, False) |
||||
|
|
||||
|
# Detect documented non-assignments. |
||||
|
next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) |
||||
|
if next_token.IsType(Type.SEMICOLON): |
||||
|
if (self._last_non_space_token and |
||||
|
self._last_non_space_token.IsType(Type.END_DOC_COMMENT)): |
||||
|
self._documented_identifiers.add(token.string) |
||||
|
|
||||
|
def _HandleIdentifier(self, identifier, is_assignment): |
||||
|
"""Process the given identifier. |
||||
|
|
||||
|
Currently checks if it references 'this' and annotates the function |
||||
|
accordingly. |
||||
|
|
||||
|
Args: |
||||
|
identifier: The identifer to process. |
||||
|
is_assignment: Whether the identifer is being written to. |
||||
|
""" |
||||
|
if identifier == 'this' or identifier.startswith('this.'): |
||||
|
function = self.GetFunction() |
||||
|
if function: |
||||
|
function.has_this = True |
||||
|
|
||||
|
|
||||
|
def HandleAfterToken(self, token): |
||||
|
"""Handle updating state after a token has been checked. |
||||
|
|
||||
|
This function should be used for destructive state changes such as |
||||
|
deleting a tracked object. |
||||
|
|
||||
|
Args: |
||||
|
token: The token to handle. |
||||
|
""" |
||||
|
type = token.type |
||||
|
if type == Type.SEMICOLON or type == Type.END_PAREN or ( |
||||
|
type == Type.END_BRACKET and |
||||
|
self._last_non_space_token.type not in ( |
||||
|
Type.SINGLE_QUOTE_STRING_END, Type.DOUBLE_QUOTE_STRING_END)): |
||||
|
# We end on any numeric array index, but keep going for string based |
||||
|
# array indices so that we pick up manually exported identifiers. |
||||
|
self._doc_comment = None |
||||
|
self._last_comment = None |
||||
|
|
||||
|
elif type == Type.END_BLOCK: |
||||
|
self._doc_comment = None |
||||
|
self._last_comment = None |
||||
|
|
||||
|
if self.InFunction() and self.IsFunctionClose(): |
||||
|
# TODO(robbyw): Detect the function's name for better errors. |
||||
|
self._functions.pop() |
||||
|
|
||||
|
elif type == Type.END_PARAMETERS and self._doc_comment: |
||||
|
self._doc_comment = None |
||||
|
self._last_comment = None |
||||
|
|
||||
|
if not token.IsAnyType(Type.WHITESPACE, Type.BLANK_LINE): |
||||
|
self._last_non_space_token = token |
||||
|
|
||||
|
self._last_line = token.line |
@ -0,0 +1,285 @@ |
|||||
|
#!/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)') |
||||
|
|
||||
|
from closure_linter.common import tokens |
||||
|
from closure_linter import javascripttokens |
||||
|
|
||||
|
import copy |
||||
|
|
||||
|
# Shorthand |
||||
|
JavaScriptToken = javascripttokens.JavaScriptToken |
||||
|
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 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.next |
||||
|
if next: |
||||
|
if func(next): |
||||
|
return next |
||||
|
if end_func and end_func(next): |
||||
|
return None |
||||
|
|
||||
|
token = next |
||||
|
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 |
||||
|
""" |
||||
|
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, tokenCount): |
||||
|
"""Deletes the given number of tokens starting with the given token. |
||||
|
|
||||
|
Args: |
||||
|
token: The token to start deleting at. |
||||
|
tokenCount: The total number of tokens to delete. |
||||
|
""" |
||||
|
for i in xrange(1, tokenCount): |
||||
|
DeleteToken(token.next) |
||||
|
DeleteToken(token) |
||||
|
|
||||
|
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 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 InsertLineAfter(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) |
||||
|
InsertTokenAfter(blank_token, token) |
||||
|
# Update all subsequent ine numbers. |
||||
|
blank_token = blank_token.next |
||||
|
while blank_token: |
||||
|
blank_token.line_number += 1 |
||||
|
blank_token = blank_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. |
||||
|
|
||||
|
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 |
File diff suppressed because it is too large
@ -0,0 +1,5 @@ |
|||||
|
[egg_info] |
||||
|
tag_build = |
||||
|
tag_date = 0 |
||||
|
tag_svn_revision = 0 |
||||
|
|
@ -0,0 +1,38 @@ |
|||||
|
#!/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. |
||||
|
|
||||
|
try: |
||||
|
from setuptools import setup |
||||
|
except ImportError: |
||||
|
from distutils.core import setup |
||||
|
|
||||
|
setup(name='closure_linter', |
||||
|
version='2.2.6', |
||||
|
description='Closure Linter', |
||||
|
license='Apache', |
||||
|
author='The Closure Linter Authors', |
||||
|
author_email='opensource@google.com', |
||||
|
url='http://code.google.com/p/closure-linter', |
||||
|
install_requires=['python-gflags'], |
||||
|
package_dir={'closure_linter': 'closure_linter'}, |
||||
|
packages=['closure_linter', 'closure_linter.common'], |
||||
|
entry_points = { |
||||
|
'console_scripts': [ |
||||
|
'gjslint = closure_linter.gjslint:main', |
||||
|
'fixjsstyle = closure_linter.fixjsstyle:main' |
||||
|
] |
||||
|
} |
||||
|
) |
Loading…
Reference in new issue