1#!/usr/bin/env python 2# 3# Copyright 2008 The Closure Linter Authors. All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS-IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Base classes for writing checkers that operate on tokens.""" 18 19__author__ = ('robbyw@google.com (Robert Walker)', 20 'ajp@google.com (Andy Perelson)', 21 'jacobr@google.com (Jacob Richman)') 22 23import StringIO 24import traceback 25 26import gflags as flags 27from closure_linter import ecmametadatapass 28from closure_linter import errorrules 29from closure_linter import errors 30from closure_linter import javascripttokenizer 31from closure_linter.common import error 32from closure_linter.common import htmlutil 33 34FLAGS = flags.FLAGS 35flags.DEFINE_boolean('debug_tokens', False, 36 'Whether to print all tokens for debugging.') 37 38flags.DEFINE_boolean('error_trace', False, 39 'Whether to show error exceptions.') 40 41 42class LintRulesBase(object): 43 """Base class for all classes defining the lint rules for a language.""" 44 45 def __init__(self): 46 self.__checker = None 47 48 def Initialize(self, checker, limited_doc_checks, is_html): 49 """Initializes to prepare to check a file. 50 51 Args: 52 checker: Class to report errors to. 53 limited_doc_checks: Whether doc checking is relaxed for this file. 54 is_html: Whether the file is an HTML file with extracted contents. 55 """ 56 self.__checker = checker 57 self._limited_doc_checks = limited_doc_checks 58 self._is_html = is_html 59 60 def _HandleError(self, code, message, token, position=None, 61 fix_data=None): 62 """Call the HandleError function for the checker we are associated with.""" 63 if errorrules.ShouldReportError(code): 64 self.__checker.HandleError(code, message, token, position, fix_data) 65 66 def _SetLimitedDocChecks(self, limited_doc_checks): 67 """Sets whether doc checking is relaxed for this file. 68 69 Args: 70 limited_doc_checks: Whether doc checking is relaxed for this file. 71 """ 72 self._limited_doc_checks = limited_doc_checks 73 74 def CheckToken(self, token, parser_state): 75 """Checks a token, given the current parser_state, for warnings and errors. 76 77 Args: 78 token: The current token under consideration. 79 parser_state: Object that indicates the parser state in the page. 80 81 Raises: 82 TypeError: If not overridden. 83 """ 84 raise TypeError('Abstract method CheckToken not implemented') 85 86 def Finalize(self, parser_state, tokenizer_mode): 87 """Perform all checks that need to occur after all lines are processed. 88 89 Args: 90 parser_state: State of the parser after parsing all tokens 91 tokenizer_mode: Mode of the tokenizer after parsing the entire page 92 93 Raises: 94 TypeError: If not overridden. 95 """ 96 raise TypeError('Abstract method Finalize not implemented') 97 98 99class CheckerBase(object): 100 """This class handles checking a LintRules object against a file.""" 101 102 def __init__(self, error_handler, lint_rules, state_tracker, 103 limited_doc_files=None, metadata_pass=None): 104 """Initialize a checker object. 105 106 Args: 107 error_handler: Object that handles errors. 108 lint_rules: LintRules object defining lint errors given a token 109 and state_tracker object. 110 state_tracker: Object that tracks the current state in the token stream. 111 limited_doc_files: List of filenames that are not required to have 112 documentation comments. 113 metadata_pass: Object that builds metadata about the token stream. 114 """ 115 self._error_handler = error_handler 116 self._lint_rules = lint_rules 117 self._state_tracker = state_tracker 118 self._metadata_pass = metadata_pass 119 self._limited_doc_files = limited_doc_files 120 121 # TODO(user): Factor out. A checker does not need to know about the 122 # tokenizer, only the token stream. 123 self._tokenizer = javascripttokenizer.JavaScriptTokenizer() 124 125 self._has_errors = False 126 127 def HandleError(self, code, message, token, position=None, 128 fix_data=None): 129 """Prints out the given error message including a line number. 130 131 Args: 132 code: The error code. 133 message: The error to print. 134 token: The token where the error occurred, or None if it was a file-wide 135 issue. 136 position: The position of the error, defaults to None. 137 fix_data: Metadata used for fixing the error. 138 """ 139 self._has_errors = True 140 self._error_handler.HandleError( 141 error.Error(code, message, token, position, fix_data)) 142 143 def HasErrors(self): 144 """Returns true if the style checker has found any errors. 145 146 Returns: 147 True if the style checker has found any errors. 148 """ 149 return self._has_errors 150 151 def Check(self, filename, source=None): 152 """Checks the file, printing warnings and errors as they are found. 153 154 Args: 155 filename: The name of the file to check. 156 source: Optional. The contents of the file. Can be either a string or 157 file-like object. If omitted, contents will be read from disk from 158 the given filename. 159 """ 160 161 if source is None: 162 try: 163 f = open(filename) 164 except IOError: 165 self._error_handler.HandleFile(filename, None) 166 self.HandleError(errors.FILE_NOT_FOUND, 'File not found', None) 167 self._error_handler.FinishFile() 168 return 169 else: 170 if type(source) in [str, unicode]: 171 f = StringIO.StringIO(source) 172 else: 173 f = source 174 175 try: 176 if filename.endswith('.html') or filename.endswith('.htm'): 177 self.CheckLines(filename, htmlutil.GetScriptLines(f), True) 178 else: 179 self.CheckLines(filename, f, False) 180 finally: 181 f.close() 182 183 def CheckLines(self, filename, lines_iter, is_html): 184 """Checks a file, given as an iterable of lines, for warnings and errors. 185 186 Args: 187 filename: The name of the file to check. 188 lines_iter: An iterator that yields one line of the file at a time. 189 is_html: Whether the file being checked is an HTML file with extracted 190 contents. 191 192 Returns: 193 A boolean indicating whether the full file could be checked or if checking 194 failed prematurely. 195 """ 196 limited_doc_checks = False 197 if self._limited_doc_files: 198 for limited_doc_filename in self._limited_doc_files: 199 if filename.endswith(limited_doc_filename): 200 limited_doc_checks = True 201 break 202 203 lint_rules = self._lint_rules 204 lint_rules.Initialize(self, limited_doc_checks, is_html) 205 206 token = self._tokenizer.TokenizeFile(lines_iter) 207 208 parse_error = None 209 if self._metadata_pass: 210 try: 211 self._metadata_pass.Reset() 212 self._metadata_pass.Process(token) 213 except ecmametadatapass.ParseError, caught_parse_error: 214 if FLAGS.error_trace: 215 traceback.print_exc() 216 parse_error = caught_parse_error 217 except Exception: 218 print 'Internal error in %s' % filename 219 traceback.print_exc() 220 return False 221 222 self._error_handler.HandleFile(filename, token) 223 224 return self._CheckTokens(token, parse_error=parse_error, 225 debug_tokens=FLAGS.debug_tokens) 226 227 def _CheckTokens(self, token, parse_error, debug_tokens): 228 """Checks a token stream for lint warnings/errors. 229 230 Args: 231 token: The first token in the token stream to check. 232 parse_error: A ParseError if any errors occurred. 233 debug_tokens: Whether every token should be printed as it is encountered 234 during the pass. 235 236 Returns: 237 A boolean indicating whether the full token stream could be checked or if 238 checking failed prematurely. 239 """ 240 result = self._ExecutePass(token, self._LintPass, parse_error, debug_tokens) 241 242 if not result: 243 return False 244 245 self._lint_rules.Finalize(self._state_tracker, self._tokenizer.mode) 246 self._error_handler.FinishFile() 247 return True 248 249 def _LintPass(self, token): 250 """Checks an individual token for lint warnings/errors. 251 252 Used to encapsulate the logic needed to check an individual token so that it 253 can be passed to _ExecutePass. 254 255 Args: 256 token: The token to check. 257 """ 258 self._lint_rules.CheckToken(token, self._state_tracker) 259 260 def _ExecutePass(self, token, pass_function, parse_error=None, 261 debug_tokens=False): 262 """Calls the given function for every token in the given token stream. 263 264 As each token is passed to the given function, state is kept up to date and, 265 depending on the error_trace flag, errors are either caught and reported, or 266 allowed to bubble up so developers can see the full stack trace. If a parse 267 error is specified, the pass will proceed as normal until the token causing 268 the parse error is reached. 269 270 Args: 271 token: The first token in the token stream. 272 pass_function: The function to call for each token in the token stream. 273 parse_error: A ParseError if any errors occurred. 274 debug_tokens: Whether every token should be printed as it is encountered 275 during the pass. 276 277 Returns: 278 A boolean indicating whether the full token stream could be checked or if 279 checking failed prematurely. 280 281 Raises: 282 Exception: If any error occurred while calling the given function. 283 """ 284 self._state_tracker.Reset() 285 while token: 286 if debug_tokens: 287 print token 288 289 if parse_error and parse_error.token == token: 290 message = ('Error parsing file at token "%s". Unable to ' 291 'check the rest of file.' % token.string) 292 self.HandleError(errors.FILE_DOES_NOT_PARSE, message, token) 293 self._error_handler.FinishFile() 294 return 295 296 try: 297 self._state_tracker.HandleToken( 298 token, self._state_tracker.GetLastNonSpaceToken()) 299 pass_function(token) 300 self._state_tracker.HandleAfterToken(token) 301 except: 302 if FLAGS.error_trace: 303 raise 304 else: 305 self.HandleError(errors.FILE_DOES_NOT_PARSE, 306 ('Error parsing file at token "%s". Unable to ' 307 'check the rest of file.' % token.string), 308 token) 309 self._error_handler.FinishFile() 310 return False 311 token = token.next 312 return True 313