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