#!/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 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