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"""Core methods for checking EcmaScript files for common style guide violations.
18"""
19
20__author__ = ('robbyw@google.com (Robert Walker)',
21              'ajp@google.com (Andy Perelson)',
22              'jacobr@google.com (Jacob Richman)')
23
24import re
25
26from closure_linter import checkerbase
27from closure_linter import ecmametadatapass
28from closure_linter import error_check
29from closure_linter import errors
30from closure_linter import indentation
31from closure_linter import javascripttokens
32from closure_linter import javascripttokenizer
33from closure_linter import statetracker
34from closure_linter import tokenutil
35from closure_linter.common import error
36from closure_linter.common import htmlutil
37from closure_linter.common import lintrunner
38from closure_linter.common import position
39from closure_linter.common import tokens
40import gflags as flags
41
42FLAGS = flags.FLAGS
43flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow')
44
45# TODO(robbyw): Check for extra parens on return statements
46# TODO(robbyw): Check for 0px in strings
47# TODO(robbyw): Ensure inline jsDoc is in {}
48# TODO(robbyw): Check for valid JS types in parameter docs
49
50# Shorthand
51Context = ecmametadatapass.EcmaContext
52Error = error.Error
53Modes = javascripttokenizer.JavaScriptModes
54Position = position.Position
55Rule = error_check.Rule
56Type = javascripttokens.JavaScriptTokenType
57
58class EcmaScriptLintRules(checkerbase.LintRulesBase):
59  """EmcaScript lint style checking rules.
60
61  Can be used to find common style errors in JavaScript, ActionScript and other
62  Ecma like scripting languages.  Style checkers for Ecma scripting languages
63  should inherit from this style checker.
64  Please do not add any state to EcmaScriptLintRules or to any subclasses.
65
66  All state should be added to the StateTracker subclass used for a particular
67  language.
68  """
69
70  # Static constants.
71  MAX_LINE_LENGTH = 80
72
73  MISSING_PARAMETER_SPACE = re.compile(r',\S')
74
75  EXTRA_SPACE = re.compile('(\(\s|\s\))')
76
77  ENDS_WITH_SPACE = re.compile('\s$')
78
79  ILLEGAL_TAB = re.compile(r'\t')
80
81  # Regex used to split up complex types to check for invalid use of ? and |.
82  TYPE_SPLIT = re.compile(r'[,<>()]')
83
84  # Regex for form of author lines after the @author tag.
85  AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)')
86
87  # Acceptable tokens to remove for line too long testing.
88  LONG_LINE_IGNORE = frozenset(['*', '//', '@see'] +
89      ['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE])
90
91  def __init__(self):
92    """Initialize this lint rule object."""
93    checkerbase.LintRulesBase.__init__(self)
94
95  def Initialize(self, checker, limited_doc_checks, is_html):
96    """Initialize this lint rule object before parsing a new file."""
97    checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks,
98                                         is_html)
99    self._indentation = indentation.IndentationRules()
100
101  def HandleMissingParameterDoc(self, token, param_name):
102    """Handle errors associated with a parameter missing a @param tag."""
103    raise TypeError('Abstract method HandleMissingParameterDoc not implemented')
104
105  def _CheckLineLength(self, last_token, state):
106    """Checks whether the line is too long.
107
108    Args:
109      last_token: The last token in the line.
110    """
111    # Start from the last token so that we have the flag object attached to
112    # and DOC_FLAG tokens.
113    line_number = last_token.line_number
114    token = last_token
115
116    # Build a representation of the string where spaces indicate potential
117    # line-break locations.
118    line = []
119    while token and token.line_number == line_number:
120      if state.IsTypeToken(token):
121        line.insert(0, 'x' * len(token.string))
122      elif token.type in (Type.IDENTIFIER, Type.NORMAL):
123        # Dots are acceptable places to wrap.
124        line.insert(0, token.string.replace('.', ' '))
125      else:
126        line.insert(0, token.string)
127      token = token.previous
128
129    line = ''.join(line)
130    line = line.rstrip('\n\r\f')
131    try:
132      length = len(unicode(line, 'utf-8'))
133    except:
134      # Unknown encoding. The line length may be wrong, as was originally the
135      # case for utf-8 (see bug 1735846). For now just accept the default
136      # length, but as we find problems we can either add test for other
137      # possible encodings or return without an error to protect against
138      # false positives at the cost of more false negatives.
139      length = len(line)
140
141    if length > self.MAX_LINE_LENGTH:
142
143      # If the line matches one of the exceptions, then it's ok.
144      for long_line_regexp in self.GetLongLineExceptions():
145        if long_line_regexp.match(last_token.line):
146          return
147
148      # If the line consists of only one "word", or multiple words but all
149      # except one are ignoreable, then it's ok.
150      parts = set(line.split())
151
152      # We allow two "words" (type and name) when the line contains @param
153      max = 1
154      if '@param' in parts:
155        max = 2
156
157      # Custom tags like @requires may have url like descriptions, so ignore
158      # the tag, similar to how we handle @see.
159      custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags])
160      if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags)) > max):
161        self._HandleError(errors.LINE_TOO_LONG,
162            'Line too long (%d characters).' % len(line), last_token)
163
164  def _CheckJsDocType(self, token):
165    """Checks the given type for style errors.
166
167    Args:
168      token: The DOC_FLAG token for the flag whose type to check.
169    """
170    flag = token.attached_object
171    type = flag.type
172    if type and type is not None and not type.isspace():
173      pieces = self.TYPE_SPLIT.split(type)
174      if len(pieces) == 1 and type.count('|') == 1 and (
175           type.endswith('|null') or type.startswith('null|')):
176         self._HandleError(errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL,
177             'Prefer "?Type" to "Type|null": "%s"' % type, token)
178
179      for p in pieces:
180        if p.count('|') and p.count('?'):
181          # TODO(robbyw): We should do actual parsing of JsDoc types.  As is,
182          # this won't report an error for {number|Array.<string>?}, etc.
183          self._HandleError(errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
184              'JsDoc types cannot contain both "?" and "|": "%s"' % p, token)
185
186      if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and (
187          flag.type_start_token.type != Type.DOC_START_BRACE or
188          flag.type_end_token.type != Type.DOC_END_BRACE):
189        self._HandleError(errors.MISSING_BRACES_AROUND_TYPE,
190            'Type must always be surrounded by curly braces.', token)
191
192  def _CheckForMissingSpaceBeforeToken(self, token):
193    """Checks for a missing space at the beginning of a token.
194
195    Reports a MISSING_SPACE error if the token does not begin with a space or
196    the previous token doesn't end with a space and the previous token is on the
197    same line as the token.
198
199    Args:
200      token: The token being checked
201    """
202    # TODO(user): Check if too many spaces?
203    if (len(token.string) == len(token.string.lstrip()) and
204        token.previous and token.line_number == token.previous.line_number and
205        len(token.previous.string) - len(token.previous.string.rstrip()) == 0):
206      self._HandleError(
207          errors.MISSING_SPACE,
208          'Missing space before "%s"' % token.string,
209          token,
210          Position.AtBeginning())
211
212  def _ExpectSpaceBeforeOperator(self, token):
213    """Returns whether a space should appear before the given operator token.
214
215    Args:
216      token: The operator token.
217
218    Returns:
219      Whether there should be a space before the token.
220    """
221    if token.string == ',' or token.metadata.IsUnaryPostOperator():
222      return False
223
224    # Colons should appear in labels, object literals, the case of a switch
225    # statement, and ternary operator. Only want a space in the case of the
226    # ternary operator.
227    if (token.string == ':' and
228        token.metadata.context.type in (Context.LITERAL_ELEMENT,
229                                        Context.CASE_BLOCK,
230                                        Context.STATEMENT)):
231      return False
232
233    if token.metadata.IsUnaryOperator() and token.IsFirstInLine():
234      return False
235
236    return True
237
238  def CheckToken(self, token, state):
239    """Checks a token, given the current parser_state, for warnings and errors.
240
241    Args:
242      token: The current token under consideration
243      state: parser_state object that indicates the current state in the page
244    """
245    # Store some convenience variables
246    first_in_line = token.IsFirstInLine()
247    last_in_line = token.IsLastInLine()
248    last_non_space_token = state.GetLastNonSpaceToken()
249
250    type = token.type
251
252    # Process the line change.
253    if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION):
254      # TODO(robbyw): Support checking indentation in HTML files.
255      indentation_errors = self._indentation.CheckToken(token, state)
256      for indentation_error in indentation_errors:
257        self._HandleError(*indentation_error)
258
259    if last_in_line:
260      self._CheckLineLength(token, state)
261
262    if type == Type.PARAMETERS:
263      # Find missing spaces in parameter lists.
264      if self.MISSING_PARAMETER_SPACE.search(token.string):
265        self._HandleError(errors.MISSING_SPACE, 'Missing space after ","',
266            token)
267
268      # Find extra spaces at the beginning of parameter lists.  Make sure
269      # we aren't at the beginning of a continuing multi-line list.
270      if not first_in_line:
271        space_count = len(token.string) - len(token.string.lstrip())
272        if space_count:
273          self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("',
274              token, Position(0, space_count))
275
276    elif (type == Type.START_BLOCK and
277          token.metadata.context.type == Context.BLOCK):
278      self._CheckForMissingSpaceBeforeToken(token)
279
280    elif type == Type.END_BLOCK:
281      # This check is for object literal end block tokens, but there is no need
282      # to test that condition since a comma at the end of any other kind of
283      # block is undoubtedly a parse error.
284      last_code = token.metadata.last_code
285      if last_code.IsOperator(','):
286        self._HandleError(errors.COMMA_AT_END_OF_LITERAL,
287            'Illegal comma at end of object literal', last_code,
288            Position.All(last_code.string))
289
290      if state.InFunction() and state.IsFunctionClose():
291        is_immediately_called = (token.next and
292                                 token.next.type == Type.START_PAREN)
293        if state.InTopLevelFunction():
294          # When the function was top-level and not immediately called, check
295          # that it's terminated by a semi-colon.
296          if state.InAssignedFunction():
297            if not is_immediately_called and (last_in_line or
298                not token.next.type == Type.SEMICOLON):
299              self._HandleError(errors.MISSING_SEMICOLON_AFTER_FUNCTION,
300                  'Missing semicolon after function assigned to a variable',
301                  token, Position.AtEnd(token.string))
302          else:
303            if not last_in_line and token.next.type == Type.SEMICOLON:
304              self._HandleError(errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
305                  'Illegal semicolon after function declaration',
306                  token.next, Position.All(token.next.string))
307
308        if (state.InInterfaceMethod() and last_code.type != Type.START_BLOCK):
309          self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE,
310              'Interface methods cannot contain code', last_code)
311
312      elif (state.IsBlockClose() and
313            token.next and token.next.type == Type.SEMICOLON):
314        self._HandleError(errors.REDUNDANT_SEMICOLON,
315            'No semicolon is required to end a code block',
316            token.next, Position.All(token.next.string))
317
318    elif type == Type.SEMICOLON:
319      if token.previous and token.previous.type == Type.WHITESPACE:
320        self._HandleError(errors.EXTRA_SPACE, 'Extra space before ";"',
321            token.previous, Position.All(token.previous.string))
322
323      if token.next and token.next.line_number == token.line_number:
324        if token.metadata.context.type != Context.FOR_GROUP_BLOCK:
325          # TODO(robbyw): Error about no multi-statement lines.
326          pass
327
328        elif token.next.type not in (
329            Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN):
330          self._HandleError(errors.MISSING_SPACE,
331              'Missing space after ";" in for statement',
332              token.next,
333              Position.AtBeginning())
334
335      last_code = token.metadata.last_code
336      if last_code and last_code.type == Type.SEMICOLON:
337        # Allow a single double semi colon in for loops for cases like:
338        # for (;;) { }.
339        # NOTE(user): This is not a perfect check, and will not throw an error
340        # for cases like: for (var i = 0;; i < n; i++) {}, but then your code
341        # probably won't work either.
342        for_token = tokenutil.CustomSearch(last_code,
343            lambda token: token.type == Type.KEYWORD and token.string == 'for',
344            end_func=lambda token: token.type == Type.SEMICOLON,
345            distance=None,
346            reverse=True)
347
348        if not for_token:
349          self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon',
350              token, Position.All(token.string))
351
352    elif type == Type.START_PAREN:
353      if token.previous and token.previous.type == Type.KEYWORD:
354        self._HandleError(errors.MISSING_SPACE, 'Missing space before "("',
355            token, Position.AtBeginning())
356      elif token.previous and token.previous.type == Type.WHITESPACE:
357        before_space = token.previous.previous
358        if (before_space and before_space.line_number == token.line_number and
359            before_space.type == Type.IDENTIFIER):
360          self._HandleError(errors.EXTRA_SPACE, 'Extra space before "("',
361              token.previous, Position.All(token.previous.string))
362
363    elif type == Type.START_BRACKET:
364      self._HandleStartBracket(token, last_non_space_token)
365    elif type in (Type.END_PAREN, Type.END_BRACKET):
366      # Ensure there is no space before closing parentheses, except when
367      # it's in a for statement with an omitted section, or when it's at the
368      # beginning of a line.
369      if (token.previous and token.previous.type == Type.WHITESPACE and
370          not token.previous.IsFirstInLine() and
371          not (last_non_space_token and last_non_space_token.line_number ==
372                   token.line_number and
373               last_non_space_token.type == Type.SEMICOLON)):
374        self._HandleError(errors.EXTRA_SPACE, 'Extra space before "%s"' %
375            token.string, token.previous, Position.All(token.previous.string))
376
377      if token.type == Type.END_BRACKET:
378        last_code = token.metadata.last_code
379        if last_code.IsOperator(','):
380          self._HandleError(errors.COMMA_AT_END_OF_LITERAL,
381              'Illegal comma at end of array literal', last_code,
382              Position.All(last_code.string))
383
384    elif type == Type.WHITESPACE:
385      if self.ILLEGAL_TAB.search(token.string):
386        if token.IsFirstInLine():
387          if token.next:
388            self._HandleError(errors.ILLEGAL_TAB,
389                'Illegal tab in whitespace before "%s"' % token.next.string,
390                token, Position.All(token.string))
391          else:
392            self._HandleError(errors.ILLEGAL_TAB,
393                'Illegal tab in whitespace',
394                token, Position.All(token.string))
395        else:
396          self._HandleError(errors.ILLEGAL_TAB,
397              'Illegal tab in whitespace after "%s"' % token.previous.string,
398              token, Position.All(token.string))
399
400      # Check whitespace length if it's not the first token of the line and
401      # if it's not immediately before a comment.
402      if last_in_line:
403        # Check for extra whitespace at the end of a line.
404        self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
405            token, Position.All(token.string))
406      elif not first_in_line and not token.next.IsComment():
407        if token.length > 1:
408          self._HandleError(errors.EXTRA_SPACE, 'Extra space after "%s"' %
409              token.previous.string, token,
410              Position(1, len(token.string) - 1))
411
412    elif type == Type.OPERATOR:
413      last_code = token.metadata.last_code
414
415      if not self._ExpectSpaceBeforeOperator(token):
416        if (token.previous and token.previous.type == Type.WHITESPACE and
417            last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER)):
418          self._HandleError(errors.EXTRA_SPACE,
419              'Extra space before "%s"' % token.string, token.previous,
420              Position.All(token.previous.string))
421
422      elif (token.previous and
423            not token.previous.IsComment() and
424            token.previous.type in Type.EXPRESSION_ENDER_TYPES):
425        self._HandleError(errors.MISSING_SPACE,
426                          'Missing space before "%s"' % token.string, token,
427                          Position.AtBeginning())
428
429      # Check that binary operators are not used to start lines.
430      if ((not last_code or last_code.line_number != token.line_number) and
431          not token.metadata.IsUnaryOperator()):
432        self._HandleError(errors.LINE_STARTS_WITH_OPERATOR,
433            'Binary operator should go on previous line "%s"' % token.string,
434            token)
435
436    elif type == Type.DOC_FLAG:
437      flag = token.attached_object
438
439      if flag.flag_type == 'bug':
440        # TODO(robbyw): Check for exactly 1 space on the left.
441        string = token.next.string.lstrip()
442        string = string.split(' ', 1)[0]
443
444        if not string.isdigit():
445          self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG,
446              '@bug should be followed by a bug number', token)
447
448      elif flag.flag_type == 'suppress':
449        if flag.type is None:
450          # A syntactically invalid suppress tag will get tokenized as a normal
451          # flag, indicating an error.
452          self._HandleError(errors.INCORRECT_SUPPRESS_SYNTAX,
453              'Invalid suppress syntax: should be @suppress {errortype}. '
454              'Spaces matter.', token)
455        else:
456          for suppress_type in flag.type.split('|'):
457            if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES:
458              self._HandleError(errors.INVALID_SUPPRESS_TYPE,
459                'Invalid suppression type: %s' % suppress_type,
460                token)
461
462      elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and
463            flag.flag_type == 'author'):
464        # TODO(user): In non strict mode check the author tag for as much as
465        # it exists, though the full form checked below isn't required.
466        string = token.next.string
467        result = self.AUTHOR_SPEC.match(string)
468        if not result:
469          self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION,
470                            'Author tag line should be of the form: '
471                            '@author foo@somewhere.com (Your Name)',
472                            token.next)
473        else:
474          # Check spacing between email address and name. Do this before
475          # checking earlier spacing so positions are easier to calculate for
476          # autofixing.
477          num_spaces = len(result.group(2))
478          if num_spaces < 1:
479            self._HandleError(errors.MISSING_SPACE,
480                              'Missing space after email address',
481                              token.next, Position(result.start(2), 0))
482          elif num_spaces > 1:
483            self._HandleError(errors.EXTRA_SPACE,
484                              'Extra space after email address',
485                              token.next,
486                              Position(result.start(2) + 1, num_spaces - 1))
487
488          # Check for extra spaces before email address. Can't be too few, if
489          # not at least one we wouldn't match @author tag.
490          num_spaces = len(result.group(1))
491          if num_spaces > 1:
492            self._HandleError(errors.EXTRA_SPACE,
493                              'Extra space before email address',
494                              token.next, Position(1, num_spaces - 1))
495
496      elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and
497            not self._limited_doc_checks):
498        if flag.flag_type == 'param':
499          if flag.name is None:
500            self._HandleError(errors.MISSING_JSDOC_PARAM_NAME,
501                'Missing name in @param tag', token)
502
503        if not flag.description or flag.description is None:
504          flag_name = token.type
505          if 'name' in token.values:
506            flag_name = '@' + token.values['name']
507          self._HandleError(errors.MISSING_JSDOC_TAG_DESCRIPTION,
508              'Missing description in %s tag' % flag_name, token)
509        else:
510          self._CheckForMissingSpaceBeforeToken(flag.description_start_token)
511
512          # We want punctuation to be inside of any tags ending a description,
513          # so strip tags before checking description. See bug 1127192. Note
514          # that depending on how lines break, the real description end token
515          # may consist only of stripped html and the effective end token can
516          # be different.
517          end_token = flag.description_end_token
518          end_string = htmlutil.StripTags(end_token.string).strip()
519          while (end_string == '' and not
520                 end_token.type in Type.FLAG_ENDING_TYPES):
521            end_token = end_token.previous
522            if end_token.type in Type.FLAG_DESCRIPTION_TYPES:
523              end_string = htmlutil.StripTags(end_token.string).rstrip()
524
525          if not (end_string.endswith('.') or end_string.endswith('?') or
526              end_string.endswith('!')):
527            # Find the position for the missing punctuation, inside of any html
528            # tags.
529            desc_str = end_token.string.rstrip()
530            while desc_str.endswith('>'):
531              start_tag_index = desc_str.rfind('<')
532              if start_tag_index < 0:
533                break
534              desc_str = desc_str[:start_tag_index].rstrip()
535            end_position = Position(len(desc_str), 0)
536
537            self._HandleError(
538                errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER,
539                ('%s descriptions must end with valid punctuation such as a '
540                 'period.' % token.string),
541                end_token, end_position)
542
543      if flag.flag_type in state.GetDocFlag().HAS_TYPE:
544        if flag.type_start_token is not None:
545          self._CheckForMissingSpaceBeforeToken(
546              token.attached_object.type_start_token)
547
548        if flag.type and flag.type != '' and not flag.type.isspace():
549          self._CheckJsDocType(token)
550
551    if type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
552        if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and
553            token.values['name'] not in FLAGS.custom_jsdoc_tags):
554          self._HandleError(errors.INVALID_JSDOC_TAG,
555              'Invalid JsDoc tag: %s' % token.values['name'], token)
556
557        if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and
558            token.values['name'] == 'inheritDoc' and
559            type == Type.DOC_INLINE_FLAG):
560          self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC,
561              'Unnecessary braces around @inheritDoc',
562              token)
563
564    elif type == Type.SIMPLE_LVALUE:
565      identifier = token.values['identifier']
566
567      if ((not state.InFunction() or state.InConstructor()) and
568          not state.InParentheses() and not state.InObjectLiteralDescendant()):
569        jsdoc = state.GetDocComment()
570        if not state.HasDocComment(identifier):
571          # Only test for documentation on identifiers with .s in them to
572          # avoid checking things like simple variables. We don't require
573          # documenting assignments to .prototype itself (bug 1880803).
574          if (not state.InConstructor() and
575              identifier.find('.') != -1 and not
576              identifier.endswith('.prototype') and not
577              self._limited_doc_checks):
578            comment = state.GetLastComment()
579            if not (comment and comment.lower().count('jsdoc inherited')):
580              self._HandleError(errors.MISSING_MEMBER_DOCUMENTATION,
581                  "No docs found for member '%s'" % identifier,
582                  token);
583        elif jsdoc and (not state.InConstructor() or
584                        identifier.startswith('this.')):
585          # We are at the top level and the function/member is documented.
586          if identifier.endswith('_') and not identifier.endswith('__'):
587            # Can have a private class which inherits documentation from a
588            # public superclass.
589            #
590            # @inheritDoc is deprecated in favor of using @override, and they
591            if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor')
592                and not ('accessControls' in jsdoc.suppressions)):
593              self._HandleError(errors.INVALID_OVERRIDE_PRIVATE,
594                  '%s should not override a private member.' % identifier,
595                  jsdoc.GetFlag('override').flag_token)
596            if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor')
597                and not ('accessControls' in jsdoc.suppressions)):
598              self._HandleError(errors.INVALID_INHERIT_DOC_PRIVATE,
599                  '%s should not inherit from a private member.' % identifier,
600                  jsdoc.GetFlag('inheritDoc').flag_token)
601            if (not jsdoc.HasFlag('private') and
602                not ('underscore' in jsdoc.suppressions) and not
603                ((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and
604                 ('accessControls' in jsdoc.suppressions))):
605              self._HandleError(errors.MISSING_PRIVATE,
606                  'Member "%s" must have @private JsDoc.' %
607                  identifier, token)
608            if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions:
609              self._HandleError(errors.UNNECESSARY_SUPPRESS,
610                  '@suppress {underscore} is not necessary with @private',
611                  jsdoc.suppressions['underscore'])
612          elif (jsdoc.HasFlag('private') and
613                not self.InExplicitlyTypedLanguage()):
614            # It is convention to hide public fields in some ECMA
615            # implementations from documentation using the @private tag.
616            self._HandleError(errors.EXTRA_PRIVATE,
617                'Member "%s" must not have @private JsDoc' %
618                identifier, token)
619
620          # These flags are only legal on localizable message definitions;
621          # such variables always begin with the prefix MSG_.
622          for f in ('desc', 'hidden', 'meaning'):
623            if (jsdoc.HasFlag(f)
624              and not identifier.startswith('MSG_')
625              and identifier.find('.MSG_') == -1):
626              self._HandleError(errors.INVALID_USE_OF_DESC_TAG,
627                  'Member "%s" should not have @%s JsDoc' % (identifier, f),
628                  token)
629
630      # Check for illegaly assigning live objects as prototype property values.
631      index = identifier.find('.prototype.')
632      # Ignore anything with additional .s after the prototype.
633      if index != -1 and identifier.find('.', index + 11) == -1:
634        equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
635        next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES)
636        if next_code and (
637            next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or
638            next_code.IsOperator('new')):
639          self._HandleError(errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE,
640              'Member %s cannot have a non-primitive value' % identifier,
641              token)
642
643    elif type == Type.END_PARAMETERS:
644      # Find extra space at the end of parameter lists.  We check the token
645      # prior to the current one when it is a closing paren.
646      if (token.previous and token.previous.type == Type.PARAMETERS
647          and self.ENDS_WITH_SPACE.search(token.previous.string)):
648        self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"',
649            token.previous)
650
651      jsdoc = state.GetDocComment()
652      if state.GetFunction().is_interface:
653        if token.previous and token.previous.type == Type.PARAMETERS:
654          self._HandleError(errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS,
655              'Interface constructor cannot have parameters',
656              token.previous)
657      elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see')
658          and not jsdoc.InheritsDocumentation()
659          and not state.InObjectLiteralDescendant() and not
660          jsdoc.IsInvalidated()):
661        distance, edit = jsdoc.CompareParameters(state.GetParams())
662        if distance:
663          params_iter = iter(state.GetParams())
664          docs_iter = iter(jsdoc.ordered_params)
665
666          for op in edit:
667            if op == 'I':
668              # Insertion.
669              # Parsing doc comments is the same for all languages
670              # but some languages care about parameters that don't have
671              # doc comments and some languages don't care.
672              # Languages that don't allow variables to by typed such as
673              # JavaScript care but languages such as ActionScript or Java
674              # that allow variables to be typed don't care.
675              if not self._limited_doc_checks:
676                self.HandleMissingParameterDoc(token, params_iter.next())
677
678            elif op == 'D':
679              # Deletion
680              self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION,
681                  'Found docs for non-existing parameter: "%s"' %
682                  docs_iter.next(), token)
683            elif op == 'S':
684              # Substitution
685              if not self._limited_doc_checks:
686                self._HandleError(errors.WRONG_PARAMETER_DOCUMENTATION,
687                    'Parameter mismatch: got "%s", expected "%s"' %
688                    (params_iter.next(), docs_iter.next()), token)
689
690            else:
691              # Equality - just advance the iterators
692              params_iter.next()
693              docs_iter.next()
694
695    elif type == Type.STRING_TEXT:
696      # If this is the first token after the start of the string, but it's at
697      # the end of a line, we know we have a multi-line string.
698      if token.previous.type in (Type.SINGLE_QUOTE_STRING_START,
699          Type.DOUBLE_QUOTE_STRING_START) and last_in_line:
700        self._HandleError(errors.MULTI_LINE_STRING,
701            'Multi-line strings are not allowed', token)
702
703
704    # This check is orthogonal to the ones above, and repeats some types, so
705    # it is a plain if and not an elif.
706    if token.type in Type.COMMENT_TYPES:
707      if self.ILLEGAL_TAB.search(token.string):
708        self._HandleError(errors.ILLEGAL_TAB,
709            'Illegal tab in comment "%s"' % token.string, token)
710
711      trimmed = token.string.rstrip()
712      if last_in_line and token.string != trimmed:
713        # Check for extra whitespace at the end of a line.
714        self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
715            token, Position(len(trimmed), len(token.string) - len(trimmed)))
716
717    # This check is also orthogonal since it is based on metadata.
718    if token.metadata.is_implied_semicolon:
719      self._HandleError(errors.MISSING_SEMICOLON,
720          'Missing semicolon at end of line', token)
721
722  def _HandleStartBracket(self, token, last_non_space_token):
723    """Handles a token that is an open bracket.
724
725    Args:
726      token: The token to handle.
727      last_non_space_token: The last token that was not a space.
728    """
729    if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and
730        last_non_space_token and
731        last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES):
732      self._HandleError(errors.EXTRA_SPACE, 'Extra space before "["',
733                        token.previous, Position.All(token.previous.string))
734    # If the [ token is the first token in a line we shouldn't complain
735    # about a missing space before [.  This is because some Ecma script
736    # languages allow syntax like:
737    # [Annotation]
738    # class MyClass {...}
739    # So we don't want to blindly warn about missing spaces before [.
740    # In the the future, when rules for computing exactly how many spaces
741    # lines should be indented are added, then we can return errors for
742    # [ tokens that are improperly indented.
743    # For example:
744    # var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName =
745    # [a,b,c];
746    # should trigger a proper indentation warning message as [ is not indented
747    # by four spaces.
748    elif (not token.IsFirstInLine() and token.previous and
749          not token.previous.type in (
750              [Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] +
751              Type.EXPRESSION_ENDER_TYPES)):
752      self._HandleError(errors.MISSING_SPACE, 'Missing space before "["',
753                        token, Position.AtBeginning())
754
755  def Finalize(self, state, tokenizer_mode):
756    last_non_space_token = state.GetLastNonSpaceToken()
757    # Check last line for ending with newline.
758    if state.GetLastLine() and not (state.GetLastLine().isspace() or
759        state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()):
760      self._HandleError(
761          errors.FILE_MISSING_NEWLINE,
762          'File does not end with new line.  (%s)' % state.GetLastLine(),
763          last_non_space_token)
764
765    # Check that the mode is not mid comment, argument list, etc.
766    if not tokenizer_mode == Modes.TEXT_MODE:
767      self._HandleError(
768          errors.FILE_IN_BLOCK,
769          'File ended in mode "%s".' % tokenizer_mode,
770          last_non_space_token)
771
772    try:
773      self._indentation.Finalize()
774    except Exception, e:
775      self._HandleError(
776          errors.FILE_DOES_NOT_PARSE,
777          str(e),
778          last_non_space_token)
779
780  def GetLongLineExceptions(self):
781    """Gets a list of regexps for lines which can be longer than the limit."""
782    return []
783
784  def InExplicitlyTypedLanguage(self):
785    """Returns whether this ecma implementation is explicitly typed."""
786    return False
787