1#!/usr/bin/env python
2# Copyright 2011 The Closure Linter Authors. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS-IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Methods for checking JS files for common style guide violations.
17
18These style guide violations should only apply to JavaScript and not an Ecma
19scripting languages.
20"""
21
22__author__ = ('robbyw@google.com (Robert Walker)',
23              'ajp@google.com (Andy Perelson)',
24              'jacobr@google.com (Jacob Richman)')
25
26import re
27
28from closure_linter import ecmalintrules
29from closure_linter import error_check
30from closure_linter import errors
31from closure_linter import javascripttokenizer
32from closure_linter import javascripttokens
33from closure_linter import requireprovidesorter
34from closure_linter import tokenutil
35from closure_linter.common import error
36from closure_linter.common import position
37
38# Shorthand
39Error = error.Error
40Position = position.Position
41Rule = error_check.Rule
42Type = javascripttokens.JavaScriptTokenType
43
44
45class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules):
46  """JavaScript lint rules that catch JavaScript specific style errors."""
47
48  def __init__(self, namespaces_info):
49    """Initializes a JavaScriptLintRules instance."""
50    ecmalintrules.EcmaScriptLintRules.__init__(self)
51    self._namespaces_info = namespaces_info
52    self._declared_private_member_tokens = {}
53    self._declared_private_members = set()
54    self._used_private_members = set()
55    # A stack of dictionaries, one for each function scope entered. Each
56    # dictionary is keyed by an identifier that defines a local variable and has
57    # a token as its value.
58    self._unused_local_variables_by_scope = []
59
60  def HandleMissingParameterDoc(self, token, param_name):
61    """Handle errors associated with a parameter missing a param tag."""
62    self._HandleError(errors.MISSING_PARAMETER_DOCUMENTATION,
63                      'Missing docs for parameter: "%s"' % param_name, token)
64
65  # pylint: disable=too-many-statements
66  def CheckToken(self, token, state):
67    """Checks a token, given the current parser_state, for warnings and errors.
68
69    Args:
70      token: The current token under consideration
71      state: parser_state object that indicates the current state in the page
72    """
73
74    # Call the base class's CheckToken function.
75    super(JavaScriptLintRules, self).CheckToken(token, state)
76
77    # Store some convenience variables
78    namespaces_info = self._namespaces_info
79
80    if error_check.ShouldCheck(Rule.UNUSED_LOCAL_VARIABLES):
81      self._CheckUnusedLocalVariables(token, state)
82
83    if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS):
84      # Find all assignments to private members.
85      if token.type == Type.SIMPLE_LVALUE:
86        identifier = token.string
87        if identifier.endswith('_') and not identifier.endswith('__'):
88          doc_comment = state.GetDocComment()
89          suppressed = doc_comment and (
90              'underscore' in doc_comment.suppressions or
91              'unusedPrivateMembers' in doc_comment.suppressions)
92          if not suppressed:
93            # Look for static members defined on a provided namespace.
94            if namespaces_info:
95              namespace = namespaces_info.GetClosurizedNamespace(identifier)
96              provided_namespaces = namespaces_info.GetProvidedNamespaces()
97            else:
98              namespace = None
99              provided_namespaces = set()
100
101            # Skip cases of this.something_.somethingElse_.
102            regex = re.compile(r'^this\.[a-zA-Z_]+$')
103            if namespace in provided_namespaces or regex.match(identifier):
104              variable = identifier.split('.')[-1]
105              self._declared_private_member_tokens[variable] = token
106              self._declared_private_members.add(variable)
107        elif not identifier.endswith('__'):
108          # Consider setting public members of private members to be a usage.
109          for piece in identifier.split('.'):
110            if piece.endswith('_'):
111              self._used_private_members.add(piece)
112
113      # Find all usages of private members.
114      if token.type == Type.IDENTIFIER:
115        for piece in token.string.split('.'):
116          if piece.endswith('_'):
117            self._used_private_members.add(piece)
118
119    if token.type == Type.DOC_FLAG:
120      flag = token.attached_object
121
122      if flag.flag_type == 'param' and flag.name_token is not None:
123        self._CheckForMissingSpaceBeforeToken(
124            token.attached_object.name_token)
125
126        if flag.type is not None and flag.name is not None:
127          if error_check.ShouldCheck(Rule.VARIABLE_ARG_MARKER):
128            # Check for variable arguments marker in type.
129            if flag.jstype.IsVarArgsType() and flag.name != 'var_args':
130              self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_NAME,
131                                'Variable length argument %s must be renamed '
132                                'to var_args.' % flag.name,
133                                token)
134            elif not flag.jstype.IsVarArgsType() and flag.name == 'var_args':
135              self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_TYPE,
136                                'Variable length argument %s type must start '
137                                'with \'...\'.' % flag.name,
138                                token)
139
140          if error_check.ShouldCheck(Rule.OPTIONAL_TYPE_MARKER):
141            # Check for optional marker in type.
142            if (flag.jstype.opt_arg and
143                not flag.name.startswith('opt_')):
144              self._HandleError(errors.JSDOC_MISSING_OPTIONAL_PREFIX,
145                                'Optional parameter name %s must be prefixed '
146                                'with opt_.' % flag.name,
147                                token)
148            elif (not flag.jstype.opt_arg and
149                  flag.name.startswith('opt_')):
150              self._HandleError(errors.JSDOC_MISSING_OPTIONAL_TYPE,
151                                'Optional parameter %s type must end with =.' %
152                                flag.name,
153                                token)
154
155      if flag.flag_type in state.GetDocFlag().HAS_TYPE:
156        # Check for both missing type token and empty type braces '{}'
157        # Missing suppress types are reported separately and we allow enums,
158        # const, private, public and protected without types.
159        if (flag.flag_type not in state.GetDocFlag().CAN_OMIT_TYPE
160            and (not flag.jstype or flag.jstype.IsEmpty())):
161          self._HandleError(errors.MISSING_JSDOC_TAG_TYPE,
162                            'Missing type in %s tag' % token.string, token)
163
164        elif flag.name_token and flag.type_end_token and tokenutil.Compare(
165            flag.type_end_token, flag.name_token) > 0:
166          self._HandleError(
167              errors.OUT_OF_ORDER_JSDOC_TAG_TYPE,
168              'Type should be immediately after %s tag' % token.string,
169              token)
170
171    elif token.type == Type.DOUBLE_QUOTE_STRING_START:
172      next_token = token.next
173      while next_token.type == Type.STRING_TEXT:
174        if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search(
175            next_token.string):
176          break
177        next_token = next_token.next
178      else:
179        self._HandleError(
180            errors.UNNECESSARY_DOUBLE_QUOTED_STRING,
181            'Single-quoted string preferred over double-quoted string.',
182            token,
183            position=Position.All(token.string))
184
185    elif token.type == Type.END_DOC_COMMENT:
186      doc_comment = state.GetDocComment()
187
188      # When @externs appears in a @fileoverview comment, it should trigger
189      # the same limited doc checks as a special filename like externs.js.
190      if doc_comment.HasFlag('fileoverview') and doc_comment.HasFlag('externs'):
191        self._SetLimitedDocChecks(True)
192
193      if (error_check.ShouldCheck(Rule.BLANK_LINES_AT_TOP_LEVEL) and
194          not self._is_html and
195          state.InTopLevel() and
196          not state.InNonScopeBlock()):
197
198        # Check if we're in a fileoverview or constructor JsDoc.
199        is_constructor = (
200            doc_comment.HasFlag('constructor') or
201            doc_comment.HasFlag('interface'))
202        # @fileoverview is an optional tag so if the dosctring is the first
203        # token in the file treat it as a file level docstring.
204        is_file_level_comment = (
205            doc_comment.HasFlag('fileoverview') or
206            not doc_comment.start_token.previous)
207
208        # If the comment is not a file overview, and it does not immediately
209        # precede some code, skip it.
210        # NOTE: The tokenutil methods are not used here because of their
211        # behavior at the top of a file.
212        next_token = token.next
213        if (not next_token or
214            (not is_file_level_comment and
215             next_token.type in Type.NON_CODE_TYPES)):
216          return
217
218        # Don't require extra blank lines around suppression of extra
219        # goog.require errors.
220        if (doc_comment.SuppressionOnly() and
221            next_token.type == Type.IDENTIFIER and
222            next_token.string in ['goog.provide', 'goog.require']):
223          return
224
225        # Find the start of this block (include comments above the block, unless
226        # this is a file overview).
227        block_start = doc_comment.start_token
228        if not is_file_level_comment:
229          token = block_start.previous
230          while token and token.type in Type.COMMENT_TYPES:
231            block_start = token
232            token = token.previous
233
234        # Count the number of blank lines before this block.
235        blank_lines = 0
236        token = block_start.previous
237        while token and token.type in [Type.WHITESPACE, Type.BLANK_LINE]:
238          if token.type == Type.BLANK_LINE:
239            # A blank line.
240            blank_lines += 1
241          elif token.type == Type.WHITESPACE and not token.line.strip():
242            # A line with only whitespace on it.
243            blank_lines += 1
244          token = token.previous
245
246        # Log errors.
247        error_message = False
248        expected_blank_lines = 0
249
250        # Only need blank line before file overview if it is not the beginning
251        # of the file, e.g. copyright is first.
252        if is_file_level_comment and blank_lines == 0 and block_start.previous:
253          error_message = 'Should have a blank line before a file overview.'
254          expected_blank_lines = 1
255        elif is_constructor and blank_lines != 3:
256          error_message = (
257              'Should have 3 blank lines before a constructor/interface.')
258          expected_blank_lines = 3
259        elif (not is_file_level_comment and not is_constructor and
260              blank_lines != 2):
261          error_message = 'Should have 2 blank lines between top-level blocks.'
262          expected_blank_lines = 2
263
264        if error_message:
265          self._HandleError(
266              errors.WRONG_BLANK_LINE_COUNT, error_message,
267              block_start, position=Position.AtBeginning(),
268              fix_data=expected_blank_lines - blank_lines)
269
270    elif token.type == Type.END_BLOCK:
271      if state.InFunction() and state.IsFunctionClose():
272        is_immediately_called = (token.next and
273                                 token.next.type == Type.START_PAREN)
274
275        function = state.GetFunction()
276        if not self._limited_doc_checks:
277          if (function.has_return and function.doc and
278              not is_immediately_called and
279              not function.doc.HasFlag('return') and
280              not function.doc.InheritsDocumentation() and
281              not function.doc.HasFlag('constructor')):
282            # Check for proper documentation of return value.
283            self._HandleError(
284                errors.MISSING_RETURN_DOCUMENTATION,
285                'Missing @return JsDoc in function with non-trivial return',
286                function.doc.end_token, position=Position.AtBeginning())
287          elif (not function.has_return and
288                not function.has_throw and
289                function.doc and
290                function.doc.HasFlag('return') and
291                not state.InInterfaceMethod()):
292            flag = function.doc.GetFlag('return')
293            valid_no_return_names = ['undefined', 'void', '*']
294            invalid_return = flag.jstype is None or not any(
295                sub_type.identifier in valid_no_return_names
296                for sub_type in flag.jstype.IterTypeGroup())
297
298            if invalid_return:
299              self._HandleError(
300                  errors.UNNECESSARY_RETURN_DOCUMENTATION,
301                  'Found @return JsDoc on function that returns nothing',
302                  flag.flag_token, position=Position.AtBeginning())
303
304        # b/4073735. Method in object literal definition of prototype can
305        # safely reference 'this'.
306        prototype_object_literal = False
307        block_start = None
308        previous_code = None
309        previous_previous_code = None
310
311        # Search for cases where prototype is defined as object literal.
312        #       previous_previous_code
313        #       |       previous_code
314        #       |       | block_start
315        #       |       | |
316        # a.b.prototype = {
317        #   c : function() {
318        #     this.d = 1;
319        #   }
320        # }
321
322        # If in object literal, find first token of block so to find previous
323        # tokens to check above condition.
324        if state.InObjectLiteral():
325          block_start = state.GetCurrentBlockStart()
326
327        # If an object literal then get previous token (code type). For above
328        # case it should be '='.
329        if block_start:
330          previous_code = tokenutil.SearchExcept(block_start,
331                                                 Type.NON_CODE_TYPES,
332                                                 reverse=True)
333
334        # If previous token to block is '=' then get its previous token.
335        if previous_code and previous_code.IsOperator('='):
336          previous_previous_code = tokenutil.SearchExcept(previous_code,
337                                                          Type.NON_CODE_TYPES,
338                                                          reverse=True)
339
340        # If variable/token before '=' ends with '.prototype' then its above
341        # case of prototype defined with object literal.
342        prototype_object_literal = (previous_previous_code and
343                                    previous_previous_code.string.endswith(
344                                        '.prototype'))
345
346        if (function.has_this and function.doc and
347            not function.doc.HasFlag('this') and
348            not function.is_constructor and
349            not function.is_interface and
350            '.prototype.' not in function.name and
351            not prototype_object_literal):
352          self._HandleError(
353              errors.MISSING_JSDOC_TAG_THIS,
354              'Missing @this JsDoc in function referencing "this". ('
355              'this usually means you are trying to reference "this" in '
356              'a static function, or you have forgotten to mark a '
357              'constructor with @constructor)',
358              function.doc.end_token, position=Position.AtBeginning())
359
360    elif token.type == Type.IDENTIFIER:
361      if token.string == 'goog.inherits' and not state.InFunction():
362        if state.GetLastNonSpaceToken().line_number == token.line_number:
363          self._HandleError(
364              errors.MISSING_LINE,
365              'Missing newline between constructor and goog.inherits',
366              token,
367              position=Position.AtBeginning())
368
369        extra_space = state.GetLastNonSpaceToken().next
370        while extra_space != token:
371          if extra_space.type == Type.BLANK_LINE:
372            self._HandleError(
373                errors.EXTRA_LINE,
374                'Extra line between constructor and goog.inherits',
375                extra_space)
376          extra_space = extra_space.next
377
378        # TODO(robbyw): Test the last function was a constructor.
379        # TODO(robbyw): Test correct @extends and @implements documentation.
380
381      elif (token.string == 'goog.provide' and
382            not state.InFunction() and
383            namespaces_info is not None):
384        namespace = tokenutil.GetStringAfterToken(token)
385
386        # Report extra goog.provide statement.
387        if not namespace or namespaces_info.IsExtraProvide(token):
388          if not namespace:
389            msg = 'Empty namespace in goog.provide'
390          else:
391            msg = 'Unnecessary goog.provide: ' +  namespace
392
393            # Hint to user if this is a Test namespace.
394            if namespace.endswith('Test'):
395              msg += (' *Test namespaces must be mentioned in the '
396                      'goog.setTestOnly() call')
397
398          self._HandleError(
399              errors.EXTRA_GOOG_PROVIDE,
400              msg,
401              token, position=Position.AtBeginning())
402
403        if namespaces_info.IsLastProvide(token):
404          # Report missing provide statements after the last existing provide.
405          missing_provides = namespaces_info.GetMissingProvides()
406          if missing_provides:
407            self._ReportMissingProvides(
408                missing_provides,
409                tokenutil.GetLastTokenInSameLine(token).next,
410                False)
411
412          # If there are no require statements, missing requires should be
413          # reported after the last provide.
414          if not namespaces_info.GetRequiredNamespaces():
415            missing_requires, illegal_alias_statements = (
416                namespaces_info.GetMissingRequires())
417            if missing_requires:
418              self._ReportMissingRequires(
419                  missing_requires,
420                  tokenutil.GetLastTokenInSameLine(token).next,
421                  True)
422            if illegal_alias_statements:
423              self._ReportIllegalAliasStatement(illegal_alias_statements)
424
425      elif (token.string == 'goog.require' and
426            not state.InFunction() and
427            namespaces_info is not None):
428        namespace = tokenutil.GetStringAfterToken(token)
429
430        # If there are no provide statements, missing provides should be
431        # reported before the first require.
432        if (namespaces_info.IsFirstRequire(token) and
433            not namespaces_info.GetProvidedNamespaces()):
434          missing_provides = namespaces_info.GetMissingProvides()
435          if missing_provides:
436            self._ReportMissingProvides(
437                missing_provides,
438                tokenutil.GetFirstTokenInSameLine(token),
439                True)
440
441        # Report extra goog.require statement.
442        if not namespace or namespaces_info.IsExtraRequire(token):
443          if not namespace:
444            msg = 'Empty namespace in goog.require'
445          else:
446            msg = 'Unnecessary goog.require: ' + namespace
447
448          self._HandleError(
449              errors.EXTRA_GOOG_REQUIRE,
450              msg,
451              token, position=Position.AtBeginning())
452
453        # Report missing goog.require statements.
454        if namespaces_info.IsLastRequire(token):
455          missing_requires, illegal_alias_statements = (
456              namespaces_info.GetMissingRequires())
457          if missing_requires:
458            self._ReportMissingRequires(
459                missing_requires,
460                tokenutil.GetLastTokenInSameLine(token).next,
461                False)
462          if illegal_alias_statements:
463            self._ReportIllegalAliasStatement(illegal_alias_statements)
464
465    elif token.type == Type.OPERATOR:
466      last_in_line = token.IsLastInLine()
467      # If the token is unary and appears to be used in a unary context
468      # it's ok.  Otherwise, if it's at the end of the line or immediately
469      # before a comment, it's ok.
470      # Don't report an error before a start bracket - it will be reported
471      # by that token's space checks.
472      if (not token.metadata.IsUnaryOperator() and not last_in_line
473          and not token.next.IsComment()
474          and not token.next.IsOperator(',')
475          and not tokenutil.IsDot(token)
476          and token.next.type not in (Type.WHITESPACE, Type.END_PAREN,
477                                      Type.END_BRACKET, Type.SEMICOLON,
478                                      Type.START_BRACKET)):
479        self._HandleError(
480            errors.MISSING_SPACE,
481            'Missing space after "%s"' % token.string,
482            token,
483            position=Position.AtEnd(token.string))
484    elif token.type == Type.WHITESPACE:
485      first_in_line = token.IsFirstInLine()
486      last_in_line = token.IsLastInLine()
487      # Check whitespace length if it's not the first token of the line and
488      # if it's not immediately before a comment.
489      if not last_in_line and not first_in_line and not token.next.IsComment():
490        # Ensure there is no space after opening parentheses.
491        if (token.previous.type in (Type.START_PAREN, Type.START_BRACKET,
492                                    Type.FUNCTION_NAME)
493            or token.next.type == Type.START_PARAMETERS):
494          self._HandleError(
495              errors.EXTRA_SPACE,
496              'Extra space after "%s"' % token.previous.string,
497              token,
498              position=Position.All(token.string))
499    elif token.type == Type.SEMICOLON:
500      previous_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES,
501                                              reverse=True)
502      if not previous_token:
503        self._HandleError(
504            errors.REDUNDANT_SEMICOLON,
505            'Semicolon without any statement',
506            token,
507            position=Position.AtEnd(token.string))
508      elif (previous_token.type == Type.KEYWORD and
509            previous_token.string not in ['break', 'continue', 'return']):
510        self._HandleError(
511            errors.REDUNDANT_SEMICOLON,
512            ('Semicolon after \'%s\' without any statement.'
513             ' Looks like an error.' % previous_token.string),
514            token,
515            position=Position.AtEnd(token.string))
516
517  def _CheckUnusedLocalVariables(self, token, state):
518    """Checks for unused local variables in function blocks.
519
520    Args:
521      token: The token to check.
522      state: The state tracker.
523    """
524    # We don't use state.InFunction because that disregards scope functions.
525    in_function = state.FunctionDepth() > 0
526    if token.type == Type.SIMPLE_LVALUE or token.type == Type.IDENTIFIER:
527      if in_function:
528        identifier = token.string
529        # Check whether the previous token was var.
530        previous_code_token = tokenutil.CustomSearch(
531            token,
532            lambda t: t.type not in Type.NON_CODE_TYPES,
533            reverse=True)
534        if previous_code_token and previous_code_token.IsKeyword('var'):
535          # Add local variable declaration to the top of the unused locals
536          # stack.
537          self._unused_local_variables_by_scope[-1][identifier] = token
538        elif token.type == Type.IDENTIFIER:
539          # This covers most cases where the variable is used as an identifier.
540          self._MarkLocalVariableUsed(token.string)
541        elif token.type == Type.SIMPLE_LVALUE and '.' in identifier:
542          # This covers cases where a value is assigned to a property of the
543          # variable.
544          self._MarkLocalVariableUsed(token.string)
545    elif token.type == Type.START_BLOCK:
546      if in_function and state.IsFunctionOpen():
547        # Push a new map onto the stack
548        self._unused_local_variables_by_scope.append({})
549    elif token.type == Type.END_BLOCK:
550      if state.IsFunctionClose():
551        # Pop the stack and report any remaining locals as unused.
552        unused_local_variables = self._unused_local_variables_by_scope.pop()
553        for unused_token in unused_local_variables.values():
554          self._HandleError(
555              errors.UNUSED_LOCAL_VARIABLE,
556              'Unused local variable: %s.' % unused_token.string,
557              unused_token)
558    elif token.type == Type.DOC_FLAG:
559      # Flags that use aliased symbols should be counted.
560      flag = token.attached_object
561      js_type = flag and flag.jstype
562      if flag and flag.flag_type in state.GetDocFlag().HAS_TYPE and js_type:
563        self._MarkAliasUsed(js_type)
564
565  def _MarkAliasUsed(self, js_type):
566    """Marks aliases in a type as used.
567
568    Recursively iterates over all subtypes in a jsdoc type annotation and
569    tracks usage of aliased symbols (which may be local variables).
570    Marks the local variable as used in the scope nearest to the current
571    scope that matches the given token.
572
573    Args:
574      js_type: The jsdoc type, a typeannotation.TypeAnnotation object.
575    """
576    if js_type.alias:
577      self._MarkLocalVariableUsed(js_type.identifier)
578    for sub_type in js_type.IterTypes():
579      self._MarkAliasUsed(sub_type)
580
581  def _MarkLocalVariableUsed(self, identifier):
582    """Marks the local variable as used in the relevant scope.
583
584    Marks the local variable in the scope nearest to the current scope that
585    matches the given identifier as used.
586
587    Args:
588      identifier: The identifier representing the potential usage of a local
589                  variable.
590    """
591    identifier = identifier.split('.', 1)[0]
592    # Find the first instance of the identifier in the stack of function scopes
593    # and mark it used.
594    for unused_local_variables in reversed(
595        self._unused_local_variables_by_scope):
596      if identifier in unused_local_variables:
597        del unused_local_variables[identifier]
598        break
599
600  def _ReportMissingProvides(self, missing_provides, token, need_blank_line):
601    """Reports missing provide statements to the error handler.
602
603    Args:
604      missing_provides: A dictionary of string(key) and integer(value) where
605          each string(key) is a namespace that should be provided, but is not
606          and integer(value) is first line number where it's required.
607      token: The token where the error was detected (also where the new provides
608          will be inserted.
609      need_blank_line: Whether a blank line needs to be inserted after the new
610          provides are inserted. May be True, False, or None, where None
611          indicates that the insert location is unknown.
612    """
613
614    missing_provides_msg = 'Missing the following goog.provide statements:\n'
615    missing_provides_msg += '\n'.join(['goog.provide(\'%s\');' % x for x in
616                                       sorted(missing_provides)])
617    missing_provides_msg += '\n'
618
619    missing_provides_msg += '\nFirst line where provided: \n'
620    missing_provides_msg += '\n'.join(
621        ['  %s : line %d' % (x, missing_provides[x]) for x in
622         sorted(missing_provides)])
623    missing_provides_msg += '\n'
624
625    self._HandleError(
626        errors.MISSING_GOOG_PROVIDE,
627        missing_provides_msg,
628        token, position=Position.AtBeginning(),
629        fix_data=(missing_provides.keys(), need_blank_line))
630
631  def _ReportMissingRequires(self, missing_requires, token, need_blank_line):
632    """Reports missing require statements to the error handler.
633
634    Args:
635      missing_requires: A dictionary of string(key) and integer(value) where
636          each string(key) is a namespace that should be required, but is not
637          and integer(value) is first line number where it's required.
638      token: The token where the error was detected (also where the new requires
639          will be inserted.
640      need_blank_line: Whether a blank line needs to be inserted before the new
641          requires are inserted. May be True, False, or None, where None
642          indicates that the insert location is unknown.
643    """
644
645    missing_requires_msg = 'Missing the following goog.require statements:\n'
646    missing_requires_msg += '\n'.join(['goog.require(\'%s\');' % x for x in
647                                       sorted(missing_requires)])
648    missing_requires_msg += '\n'
649
650    missing_requires_msg += '\nFirst line where required: \n'
651    missing_requires_msg += '\n'.join(
652        ['  %s : line %d' % (x, missing_requires[x]) for x in
653         sorted(missing_requires)])
654    missing_requires_msg += '\n'
655
656    self._HandleError(
657        errors.MISSING_GOOG_REQUIRE,
658        missing_requires_msg,
659        token, position=Position.AtBeginning(),
660        fix_data=(missing_requires.keys(), need_blank_line))
661
662  def _ReportIllegalAliasStatement(self, illegal_alias_statements):
663    """Reports alias statements that would need a goog.require."""
664    for namespace, token in illegal_alias_statements.iteritems():
665      self._HandleError(
666          errors.ALIAS_STMT_NEEDS_GOOG_REQUIRE,
667          'The alias definition would need the namespace \'%s\' which is not '
668          'required through any other symbol.' % namespace,
669          token, position=Position.AtBeginning())
670
671  def Finalize(self, state):
672    """Perform all checks that need to occur after all lines are processed."""
673    # Call the base class's Finalize function.
674    super(JavaScriptLintRules, self).Finalize(state)
675
676    if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS):
677      # Report an error for any declared private member that was never used.
678      unused_private_members = (self._declared_private_members -
679                                self._used_private_members)
680
681      for variable in unused_private_members:
682        token = self._declared_private_member_tokens[variable]
683        self._HandleError(errors.UNUSED_PRIVATE_MEMBER,
684                          'Unused private member: %s.' % token.string,
685                          token)
686
687      # Clear state to prepare for the next file.
688      self._declared_private_member_tokens = {}
689      self._declared_private_members = set()
690      self._used_private_members = set()
691
692    namespaces_info = self._namespaces_info
693    if namespaces_info is not None:
694      # If there are no provide or require statements, missing provides and
695      # requires should be reported on line 1.
696      if (not namespaces_info.GetProvidedNamespaces() and
697          not namespaces_info.GetRequiredNamespaces()):
698        missing_provides = namespaces_info.GetMissingProvides()
699        if missing_provides:
700          self._ReportMissingProvides(
701              missing_provides, state.GetFirstToken(), None)
702
703        missing_requires, illegal_alias = namespaces_info.GetMissingRequires()
704        if missing_requires:
705          self._ReportMissingRequires(
706              missing_requires, state.GetFirstToken(), None)
707        if illegal_alias:
708          self._ReportIllegalAliasStatement(illegal_alias)
709
710    self._CheckSortedRequiresProvides(state.GetFirstToken())
711
712  def _CheckSortedRequiresProvides(self, token):
713    """Checks that all goog.require and goog.provide statements are sorted.
714
715    Note that this method needs to be run after missing statements are added to
716    preserve alphabetical order.
717
718    Args:
719      token: The first token in the token stream.
720    """
721    sorter = requireprovidesorter.RequireProvideSorter()
722    first_provide_token = sorter.CheckProvides(token)
723    if first_provide_token:
724      new_order = sorter.GetFixedProvideString(first_provide_token)
725      self._HandleError(
726          errors.GOOG_PROVIDES_NOT_ALPHABETIZED,
727          'goog.provide classes must be alphabetized.  The correct code is:\n' +
728          new_order,
729          first_provide_token,
730          position=Position.AtBeginning(),
731          fix_data=first_provide_token)
732
733    first_require_token = sorter.CheckRequires(token)
734    if first_require_token:
735      new_order = sorter.GetFixedRequireString(first_require_token)
736      self._HandleError(
737          errors.GOOG_REQUIRES_NOT_ALPHABETIZED,
738          'goog.require classes must be alphabetized.  The correct code is:\n' +
739          new_order,
740          first_require_token,
741          position=Position.AtBeginning(),
742          fix_data=first_require_token)
743
744  def GetLongLineExceptions(self):
745    """Gets a list of regexps for lines which can be longer than the limit.
746
747    Returns:
748      A list of regexps, used as matches (rather than searches).
749    """
750    return [
751        re.compile(r'.*// @suppress longLineCheck$'),
752        re.compile(r'((var|let|const) .+\s*=\s*)?goog\.require\(.+\);?\s*$'),
753        re.compile(r'goog\.(forwardDeclare|module|provide|setTestOnly)'
754                   r'\(.+\);?\s*$'),
755        re.compile(r'[\s/*]*@visibility\s*{.*}[\s*/]*$'),
756        ]
757