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