1#!/usr/bin/env python
2#
3# Copyright 2007 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"""Light weight EcmaScript state tracker that reads tokens and tracks state."""
18
19__author__ = ('robbyw@google.com (Robert Walker)',
20              'ajp@google.com (Andy Perelson)')
21
22import re
23
24from closure_linter import javascripttokenizer
25from closure_linter import javascripttokens
26from closure_linter import tokenutil
27
28# Shorthand
29Type = javascripttokens.JavaScriptTokenType
30
31
32class DocFlag(object):
33  """Generic doc flag object.
34
35  Attribute:
36    flag_type: param, return, define, type, etc.
37    flag_token: The flag token.
38    type_start_token: The first token specifying the flag type,
39      including braces.
40    type_end_token: The last token specifying the flag type,
41      including braces.
42    type: The type spec.
43    name_token: The token specifying the flag name.
44    name: The flag name
45    description_start_token: The first token in the description.
46    description_end_token: The end token in the description.
47    description: The description.
48  """
49
50  # Please keep these lists alphabetized.
51
52  # The list of standard jsdoc tags is from
53  STANDARD_DOC = frozenset([
54      'author',
55      'bug',
56      'const',
57      'constructor',
58      'define',
59      'deprecated',
60      'enum',
61      'export',
62      'extends',
63      'externs',
64      'fileoverview',
65      'implements',
66      'implicitCast',
67      'interface',
68      'lends',
69      'license',
70      'noalias',
71      'nocompile',
72      'nosideeffects',
73      'override',
74      'owner',
75      'param',
76      'preserve',
77      'private',
78      'return',
79      'see',
80      'supported',
81      'template',
82      'this',
83      'type',
84      'typedef',
85      ])
86
87  ANNOTATION = frozenset(['preserveTry', 'suppress'])
88
89  LEGAL_DOC = STANDARD_DOC | ANNOTATION
90
91  # Includes all Closure Compiler @suppress types.
92  # Not all of these annotations are interpreted by Closure Linter.
93  #
94  # Specific cases:
95  # - accessControls is supported by the compiler at the expression
96  #   and method level to suppress warnings about private/protected
97  #   access (method level applies to all references in the method).
98  #   The linter mimics the compiler behavior.
99  SUPPRESS_TYPES = frozenset([
100      'accessControls',
101      'ambiguousFunctionDecl',
102      'checkRegExp',
103      'checkTypes',
104      'checkVars',
105      'const',
106      'constantProperty',
107      'deprecated',
108      'duplicate',
109      'es5Strict',
110      'externsValidation',
111      'extraProvide',
112      'extraRequire',
113      'fileoverviewTags',
114      'globalThis',
115      'internetExplorerChecks',
116      'invalidCasts',
117      'missingProperties',
118      'missingProvide',
119      'missingRequire',
120      'nonStandardJsDocs',
121      'strictModuleDepCheck',
122      'tweakValidation',
123      'typeInvalidation',
124      'undefinedNames',
125      'undefinedVars',
126      'underscore',
127      'unknownDefines',
128      'uselessCode',
129      'visibility',
130      'with'])
131
132  HAS_DESCRIPTION = frozenset([
133    'define', 'deprecated', 'desc', 'fileoverview', 'license', 'param',
134    'preserve', 'return', 'supported'])
135
136  HAS_TYPE = frozenset([
137      'define', 'enum', 'extends', 'implements', 'param', 'return', 'type',
138      'suppress'])
139
140  TYPE_ONLY = frozenset(['enum', 'extends', 'implements',  'suppress', 'type'])
141
142  HAS_NAME = frozenset(['param'])
143
144  EMPTY_COMMENT_LINE = re.compile(r'^\s*\*?\s*$')
145  EMPTY_STRING = re.compile(r'^\s*$')
146
147  def __init__(self, flag_token):
148    """Creates the DocFlag object and attaches it to the given start token.
149
150    Args:
151      flag_token: The starting token of the flag.
152    """
153    self.flag_token = flag_token
154    self.flag_type = flag_token.string.strip().lstrip('@')
155
156    # Extract type, if applicable.
157    self.type = None
158    self.type_start_token = None
159    self.type_end_token = None
160    if self.flag_type in self.HAS_TYPE:
161      brace = tokenutil.SearchUntil(flag_token, [Type.DOC_START_BRACE],
162                                    Type.FLAG_ENDING_TYPES)
163      if brace:
164        end_token, contents = _GetMatchingEndBraceAndContents(brace)
165        self.type = contents
166        self.type_start_token = brace
167        self.type_end_token = end_token
168      elif (self.flag_type in self.TYPE_ONLY and
169          flag_token.next.type not in Type.FLAG_ENDING_TYPES):
170        self.type_start_token = flag_token.next
171        self.type_end_token, self.type = _GetEndTokenAndContents(
172            self.type_start_token)
173        if self.type is not None:
174          self.type = self.type.strip()
175
176    # Extract name, if applicable.
177    self.name_token = None
178    self.name = None
179    if self.flag_type in self.HAS_NAME:
180      # Handle bad case, name could be immediately after flag token.
181      self.name_token = _GetNextIdentifierToken(flag_token)
182
183      # Handle good case, if found token is after type start, look for
184      # identifier after type end, since types contain identifiers.
185      if (self.type and self.name_token and
186          tokenutil.Compare(self.name_token, self.type_start_token) > 0):
187        self.name_token = _GetNextIdentifierToken(self.type_end_token)
188
189      if self.name_token:
190        self.name = self.name_token.string
191
192    # Extract description, if applicable.
193    self.description_start_token = None
194    self.description_end_token = None
195    self.description = None
196    if self.flag_type in self.HAS_DESCRIPTION:
197      search_start_token = flag_token
198      if self.name_token and self.type_end_token:
199        if tokenutil.Compare(self.type_end_token, self.name_token) > 0:
200          search_start_token = self.type_end_token
201        else:
202          search_start_token = self.name_token
203      elif self.name_token:
204        search_start_token = self.name_token
205      elif self.type:
206        search_start_token = self.type_end_token
207
208      interesting_token = tokenutil.Search(search_start_token,
209          Type.FLAG_DESCRIPTION_TYPES | Type.FLAG_ENDING_TYPES)
210      if interesting_token.type in Type.FLAG_DESCRIPTION_TYPES:
211        self.description_start_token = interesting_token
212        self.description_end_token, self.description = (
213            _GetEndTokenAndContents(interesting_token))
214
215
216class DocComment(object):
217  """JavaScript doc comment object.
218
219  Attributes:
220    ordered_params: Ordered list of parameters documented.
221    start_token: The token that starts the doc comment.
222    end_token: The token that ends the doc comment.
223    suppressions: Map of suppression type to the token that added it.
224  """
225  def __init__(self, start_token):
226    """Create the doc comment object.
227
228    Args:
229      start_token: The first token in the doc comment.
230    """
231    self.__params = {}
232    self.ordered_params = []
233    self.__flags = {}
234    self.start_token = start_token
235    self.end_token = None
236    self.suppressions = {}
237    self.invalidated = False
238
239  def Invalidate(self):
240    """Indicate that the JSDoc is well-formed but we had problems parsing it.
241
242    This is a short-circuiting mechanism so that we don't emit false
243    positives about well-formed doc comments just because we don't support
244    hot new syntaxes.
245    """
246    self.invalidated = True
247
248  def IsInvalidated(self):
249    """Test whether Invalidate() has been called."""
250    return self.invalidated
251
252  def AddParam(self, name, param_type):
253    """Add a new documented parameter.
254
255    Args:
256      name: The name of the parameter to document.
257      param_type: The parameter's declared JavaScript type.
258    """
259    self.ordered_params.append(name)
260    self.__params[name] = param_type
261
262  def AddSuppression(self, token):
263    """Add a new error suppression flag.
264
265    Args:
266      token: The suppression flag token.
267    """
268    #TODO(user): Error if no braces
269    brace = tokenutil.SearchUntil(token, [Type.DOC_START_BRACE],
270                                  [Type.DOC_FLAG])
271    if brace:
272      end_token, contents = _GetMatchingEndBraceAndContents(brace)
273      for suppression in contents.split('|'):
274        self.suppressions[suppression] = token
275
276  def SuppressionOnly(self):
277    """Returns whether this comment contains only suppression flags."""
278    for flag_type in self.__flags.keys():
279      if flag_type != 'suppress':
280        return False
281    return True
282
283  def AddFlag(self, flag):
284    """Add a new document flag.
285
286    Args:
287      flag: DocFlag object.
288    """
289    self.__flags[flag.flag_type] = flag
290
291  def InheritsDocumentation(self):
292    """Test if the jsdoc implies documentation inheritance.
293
294    Returns:
295        True if documentation may be pulled off the superclass.
296    """
297    return self.HasFlag('inheritDoc') or self.HasFlag('override')
298
299  def HasFlag(self, flag_type):
300    """Test if the given flag has been set.
301
302    Args:
303      flag_type: The type of the flag to check.
304
305    Returns:
306      True if the flag is set.
307    """
308    return flag_type in self.__flags
309
310  def GetFlag(self, flag_type):
311    """Gets the last flag of the given type.
312
313    Args:
314      flag_type: The type of the flag to get.
315
316    Returns:
317      The last instance of the given flag type in this doc comment.
318    """
319    return self.__flags[flag_type]
320
321  def CompareParameters(self, params):
322    """Computes the edit distance and list from the function params to the docs.
323
324    Uses the Levenshtein edit distance algorithm, with code modified from
325    http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#Python
326
327    Args:
328      params: The parameter list for the function declaration.
329
330    Returns:
331      The edit distance, the edit list.
332    """
333    source_len, target_len = len(self.ordered_params), len(params)
334    edit_lists = [[]]
335    distance = [[]]
336    for i in range(target_len+1):
337      edit_lists[0].append(['I'] * i)
338      distance[0].append(i)
339
340    for j in range(1, source_len+1):
341      edit_lists.append([['D'] * j])
342      distance.append([j])
343
344    for i in range(source_len):
345      for j in range(target_len):
346        cost = 1
347        if self.ordered_params[i] == params[j]:
348          cost = 0
349
350        deletion = distance[i][j+1] + 1
351        insertion = distance[i+1][j] + 1
352        substitution = distance[i][j] + cost
353
354        edit_list = None
355        best = None
356        if deletion <= insertion and deletion <= substitution:
357          # Deletion is best.
358          best = deletion
359          edit_list = list(edit_lists[i][j+1])
360          edit_list.append('D')
361
362        elif insertion <= substitution:
363          # Insertion is best.
364          best = insertion
365          edit_list = list(edit_lists[i+1][j])
366          edit_list.append('I')
367          edit_lists[i+1].append(edit_list)
368
369        else:
370          # Substitution is best.
371          best = substitution
372          edit_list = list(edit_lists[i][j])
373          if cost:
374            edit_list.append('S')
375          else:
376            edit_list.append('=')
377
378        edit_lists[i+1].append(edit_list)
379        distance[i+1].append(best)
380
381    return distance[source_len][target_len], edit_lists[source_len][target_len]
382
383  def __repr__(self):
384    """Returns a string representation of this object.
385
386    Returns:
387      A string representation of this object.
388    """
389    return '<DocComment: %s, %s>' % (str(self.__params), str(self.__flags))
390
391
392#
393# Helper methods used by DocFlag and DocComment to parse out flag information.
394#
395
396
397def _GetMatchingEndBraceAndContents(start_brace):
398  """Returns the matching end brace and contents between the two braces.
399
400  If any FLAG_ENDING_TYPE token is encountered before a matching end brace, then
401  that token is used as the matching ending token. Contents will have all
402  comment prefixes stripped out of them, and all comment prefixes in between the
403  start and end tokens will be split out into separate DOC_PREFIX tokens.
404
405  Args:
406    start_brace: The DOC_START_BRACE token immediately before desired contents.
407
408  Returns:
409    The matching ending token (DOC_END_BRACE or FLAG_ENDING_TYPE) and a string
410    of the contents between the matching tokens, minus any comment prefixes.
411  """
412  open_count = 1
413  close_count = 0
414  contents = []
415
416  # We don't consider the start brace part of the type string.
417  token = start_brace.next
418  while open_count != close_count:
419    if token.type == Type.DOC_START_BRACE:
420      open_count += 1
421    elif token.type == Type.DOC_END_BRACE:
422      close_count += 1
423
424    if token.type != Type.DOC_PREFIX:
425      contents.append(token.string)
426
427    if token.type in Type.FLAG_ENDING_TYPES:
428      break
429    token = token.next
430
431  #Don't include the end token (end brace, end doc comment, etc.) in type.
432  token = token.previous
433  contents = contents[:-1]
434
435  return token, ''.join(contents)
436
437
438def _GetNextIdentifierToken(start_token):
439  """Searches for and returns the first identifier at the beginning of a token.
440
441  Searches each token after the start to see if it starts with an identifier.
442  If found, will split the token into at most 3 piecies: leading whitespace,
443  identifier, rest of token, returning the identifier token. If no identifier is
444  found returns None and changes no tokens. Search is abandoned when a
445  FLAG_ENDING_TYPE token is found.
446
447  Args:
448    start_token: The token to start searching after.
449
450  Returns:
451    The identifier token is found, None otherwise.
452  """
453  token = start_token.next
454
455  while token and not token.type in Type.FLAG_ENDING_TYPES:
456    match = javascripttokenizer.JavaScriptTokenizer.IDENTIFIER.match(
457        token.string)
458    if (match is not None and token.type == Type.COMMENT and
459        len(token.string) == len(match.group(0))):
460      return token
461
462    token = token.next
463
464  return None
465
466
467def _GetEndTokenAndContents(start_token):
468  """Returns last content token and all contents before FLAG_ENDING_TYPE token.
469
470  Comment prefixes are split into DOC_PREFIX tokens and stripped from the
471  returned contents.
472
473  Args:
474    start_token: The token immediately before the first content token.
475
476  Returns:
477    The last content token and a string of all contents including start and
478    end tokens, with comment prefixes stripped.
479  """
480  iterator = start_token
481  last_line = iterator.line_number
482  last_token = None
483  contents = ''
484  doc_depth = 0
485  while not iterator.type in Type.FLAG_ENDING_TYPES or doc_depth > 0:
486    if (iterator.IsFirstInLine() and
487        DocFlag.EMPTY_COMMENT_LINE.match(iterator.line)):
488      # If we have a blank comment line, consider that an implicit
489      # ending of the description. This handles a case like:
490      #
491      # * @return {boolean} True
492      # *
493      # * Note: This is a sentence.
494      #
495      # The note is not part of the @return description, but there was
496      # no definitive ending token. Rather there was a line containing
497      # only a doc comment prefix or whitespace.
498      break
499
500    # b/2983692
501    # don't prematurely match against a @flag if inside a doc flag
502    # need to think about what is the correct behavior for unterminated
503    # inline doc flags
504    if (iterator.type == Type.DOC_START_BRACE and
505        iterator.next.type == Type.DOC_INLINE_FLAG):
506      doc_depth += 1
507    elif (iterator.type == Type.DOC_END_BRACE and
508        doc_depth > 0):
509      doc_depth -= 1
510
511    if iterator.type in Type.FLAG_DESCRIPTION_TYPES:
512      contents += iterator.string
513      last_token = iterator
514
515    iterator = iterator.next
516    if iterator.line_number != last_line:
517      contents += '\n'
518      last_line = iterator.line_number
519
520  end_token = last_token
521  if DocFlag.EMPTY_STRING.match(contents):
522    contents = None
523  else:
524    # Strip trailing newline.
525    contents = contents[:-1]
526
527  return end_token, contents
528
529
530class Function(object):
531  """Data about a JavaScript function.
532
533  Attributes:
534    block_depth: Block depth the function began at.
535    doc: The DocComment associated with the function.
536    has_return: If the function has a return value.
537    has_this: If the function references the 'this' object.
538    is_assigned: If the function is part of an assignment.
539    is_constructor: If the function is a constructor.
540    name: The name of the function, whether given in the function keyword or
541        as the lvalue the function is assigned to.
542  """
543
544  def __init__(self, block_depth, is_assigned, doc, name):
545    self.block_depth = block_depth
546    self.is_assigned = is_assigned
547    self.is_constructor = doc and doc.HasFlag('constructor')
548    self.is_interface = doc and doc.HasFlag('interface')
549    self.has_return = False
550    self.has_throw = False
551    self.has_this = False
552    self.name = name
553    self.doc = doc
554
555
556class StateTracker(object):
557  """EcmaScript state tracker.
558
559  Tracks block depth, function names, etc. within an EcmaScript token stream.
560  """
561
562  OBJECT_LITERAL = 'o'
563  CODE = 'c'
564
565  def __init__(self, doc_flag=DocFlag):
566    """Initializes a JavaScript token stream state tracker.
567
568    Args:
569      doc_flag: An optional custom DocFlag used for validating
570          documentation flags.
571    """
572    self._doc_flag = doc_flag
573    self.Reset()
574
575  def Reset(self):
576    """Resets the state tracker to prepare for processing a new page."""
577    self._block_depth = 0
578    self._is_block_close = False
579    self._paren_depth = 0
580    self._functions = []
581    self._functions_by_name = {}
582    self._last_comment = None
583    self._doc_comment = None
584    self._cumulative_params = None
585    self._block_types = []
586    self._last_non_space_token = None
587    self._last_line = None
588    self._first_token = None
589    self._documented_identifiers = set()
590
591  def InFunction(self):
592    """Returns true if the current token is within a function.
593
594    Returns:
595      True if the current token is within a function.
596    """
597    return bool(self._functions)
598
599  def InConstructor(self):
600    """Returns true if the current token is within a constructor.
601
602    Returns:
603      True if the current token is within a constructor.
604    """
605    return self.InFunction() and self._functions[-1].is_constructor
606
607  def InInterfaceMethod(self):
608    """Returns true if the current token is within an interface method.
609
610    Returns:
611      True if the current token is within an interface method.
612    """
613    if self.InFunction():
614      if self._functions[-1].is_interface:
615        return True
616      else:
617        name = self._functions[-1].name
618        prototype_index = name.find('.prototype.')
619        if prototype_index != -1:
620          class_function_name = name[0:prototype_index]
621          if (class_function_name in self._functions_by_name and
622              self._functions_by_name[class_function_name].is_interface):
623            return True
624
625    return False
626
627  def InTopLevelFunction(self):
628    """Returns true if the current token is within a top level function.
629
630    Returns:
631      True if the current token is within a top level function.
632    """
633    return len(self._functions) == 1 and self.InTopLevel()
634
635  def InAssignedFunction(self):
636    """Returns true if the current token is within a function variable.
637
638    Returns:
639      True if if the current token is within a function variable
640    """
641    return self.InFunction() and self._functions[-1].is_assigned
642
643  def IsFunctionOpen(self):
644    """Returns true if the current token is a function block open.
645
646    Returns:
647      True if the current token is a function block open.
648    """
649    return (self._functions and
650            self._functions[-1].block_depth == self._block_depth - 1)
651
652  def IsFunctionClose(self):
653    """Returns true if the current token is a function block close.
654
655    Returns:
656      True if the current token is a function block close.
657    """
658    return (self._functions and
659            self._functions[-1].block_depth == self._block_depth)
660
661  def InBlock(self):
662    """Returns true if the current token is within a block.
663
664    Returns:
665      True if the current token is within a block.
666    """
667    return bool(self._block_depth)
668
669  def IsBlockClose(self):
670    """Returns true if the current token is a block close.
671
672    Returns:
673      True if the current token is a block close.
674    """
675    return self._is_block_close
676
677  def InObjectLiteral(self):
678    """Returns true if the current token is within an object literal.
679
680    Returns:
681      True if the current token is within an object literal.
682    """
683    return self._block_depth and self._block_types[-1] == self.OBJECT_LITERAL
684
685  def InObjectLiteralDescendant(self):
686    """Returns true if the current token has an object literal ancestor.
687
688    Returns:
689      True if the current token has an object literal ancestor.
690    """
691    return self.OBJECT_LITERAL in self._block_types
692
693  def InParentheses(self):
694    """Returns true if the current token is within parentheses.
695
696    Returns:
697      True if the current token is within parentheses.
698    """
699    return bool(self._paren_depth)
700
701  def InTopLevel(self):
702    """Whether we are at the top level in the class.
703
704    This function call is language specific.  In some languages like
705    JavaScript, a function is top level if it is not inside any parenthesis.
706    In languages such as ActionScript, a function is top level if it is directly
707    within a class.
708    """
709    raise TypeError('Abstract method InTopLevel not implemented')
710
711  def GetBlockType(self, token):
712    """Determine the block type given a START_BLOCK token.
713
714    Code blocks come after parameters, keywords  like else, and closing parens.
715
716    Args:
717      token: The current token. Can be assumed to be type START_BLOCK.
718    Returns:
719      Code block type for current token.
720    """
721    raise TypeError('Abstract method GetBlockType not implemented')
722
723  def GetParams(self):
724    """Returns the accumulated input params as an array.
725
726    In some EcmasSript languages, input params are specified like
727    (param:Type, param2:Type2, ...)
728    in other they are specified just as
729    (param, param2)
730    We handle both formats for specifying parameters here and leave
731    it to the compilers for each language to detect compile errors.
732    This allows more code to be reused between lint checkers for various
733    EcmaScript languages.
734
735    Returns:
736      The accumulated input params as an array.
737    """
738    params = []
739    if self._cumulative_params:
740      params = re.compile(r'\s+').sub('', self._cumulative_params).split(',')
741      # Strip out the type from parameters of the form name:Type.
742      params = map(lambda param: param.split(':')[0], params)
743
744    return params
745
746  def GetLastComment(self):
747    """Return the last plain comment that could be used as documentation.
748
749    Returns:
750      The last plain comment that could be used as documentation.
751    """
752    return self._last_comment
753
754  def GetDocComment(self):
755    """Return the most recent applicable documentation comment.
756
757    Returns:
758      The last applicable documentation comment.
759    """
760    return self._doc_comment
761
762  def HasDocComment(self, identifier):
763    """Returns whether the identifier has been documented yet.
764
765    Args:
766      identifier: The identifier.
767
768    Returns:
769      Whether the identifier has been documented yet.
770    """
771    return identifier in self._documented_identifiers
772
773  def InDocComment(self):
774    """Returns whether the current token is in a doc comment.
775
776    Returns:
777      Whether the current token is in a doc comment.
778    """
779    return self._doc_comment and self._doc_comment.end_token is None
780
781  def GetDocFlag(self):
782    """Returns the current documentation flags.
783
784    Returns:
785      The current documentation flags.
786    """
787    return self._doc_flag
788
789  def IsTypeToken(self, t):
790    if self.InDocComment() and t.type not in (Type.START_DOC_COMMENT,
791        Type.DOC_FLAG, Type.DOC_INLINE_FLAG, Type.DOC_PREFIX):
792      f = tokenutil.SearchUntil(t, [Type.DOC_FLAG], [Type.START_DOC_COMMENT],
793                                None, True)
794      if f and f.attached_object.type_start_token is not None:
795        return (tokenutil.Compare(t, f.attached_object.type_start_token) > 0 and
796                tokenutil.Compare(t, f.attached_object.type_end_token) < 0)
797    return False
798
799  def GetFunction(self):
800    """Return the function the current code block is a part of.
801
802    Returns:
803      The current Function object.
804    """
805    if self._functions:
806      return self._functions[-1]
807
808  def GetBlockDepth(self):
809    """Return the block depth.
810
811    Returns:
812      The current block depth.
813    """
814    return self._block_depth
815
816  def GetLastNonSpaceToken(self):
817    """Return the last non whitespace token."""
818    return self._last_non_space_token
819
820  def GetLastLine(self):
821    """Return the last line."""
822    return self._last_line
823
824  def GetFirstToken(self):
825    """Return the very first token in the file."""
826    return self._first_token
827
828  def HandleToken(self, token, last_non_space_token):
829    """Handles the given token and updates state.
830
831    Args:
832      token: The token to handle.
833      last_non_space_token:
834    """
835    self._is_block_close = False
836
837    if not self._first_token:
838      self._first_token = token
839
840    # Track block depth.
841    type = token.type
842    if type == Type.START_BLOCK:
843      self._block_depth += 1
844
845      # Subclasses need to handle block start very differently because
846      # whether a block is a CODE or OBJECT_LITERAL block varies significantly
847      # by language.
848      self._block_types.append(self.GetBlockType(token))
849
850    # Track block depth.
851    elif type == Type.END_BLOCK:
852      self._is_block_close = not self.InObjectLiteral()
853      self._block_depth -= 1
854      self._block_types.pop()
855
856    # Track parentheses depth.
857    elif type == Type.START_PAREN:
858      self._paren_depth += 1
859
860    # Track parentheses depth.
861    elif type == Type.END_PAREN:
862      self._paren_depth -= 1
863
864    elif type == Type.COMMENT:
865      self._last_comment = token.string
866
867    elif type == Type.START_DOC_COMMENT:
868      self._last_comment = None
869      self._doc_comment = DocComment(token)
870
871    elif type == Type.END_DOC_COMMENT:
872      self._doc_comment.end_token = token
873
874    elif type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
875      flag = self._doc_flag(token)
876      token.attached_object = flag
877      self._doc_comment.AddFlag(flag)
878
879      if flag.flag_type == 'param' and flag.name:
880        self._doc_comment.AddParam(flag.name, flag.type)
881      elif flag.flag_type == 'suppress':
882        self._doc_comment.AddSuppression(token)
883
884    elif type == Type.FUNCTION_DECLARATION:
885      last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, None,
886                                         True)
887      doc = None
888      # Only functions outside of parens are eligible for documentation.
889      if not self._paren_depth:
890        doc = self._doc_comment
891
892      name = ''
893      is_assigned = last_code and (last_code.IsOperator('=') or
894          last_code.IsOperator('||') or last_code.IsOperator('&&') or
895          (last_code.IsOperator(':') and not self.InObjectLiteral()))
896      if is_assigned:
897        # TODO(robbyw): This breaks for x[2] = ...
898        # Must use loop to find full function name in the case of line-wrapped
899        # declarations (bug 1220601) like:
900        # my.function.foo.
901        #   bar = function() ...
902        identifier = tokenutil.Search(last_code, Type.SIMPLE_LVALUE, None, True)
903        while identifier and identifier.type in (
904            Type.IDENTIFIER, Type.SIMPLE_LVALUE):
905          name = identifier.string + name
906          # Traverse behind us, skipping whitespace and comments.
907          while True:
908            identifier = identifier.previous
909            if not identifier or not identifier.type in Type.NON_CODE_TYPES:
910              break
911
912      else:
913        next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
914        while next_token and next_token.IsType(Type.FUNCTION_NAME):
915          name += next_token.string
916          next_token = tokenutil.Search(next_token, Type.FUNCTION_NAME, 2)
917
918      function = Function(self._block_depth, is_assigned, doc, name)
919      self._functions.append(function)
920      self._functions_by_name[name] = function
921
922    elif type == Type.START_PARAMETERS:
923      self._cumulative_params = ''
924
925    elif type == Type.PARAMETERS:
926      self._cumulative_params += token.string
927
928    elif type == Type.KEYWORD and token.string == 'return':
929      next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
930      if not next_token.IsType(Type.SEMICOLON):
931        function = self.GetFunction()
932        if function:
933          function.has_return = True
934
935    elif type == Type.KEYWORD and token.string == 'throw':
936      function = self.GetFunction()
937      if function:
938        function.has_throw = True
939
940    elif type == Type.SIMPLE_LVALUE:
941      identifier = token.values['identifier']
942      jsdoc = self.GetDocComment()
943      if jsdoc:
944        self._documented_identifiers.add(identifier)
945
946      self._HandleIdentifier(identifier, True)
947
948    elif type == Type.IDENTIFIER:
949      self._HandleIdentifier(token.string, False)
950
951      # Detect documented non-assignments.
952      next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
953      if next_token.IsType(Type.SEMICOLON):
954        if (self._last_non_space_token and
955            self._last_non_space_token.IsType(Type.END_DOC_COMMENT)):
956          self._documented_identifiers.add(token.string)
957
958  def _HandleIdentifier(self, identifier, is_assignment):
959    """Process the given identifier.
960
961    Currently checks if it references 'this' and annotates the function
962    accordingly.
963
964    Args:
965      identifier: The identifer to process.
966      is_assignment: Whether the identifer is being written to.
967    """
968    if identifier == 'this' or identifier.startswith('this.'):
969      function = self.GetFunction()
970      if function:
971        function.has_this = True
972
973
974  def HandleAfterToken(self, token):
975    """Handle updating state after a token has been checked.
976
977    This function should be used for destructive state changes such as
978    deleting a tracked object.
979
980    Args:
981      token: The token to handle.
982    """
983    type = token.type
984    if type == Type.SEMICOLON or type == Type.END_PAREN or (
985        type == Type.END_BRACKET and
986        self._last_non_space_token.type not in (
987            Type.SINGLE_QUOTE_STRING_END, Type.DOUBLE_QUOTE_STRING_END)):
988      # We end on any numeric array index, but keep going for string based
989      # array indices so that we pick up manually exported identifiers.
990      self._doc_comment = None
991      self._last_comment = None
992
993    elif type == Type.END_BLOCK:
994      self._doc_comment = None
995      self._last_comment = None
996
997      if self.InFunction() and self.IsFunctionClose():
998        # TODO(robbyw): Detect the function's name for better errors.
999        self._functions.pop()
1000
1001    elif type == Type.END_PARAMETERS and self._doc_comment:
1002      self._doc_comment = None
1003      self._last_comment = None
1004
1005    if not token.IsAnyType(Type.WHITESPACE, Type.BLANK_LINE):
1006      self._last_non_space_token = token
1007
1008    self._last_line = token.line
1009