blob: c7735a0e3f1c67f872a7d372ed4d4ecac5bc8554 [file] [log] [blame]
#!/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 StringIO
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 _SetLimitedDocChecks(self, limited_doc_checks):
"""Sets whether doc checking is relaxed for this file.
Args:
limited_doc_checks: Whether doc checking is relaxed for this file.
"""
self._limited_doc_checks = limited_doc_checks
def CheckToken(self, token, parser_state):
"""Checks a token, given the current parser_state, for warnings and errors.
Args:
token: The current token under consideration.
parser_state: Object that indicates the parser state in the page.
Raises:
TypeError: If not overridden.
"""
raise TypeError('Abstract method CheckToken not implemented')
def Finalize(self, parser_state, 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
# TODO(user): Factor out. A checker does not need to know about the
# tokenizer, only the token stream.
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, source=None):
"""Checks the file, printing warnings and errors as they are found.
Args:
filename: The name of the file to check.
source: Optional. The contents of the file. Can be either a string or
file-like object. If omitted, contents will be read from disk from
the given filename.
"""
if source is None:
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
else:
if type(source) in [str, unicode]:
f = StringIO.StringIO(source)
else:
f = source
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
lint_rules = self._lint_rules
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)
return self._CheckTokens(token, parse_error=parse_error,
debug_tokens=FLAGS.debug_tokens)
def _CheckTokens(self, token, parse_error, debug_tokens):
"""Checks a token stream for lint warnings/errors.
Args:
token: The first token in the token stream to check.
parse_error: A ParseError if any errors occurred.
debug_tokens: Whether every token should be printed as it is encountered
during the pass.
Returns:
A boolean indicating whether the full token stream could be checked or if
checking failed prematurely.
"""
result = self._ExecutePass(token, self._LintPass, parse_error, debug_tokens)
if not result:
return False
self._lint_rules.Finalize(self._state_tracker, self._tokenizer.mode)
self._error_handler.FinishFile()
return True
def _LintPass(self, token):
"""Checks an individual token for lint warnings/errors.
Used to encapsulate the logic needed to check an individual token so that it
can be passed to _ExecutePass.
Args:
token: The token to check.
"""
self._lint_rules.CheckToken(token, self._state_tracker)
def _ExecutePass(self, token, pass_function, parse_error=None,
debug_tokens=False):
"""Calls the given function for every token in the given token stream.
As each token is passed to the given function, state is kept up to date and,
depending on the error_trace flag, errors are either caught and reported, or
allowed to bubble up so developers can see the full stack trace. If a parse
error is specified, the pass will proceed as normal until the token causing
the parse error is reached.
Args:
token: The first token in the token stream.
pass_function: The function to call for each token in the token stream.
parse_error: A ParseError if any errors occurred.
debug_tokens: Whether every token should be printed as it is encountered
during the pass.
Returns:
A boolean indicating whether the full token stream could be checked or if
checking failed prematurely.
Raises:
Exception: If any error occurred while calling the given function.
"""
self._state_tracker.Reset()
while token:
if debug_tokens:
print token
if parse_error and parse_error.token == 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
try:
self._state_tracker.HandleToken(
token, self._state_tracker.GetLastNonSpaceToken())
pass_function(token)
self._state_tracker.HandleAfterToken(token)
except:
if FLAGS.error_trace:
raise
else:
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
token = token.next
return True