Home | History | Annotate | Download | only in closure_linter
      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 (at] google.com (Robert Walker)',
     20               'ajp (at] google.com (Andy Perelson)')
     21 
     22 import re
     23 
     24 from closure_linter import javascripttokenizer
     25 from closure_linter import javascripttokens
     26 from closure_linter import tokenutil
     27 
     28 # Shorthand
     29 Type = javascripttokens.JavaScriptTokenType
     30 
     31 
     32 class 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 
    216 class 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 
    397 def _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 
    438 def _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 
    467 def _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 
    530 class 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 
    556 class 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