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

"""Core methods for checking EcmaScript files for common style guide violations.
"""

__author__ = ('robbyw@google.com (Robert Walker)',
              'ajp@google.com (Andy Perelson)',
              'jacobr@google.com (Jacob Richman)')

import re

from closure_linter import checkerbase
from closure_linter import ecmametadatapass
from closure_linter import error_check
from closure_linter import errors
from closure_linter import indentation
from closure_linter import javascripttokens
from closure_linter import javascripttokenizer
from closure_linter import statetracker
from closure_linter import tokenutil
from closure_linter.common import error
from closure_linter.common import htmlutil
from closure_linter.common import lintrunner
from closure_linter.common import position
from closure_linter.common import tokens
import gflags as flags

FLAGS = flags.FLAGS
flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow')

# TODO(robbyw): Check for extra parens on return statements
# TODO(robbyw): Check for 0px in strings
# TODO(robbyw): Ensure inline jsDoc is in {}
# TODO(robbyw): Check for valid JS types in parameter docs

# Shorthand
Context = ecmametadatapass.EcmaContext
Error = error.Error
Modes = javascripttokenizer.JavaScriptModes
Position = position.Position
Rule = error_check.Rule
Type = javascripttokens.JavaScriptTokenType

class EcmaScriptLintRules(checkerbase.LintRulesBase):
  """EmcaScript lint style checking rules.

  Can be used to find common style errors in JavaScript, ActionScript and other
  Ecma like scripting languages.  Style checkers for Ecma scripting languages
  should inherit from this style checker.
  Please do not add any state to EcmaScriptLintRules or to any subclasses.

  All state should be added to the StateTracker subclass used for a particular
  language.
  """

  # Static constants.
  MAX_LINE_LENGTH = 80

  MISSING_PARAMETER_SPACE = re.compile(r',\S')

  EXTRA_SPACE = re.compile('(\(\s|\s\))')

  ENDS_WITH_SPACE = re.compile('\s$')

  ILLEGAL_TAB = re.compile(r'\t')

  # Regex used to split up complex types to check for invalid use of ? and |.
  TYPE_SPLIT = re.compile(r'[,<>()]')

  # Regex for form of author lines after the @author tag.
  AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)')

  # Acceptable tokens to remove for line too long testing.
  LONG_LINE_IGNORE = frozenset(['*', '//', '@see'] +
      ['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE])

  def __init__(self):
    """Initialize this lint rule object."""
    checkerbase.LintRulesBase.__init__(self)

  def Initialize(self, checker, limited_doc_checks, is_html):
    """Initialize this lint rule object before parsing a new file."""
    checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks,
                                         is_html)
    self._indentation = indentation.IndentationRules()

  def HandleMissingParameterDoc(self, token, param_name):
    """Handle errors associated with a parameter missing a @param tag."""
    raise TypeError('Abstract method HandleMissingParameterDoc not implemented')

  def _CheckLineLength(self, last_token, state):
    """Checks whether the line is too long.

    Args:
      last_token: The last token in the line.
    """
    # Start from the last token so that we have the flag object attached to
    # and DOC_FLAG tokens.
    line_number = last_token.line_number
    token = last_token

    # Build a representation of the string where spaces indicate potential
    # line-break locations.
    line = []
    while token and token.line_number == line_number:
      if state.IsTypeToken(token):
        line.insert(0, 'x' * len(token.string))
      elif token.type in (Type.IDENTIFIER, Type.NORMAL):
        # Dots are acceptable places to wrap.
        line.insert(0, token.string.replace('.', ' '))
      else:
        line.insert(0, token.string)
      token = token.previous

    line = ''.join(line)
    line = line.rstrip('\n\r\f')
    try:
      length = len(unicode(line, 'utf-8'))
    except:
      # Unknown encoding. The line length may be wrong, as was originally the
      # case for utf-8 (see bug 1735846). For now just accept the default
      # length, but as we find problems we can either add test for other
      # possible encodings or return without an error to protect against
      # false positives at the cost of more false negatives.
      length = len(line)

    if length > self.MAX_LINE_LENGTH:

      # If the line matches one of the exceptions, then it's ok.
      for long_line_regexp in self.GetLongLineExceptions():
        if long_line_regexp.match(last_token.line):
          return

      # If the line consists of only one "word", or multiple words but all
      # except one are ignoreable, then it's ok.
      parts = set(line.split())

      # We allow two "words" (type and name) when the line contains @param
      max = 1
      if '@param' in parts:
        max = 2

      # Custom tags like @requires may have url like descriptions, so ignore
      # the tag, similar to how we handle @see.
      custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags])
      if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags)) > max):
        self._HandleError(errors.LINE_TOO_LONG,
            'Line too long (%d characters).' % len(line), last_token)

  def _CheckJsDocType(self, token):
    """Checks the given type for style errors.

    Args:
      token: The DOC_FLAG token for the flag whose type to check.
    """
    flag = token.attached_object
    type = flag.type
    if type and type is not None and not type.isspace():
      pieces = self.TYPE_SPLIT.split(type)
      if len(pieces) == 1 and type.count('|') == 1 and (
           type.endswith('|null') or type.startswith('null|')):
         self._HandleError(errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL,
             'Prefer "?Type" to "Type|null": "%s"' % type, token)

      for p in pieces:
        if p.count('|') and p.count('?'):
          # TODO(robbyw): We should do actual parsing of JsDoc types.  As is,
          # this won't report an error for {number|Array.<string>?}, etc.
          self._HandleError(errors.JSDOC_ILLEGAL_QUESTION_WITH_PIPE,
              'JsDoc types cannot contain both "?" and "|": "%s"' % p, token)

      if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and (
          flag.type_start_token.type != Type.DOC_START_BRACE or
          flag.type_end_token.type != Type.DOC_END_BRACE):
        self._HandleError(errors.MISSING_BRACES_AROUND_TYPE,
            'Type must always be surrounded by curly braces.', token)

  def _CheckForMissingSpaceBeforeToken(self, token):
    """Checks for a missing space at the beginning of a token.

    Reports a MISSING_SPACE error if the token does not begin with a space or
    the previous token doesn't end with a space and the previous token is on the
    same line as the token.

    Args:
      token: The token being checked
    """
    # TODO(user): Check if too many spaces?
    if (len(token.string) == len(token.string.lstrip()) and
        token.previous and token.line_number == token.previous.line_number and
        len(token.previous.string) - len(token.previous.string.rstrip()) == 0):
      self._HandleError(
          errors.MISSING_SPACE,
          'Missing space before "%s"' % token.string,
          token,
          Position.AtBeginning())

  def _ExpectSpaceBeforeOperator(self, token):
    """Returns whether a space should appear before the given operator token.

    Args:
      token: The operator token.

    Returns:
      Whether there should be a space before the token.
    """
    if token.string == ',' or token.metadata.IsUnaryPostOperator():
      return False

    # Colons should appear in labels, object literals, the case of a switch
    # statement, and ternary operator. Only want a space in the case of the
    # ternary operator.
    if (token.string == ':' and
        token.metadata.context.type in (Context.LITERAL_ELEMENT,
                                        Context.CASE_BLOCK,
                                        Context.STATEMENT)):
      return False

    if token.metadata.IsUnaryOperator() and token.IsFirstInLine():
      return False

    return True

  def CheckToken(self, token, state):
    """Checks a token, given the current parser_state, for warnings and errors.

    Args:
      token: The current token under consideration
      state: parser_state object that indicates the current state in the page
    """
    # Store some convenience variables
    first_in_line = token.IsFirstInLine()
    last_in_line = token.IsLastInLine()
    last_non_space_token = state.GetLastNonSpaceToken()

    type = token.type

    # Process the line change.
    if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION):
      # TODO(robbyw): Support checking indentation in HTML files.
      indentation_errors = self._indentation.CheckToken(token, state)
      for indentation_error in indentation_errors:
        self._HandleError(*indentation_error)

    if last_in_line:
      self._CheckLineLength(token, state)

    if type == Type.PARAMETERS:
      # Find missing spaces in parameter lists.
      if self.MISSING_PARAMETER_SPACE.search(token.string):
        self._HandleError(errors.MISSING_SPACE, 'Missing space after ","',
            token)

      # Find extra spaces at the beginning of parameter lists.  Make sure
      # we aren't at the beginning of a continuing multi-line list.
      if not first_in_line:
        space_count = len(token.string) - len(token.string.lstrip())
        if space_count:
          self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("',
              token, Position(0, space_count))

    elif (type == Type.START_BLOCK and
          token.metadata.context.type == Context.BLOCK):
      self._CheckForMissingSpaceBeforeToken(token)

    elif type == Type.END_BLOCK:
      # This check is for object literal end block tokens, but there is no need
      # to test that condition since a comma at the end of any other kind of
      # block is undoubtedly a parse error.
      last_code = token.metadata.last_code
      if last_code.IsOperator(','):
        self._HandleError(errors.COMMA_AT_END_OF_LITERAL,
            'Illegal comma at end of object literal', last_code,
            Position.All(last_code.string))

      if state.InFunction() and state.IsFunctionClose():
        is_immediately_called = (token.next and
                                 token.next.type == Type.START_PAREN)
        if state.InTopLevelFunction():
          # When the function was top-level and not immediately called, check
          # that it's terminated by a semi-colon.
          if state.InAssignedFunction():
            if not is_immediately_called and (last_in_line or
                not token.next.type == Type.SEMICOLON):
              self._HandleError(errors.MISSING_SEMICOLON_AFTER_FUNCTION,
                  'Missing semicolon after function assigned to a variable',
                  token, Position.AtEnd(token.string))
          else:
            if not last_in_line and token.next.type == Type.SEMICOLON:
              self._HandleError(errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION,
                  'Illegal semicolon after function declaration',
                  token.next, Position.All(token.next.string))

        if (state.InInterfaceMethod() and last_code.type != Type.START_BLOCK):
          self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE,
              'Interface methods cannot contain code', last_code)

      elif (state.IsBlockClose() and
            token.next and token.next.type == Type.SEMICOLON):
        self._HandleError(errors.REDUNDANT_SEMICOLON,
            'No semicolon is required to end a code block',
            token.next, Position.All(token.next.string))

    elif type == Type.SEMICOLON:
      if token.previous and token.previous.type == Type.WHITESPACE:
        self._HandleError(errors.EXTRA_SPACE, 'Extra space before ";"',
            token.previous, Position.All(token.previous.string))

      if token.next and token.next.line_number == token.line_number:
        if token.metadata.context.type != Context.FOR_GROUP_BLOCK:
          # TODO(robbyw): Error about no multi-statement lines.
          pass

        elif token.next.type not in (
            Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN):
          self._HandleError(errors.MISSING_SPACE,
              'Missing space after ";" in for statement',
              token.next,
              Position.AtBeginning())

      last_code = token.metadata.last_code
      if last_code and last_code.type == Type.SEMICOLON:
        # Allow a single double semi colon in for loops for cases like:
        # for (;;) { }.
        # NOTE(user): This is not a perfect check, and will not throw an error
        # for cases like: for (var i = 0;; i < n; i++) {}, but then your code
        # probably won't work either.
        for_token = tokenutil.CustomSearch(last_code,
            lambda token: token.type == Type.KEYWORD and token.string == 'for',
            end_func=lambda token: token.type == Type.SEMICOLON,
            distance=None,
            reverse=True)

        if not for_token:
          self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon',
              token, Position.All(token.string))

    elif type == Type.START_PAREN:
      if token.previous and token.previous.type == Type.KEYWORD:
        self._HandleError(errors.MISSING_SPACE, 'Missing space before "("',
            token, Position.AtBeginning())
      elif token.previous and token.previous.type == Type.WHITESPACE:
        before_space = token.previous.previous
        if (before_space and before_space.line_number == token.line_number and
            before_space.type == Type.IDENTIFIER):
          self._HandleError(errors.EXTRA_SPACE, 'Extra space before "("',
              token.previous, Position.All(token.previous.string))

    elif type == Type.START_BRACKET:
      self._HandleStartBracket(token, last_non_space_token)
    elif type in (Type.END_PAREN, Type.END_BRACKET):
      # Ensure there is no space before closing parentheses, except when
      # it's in a for statement with an omitted section, or when it's at the
      # beginning of a line.
      if (token.previous and token.previous.type == Type.WHITESPACE and
          not token.previous.IsFirstInLine() and
          not (last_non_space_token and last_non_space_token.line_number ==
                   token.line_number and
               last_non_space_token.type == Type.SEMICOLON)):
        self._HandleError(errors.EXTRA_SPACE, 'Extra space before "%s"' %
            token.string, token.previous, Position.All(token.previous.string))

      if token.type == Type.END_BRACKET:
        last_code = token.metadata.last_code
        if last_code.IsOperator(','):
          self._HandleError(errors.COMMA_AT_END_OF_LITERAL,
              'Illegal comma at end of array literal', last_code,
              Position.All(last_code.string))

    elif type == Type.WHITESPACE:
      if self.ILLEGAL_TAB.search(token.string):
        if token.IsFirstInLine():
          if token.next:
            self._HandleError(errors.ILLEGAL_TAB,
                'Illegal tab in whitespace before "%s"' % token.next.string,
                token, Position.All(token.string))
          else:
            self._HandleError(errors.ILLEGAL_TAB,
                'Illegal tab in whitespace',
                token, Position.All(token.string))
        else:
          self._HandleError(errors.ILLEGAL_TAB,
              'Illegal tab in whitespace after "%s"' % token.previous.string,
              token, Position.All(token.string))

      # Check whitespace length if it's not the first token of the line and
      # if it's not immediately before a comment.
      if last_in_line:
        # Check for extra whitespace at the end of a line.
        self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
            token, Position.All(token.string))
      elif not first_in_line and not token.next.IsComment():
        if token.length > 1:
          self._HandleError(errors.EXTRA_SPACE, 'Extra space after "%s"' %
              token.previous.string, token,
              Position(1, len(token.string) - 1))

    elif type == Type.OPERATOR:
      last_code = token.metadata.last_code

      if not self._ExpectSpaceBeforeOperator(token):
        if (token.previous and token.previous.type == Type.WHITESPACE and
            last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER)):
          self._HandleError(errors.EXTRA_SPACE,
              'Extra space before "%s"' % token.string, token.previous,
              Position.All(token.previous.string))

      elif (token.previous and
            not token.previous.IsComment() and
            token.previous.type in Type.EXPRESSION_ENDER_TYPES):
        self._HandleError(errors.MISSING_SPACE,
                          'Missing space before "%s"' % token.string, token,
                          Position.AtBeginning())

      # Check that binary operators are not used to start lines.
      if ((not last_code or last_code.line_number != token.line_number) and
          not token.metadata.IsUnaryOperator()):
        self._HandleError(errors.LINE_STARTS_WITH_OPERATOR,
            'Binary operator should go on previous line "%s"' % token.string,
            token)

    elif type == Type.DOC_FLAG:
      flag = token.attached_object

      if flag.flag_type == 'bug':
        # TODO(robbyw): Check for exactly 1 space on the left.
        string = token.next.string.lstrip()
        string = string.split(' ', 1)[0]

        if not string.isdigit():
          self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG,
              '@bug should be followed by a bug number', token)

      elif flag.flag_type == 'suppress':
        if flag.type is None:
          # A syntactically invalid suppress tag will get tokenized as a normal
          # flag, indicating an error.
          self._HandleError(errors.INCORRECT_SUPPRESS_SYNTAX,
              'Invalid suppress syntax: should be @suppress {errortype}. '
              'Spaces matter.', token)
        else:
          for suppress_type in flag.type.split('|'):
            if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES:
              self._HandleError(errors.INVALID_SUPPRESS_TYPE,
                'Invalid suppression type: %s' % suppress_type,
                token)

      elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and
            flag.flag_type == 'author'):
        # TODO(user): In non strict mode check the author tag for as much as
        # it exists, though the full form checked below isn't required.
        string = token.next.string
        result = self.AUTHOR_SPEC.match(string)
        if not result:
          self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION,
                            'Author tag line should be of the form: '
                            '@author foo@somewhere.com (Your Name)',
                            token.next)
        else:
          # Check spacing between email address and name. Do this before
          # checking earlier spacing so positions are easier to calculate for
          # autofixing.
          num_spaces = len(result.group(2))
          if num_spaces < 1:
            self._HandleError(errors.MISSING_SPACE,
                              'Missing space after email address',
                              token.next, Position(result.start(2), 0))
          elif num_spaces > 1:
            self._HandleError(errors.EXTRA_SPACE,
                              'Extra space after email address',
                              token.next,
                              Position(result.start(2) + 1, num_spaces - 1))

          # Check for extra spaces before email address. Can't be too few, if
          # not at least one we wouldn't match @author tag.
          num_spaces = len(result.group(1))
          if num_spaces > 1:
            self._HandleError(errors.EXTRA_SPACE,
                              'Extra space before email address',
                              token.next, Position(1, num_spaces - 1))

      elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and
            not self._limited_doc_checks):
        if flag.flag_type == 'param':
          if flag.name is None:
            self._HandleError(errors.MISSING_JSDOC_PARAM_NAME,
                'Missing name in @param tag', token)

        if not flag.description or flag.description is None:
          flag_name = token.type
          if 'name' in token.values:
            flag_name = '@' + token.values['name']
          self._HandleError(errors.MISSING_JSDOC_TAG_DESCRIPTION,
              'Missing description in %s tag' % flag_name, token)
        else:
          self._CheckForMissingSpaceBeforeToken(flag.description_start_token)

          # We want punctuation to be inside of any tags ending a description,
          # so strip tags before checking description. See bug 1127192. Note
          # that depending on how lines break, the real description end token
          # may consist only of stripped html and the effective end token can
          # be different.
          end_token = flag.description_end_token
          end_string = htmlutil.StripTags(end_token.string).strip()
          while (end_string == '' and not
                 end_token.type in Type.FLAG_ENDING_TYPES):
            end_token = end_token.previous
            if end_token.type in Type.FLAG_DESCRIPTION_TYPES:
              end_string = htmlutil.StripTags(end_token.string).rstrip()

          if not (end_string.endswith('.') or end_string.endswith('?') or
              end_string.endswith('!')):
            # Find the position for the missing punctuation, inside of any html
            # tags.
            desc_str = end_token.string.rstrip()
            while desc_str.endswith('>'):
              start_tag_index = desc_str.rfind('<')
              if start_tag_index < 0:
                break
              desc_str = desc_str[:start_tag_index].rstrip()
            end_position = Position(len(desc_str), 0)

            self._HandleError(
                errors.JSDOC_TAG_DESCRIPTION_ENDS_WITH_INVALID_CHARACTER,
                ('%s descriptions must end with valid punctuation such as a '
                 'period.' % token.string),
                end_token, end_position)

      if flag.flag_type in state.GetDocFlag().HAS_TYPE:
        if flag.type_start_token is not None:
          self._CheckForMissingSpaceBeforeToken(
              token.attached_object.type_start_token)

        if flag.type and flag.type != '' and not flag.type.isspace():
          self._CheckJsDocType(token)

    if type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
        if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and
            token.values['name'] not in FLAGS.custom_jsdoc_tags):
          self._HandleError(errors.INVALID_JSDOC_TAG,
              'Invalid JsDoc tag: %s' % token.values['name'], token)

        if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and
            token.values['name'] == 'inheritDoc' and
            type == Type.DOC_INLINE_FLAG):
          self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC,
              'Unnecessary braces around @inheritDoc',
              token)

    elif type == Type.SIMPLE_LVALUE:
      identifier = token.values['identifier']

      if ((not state.InFunction() or state.InConstructor()) and
          not state.InParentheses() and not state.InObjectLiteralDescendant()):
        jsdoc = state.GetDocComment()
        if not state.HasDocComment(identifier):
          # Only test for documentation on identifiers with .s in them to
          # avoid checking things like simple variables. We don't require
          # documenting assignments to .prototype itself (bug 1880803).
          if (not state.InConstructor() and
              identifier.find('.') != -1 and not
              identifier.endswith('.prototype') and not
              self._limited_doc_checks):
            comment = state.GetLastComment()
            if not (comment and comment.lower().count('jsdoc inherited')):
              self._HandleError(errors.MISSING_MEMBER_DOCUMENTATION,
                  "No docs found for member '%s'" % identifier,
                  token);
        elif jsdoc and (not state.InConstructor() or
                        identifier.startswith('this.')):
          # We are at the top level and the function/member is documented.
          if identifier.endswith('_') and not identifier.endswith('__'):
            # Can have a private class which inherits documentation from a
            # public superclass.
            #
            # @inheritDoc is deprecated in favor of using @override, and they
            if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor')
                and not ('accessControls' in jsdoc.suppressions)):
              self._HandleError(errors.INVALID_OVERRIDE_PRIVATE,
                  '%s should not override a private member.' % identifier,
                  jsdoc.GetFlag('override').flag_token)
            if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor')
                and not ('accessControls' in jsdoc.suppressions)):
              self._HandleError(errors.INVALID_INHERIT_DOC_PRIVATE,
                  '%s should not inherit from a private member.' % identifier,
                  jsdoc.GetFlag('inheritDoc').flag_token)
            if (not jsdoc.HasFlag('private') and
                not ('underscore' in jsdoc.suppressions) and not
                ((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and
                 ('accessControls' in jsdoc.suppressions))):
              self._HandleError(errors.MISSING_PRIVATE,
                  'Member "%s" must have @private JsDoc.' %
                  identifier, token)
            if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions:
              self._HandleError(errors.UNNECESSARY_SUPPRESS,
                  '@suppress {underscore} is not necessary with @private',
                  jsdoc.suppressions['underscore'])
          elif (jsdoc.HasFlag('private') and
                not self.InExplicitlyTypedLanguage()):
            # It is convention to hide public fields in some ECMA
            # implementations from documentation using the @private tag.
            self._HandleError(errors.EXTRA_PRIVATE,
                'Member "%s" must not have @private JsDoc' %
                identifier, token)

          # These flags are only legal on localizable message definitions;
          # such variables always begin with the prefix MSG_.
          for f in ('desc', 'hidden', 'meaning'):
            if (jsdoc.HasFlag(f)
              and not identifier.startswith('MSG_')
              and identifier.find('.MSG_') == -1):
              self._HandleError(errors.INVALID_USE_OF_DESC_TAG,
                  'Member "%s" should not have @%s JsDoc' % (identifier, f),
                  token)

      # Check for illegaly assigning live objects as prototype property values.
      index = identifier.find('.prototype.')
      # Ignore anything with additional .s after the prototype.
      if index != -1 and identifier.find('.', index + 11) == -1:
        equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
        next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES)
        if next_code and (
            next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or
            next_code.IsOperator('new')):
          self._HandleError(errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE,
              'Member %s cannot have a non-primitive value' % identifier,
              token)

    elif type == Type.END_PARAMETERS:
      # Find extra space at the end of parameter lists.  We check the token
      # prior to the current one when it is a closing paren.
      if (token.previous and token.previous.type == Type.PARAMETERS
          and self.ENDS_WITH_SPACE.search(token.previous.string)):
        self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"',
            token.previous)

      jsdoc = state.GetDocComment()
      if state.GetFunction().is_interface:
        if token.previous and token.previous.type == Type.PARAMETERS:
          self._HandleError(errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS,
              'Interface constructor cannot have parameters',
              token.previous)
      elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see')
          and not jsdoc.InheritsDocumentation()
          and not state.InObjectLiteralDescendant() and not
          jsdoc.IsInvalidated()):
        distance, edit = jsdoc.CompareParameters(state.GetParams())
        if distance:
          params_iter = iter(state.GetParams())
          docs_iter = iter(jsdoc.ordered_params)

          for op in edit:
            if op == 'I':
              # Insertion.
              # Parsing doc comments is the same for all languages
              # but some languages care about parameters that don't have
              # doc comments and some languages don't care.
              # Languages that don't allow variables to by typed such as
              # JavaScript care but languages such as ActionScript or Java
              # that allow variables to be typed don't care.
              if not self._limited_doc_checks:
                self.HandleMissingParameterDoc(token, params_iter.next())

            elif op == 'D':
              # Deletion
              self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION,
                  'Found docs for non-existing parameter: "%s"' %
                  docs_iter.next(), token)
            elif op == 'S':
              # Substitution
              if not self._limited_doc_checks:
                self._HandleError(errors.WRONG_PARAMETER_DOCUMENTATION,
                    'Parameter mismatch: got "%s", expected "%s"' %
                    (params_iter.next(), docs_iter.next()), token)

            else:
              # Equality - just advance the iterators
              params_iter.next()
              docs_iter.next()

    elif type == Type.STRING_TEXT:
      # If this is the first token after the start of the string, but it's at
      # the end of a line, we know we have a multi-line string.
      if token.previous.type in (Type.SINGLE_QUOTE_STRING_START,
          Type.DOUBLE_QUOTE_STRING_START) and last_in_line:
        self._HandleError(errors.MULTI_LINE_STRING,
            'Multi-line strings are not allowed', token)


    # This check is orthogonal to the ones above, and repeats some types, so
    # it is a plain if and not an elif.
    if token.type in Type.COMMENT_TYPES:
      if self.ILLEGAL_TAB.search(token.string):
        self._HandleError(errors.ILLEGAL_TAB,
            'Illegal tab in comment "%s"' % token.string, token)

      trimmed = token.string.rstrip()
      if last_in_line and token.string != trimmed:
        # Check for extra whitespace at the end of a line.
        self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line',
            token, Position(len(trimmed), len(token.string) - len(trimmed)))

    # This check is also orthogonal since it is based on metadata.
    if token.metadata.is_implied_semicolon:
      self._HandleError(errors.MISSING_SEMICOLON,
          'Missing semicolon at end of line', token)

  def _HandleStartBracket(self, token, last_non_space_token):
    """Handles a token that is an open bracket.

    Args:
      token: The token to handle.
      last_non_space_token: The last token that was not a space.
    """
    if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and
        last_non_space_token and
        last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES):
      self._HandleError(errors.EXTRA_SPACE, 'Extra space before "["',
                        token.previous, Position.All(token.previous.string))
    # If the [ token is the first token in a line we shouldn't complain
    # about a missing space before [.  This is because some Ecma script
    # languages allow syntax like:
    # [Annotation]
    # class MyClass {...}
    # So we don't want to blindly warn about missing spaces before [.
    # In the the future, when rules for computing exactly how many spaces
    # lines should be indented are added, then we can return errors for
    # [ tokens that are improperly indented.
    # For example:
    # var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName =
    # [a,b,c];
    # should trigger a proper indentation warning message as [ is not indented
    # by four spaces.
    elif (not token.IsFirstInLine() and token.previous and
          not token.previous.type in (
              [Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] +
              Type.EXPRESSION_ENDER_TYPES)):
      self._HandleError(errors.MISSING_SPACE, 'Missing space before "["',
                        token, Position.AtBeginning())

  def Finalize(self, state, tokenizer_mode):
    last_non_space_token = state.GetLastNonSpaceToken()
    # Check last line for ending with newline.
    if state.GetLastLine() and not (state.GetLastLine().isspace() or
        state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()):
      self._HandleError(
          errors.FILE_MISSING_NEWLINE,
          'File does not end with new line.  (%s)' % state.GetLastLine(),
          last_non_space_token)

    # Check that the mode is not mid comment, argument list, etc.
    if not tokenizer_mode == Modes.TEXT_MODE:
      self._HandleError(
          errors.FILE_IN_BLOCK,
          'File ended in mode "%s".' % tokenizer_mode,
          last_non_space_token)

    try:
      self._indentation.Finalize()
    except Exception, e:
      self._HandleError(
          errors.FILE_DOES_NOT_PARSE,
          str(e),
          last_non_space_token)

  def GetLongLineExceptions(self):
    """Gets a list of regexps for lines which can be longer than the limit."""
    return []

  def InExplicitlyTypedLanguage(self):
    """Returns whether this ecma implementation is explicitly typed."""
    return False
