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
27from closure_linter import typeannotation
28
29# Shorthand
30Type = javascripttokens.JavaScriptTokenType
31
32
33class DocFlag(object):
34  """Generic doc flag object.
35
36  Attribute:
37    flag_type: param, return, define, type, etc.
38    flag_token: The flag token.
39    type_start_token: The first token specifying the flag type,
40      including braces.
41    type_end_token: The last token specifying the flag type,
42      including braces.
43    type: The type spec string.
44    jstype: The type spec, a TypeAnnotation instance.
45    name_token: The token specifying the flag name.
46    name: The flag name
47    description_start_token: The first token in the description.
48    description_end_token: The end token in the description.
49    description: The description.
50  """
51
52  # Please keep these lists alphabetized.
53
54  # The list of standard jsdoc tags is from
55  STANDARD_DOC = frozenset([
56      'author',
57      'bug',
58      'classTemplate',
59      'consistentIdGenerator',
60      'const',
61      'constructor',
62      'define',
63      'deprecated',
64      'dict',
65      'enum',
66      'export',
67      'expose',
68      'extends',
69      'externs',
70      'fileoverview',
71      'idGenerator',
72      'implements',
73      'implicitCast',
74      'interface',
75      'lends',
76      'license',
77      'ngInject',  # This annotation is specific to AngularJS.
78      'noalias',
79      'nocompile',
80      'nosideeffects',
81      'override',
82      'owner',
83      'nocollapse',
84      'package',
85      'param',
86      'polymerBehavior',  # This annotation is specific to Polymer.
87      'preserve',
88      'private',
89      'protected',
90      'public',
91      'return',
92      'see',
93      'stableIdGenerator',
94      'struct',
95      'supported',
96      'template',
97      'this',
98      'type',
99      'typedef',
100      'unrestricted',
101      ])
102
103  ANNOTATION = frozenset(['preserveTry', 'suppress'])
104
105  LEGAL_DOC = STANDARD_DOC | ANNOTATION
106
107  # Includes all Closure Compiler @suppress types.
108  # Not all of these annotations are interpreted by Closure Linter.
109  #
110  # Specific cases:
111  # - accessControls is supported by the compiler at the expression
112  #   and method level to suppress warnings about private/protected
113  #   access (method level applies to all references in the method).
114  #   The linter mimics the compiler behavior.
115  SUPPRESS_TYPES = frozenset([
116      'accessControls',
117      'ambiguousFunctionDecl',
118      'checkDebuggerStatement',
119      'checkRegExp',
120      'checkStructDictInheritance',
121      'checkTypes',
122      'checkVars',
123      'const',
124      'constantProperty',
125      'deprecated',
126      'duplicate',
127      'es5Strict',
128      'externsValidation',
129      'extraProvide',
130      'extraRequire',
131      'fileoverviewTags',
132      'globalThis',
133      'internetExplorerChecks',
134      'invalidCasts',
135      'missingProperties',
136      'missingProvide',
137      'missingRequire',
138      'missingReturn',
139      'nonStandardJsDocs',
140      'reportUnknownTypes',
141      'strictModuleDepCheck',
142      'suspiciousCode',
143      'tweakValidation',
144      'typeInvalidation',
145      'undefinedNames',
146      'undefinedVars',
147      'underscore',
148      'unknownDefines',
149      'unnecessaryCasts',
150      'unusedPrivateMembers',
151      'uselessCode',
152      'visibility',
153      'with',
154  ])
155
156  HAS_DESCRIPTION = frozenset([
157      'define',
158      'deprecated',
159      'desc',
160      'fileoverview',
161      'license',
162      'param',
163      'preserve',
164      'return',
165      'supported',
166  ])
167
168  # Docflags whose argument should be parsed using the typeannotation parser.
169  HAS_TYPE = frozenset([
170      'const',
171      'define',
172      'enum',
173      'export',
174      'extends',
175      'final',
176      'implements',
177      'mods',
178      'package',
179      'param',
180      'private',
181      'protected',
182      'public',
183      'return',
184      'suppress',
185      'type',
186      'typedef',
187  ])
188
189  # Docflags for which it's ok to omit the type (flag without an argument).
190  CAN_OMIT_TYPE = frozenset([
191      'const',
192      'enum',
193      'export',
194      'final',
195      'package',
196      'private',
197      'protected',
198      'public',
199      'suppress',  # We'll raise a separate INCORRECT_SUPPRESS_SYNTAX instead.
200  ])
201
202  # Docflags that only take a type as an argument and should not parse a
203  # following description.
204  TYPE_ONLY = frozenset([
205      'const',
206      'enum',
207      'extends',
208      'implements',
209      'package',
210      'suppress',
211      'type',
212  ])
213
214  HAS_NAME = frozenset(['param'])
215
216  EMPTY_COMMENT_LINE = re.compile(r'^\s*\*?\s*$')
217  EMPTY_STRING = re.compile(r'^\s*$')
218
219  def __init__(self, flag_token, error_handler=None):
220    """Creates the DocFlag object and attaches it to the given start token.
221
222    Args:
223      flag_token: The starting token of the flag.
224      error_handler: An optional error handler for errors occurring while
225        parsing the doctype.
226    """
227    self.flag_token = flag_token
228    self.flag_type = flag_token.string.strip().lstrip('@')
229
230    # Extract type, if applicable.
231    self.type = None
232    self.jstype = None
233    self.type_start_token = None
234    self.type_end_token = None
235    if self.flag_type in self.HAS_TYPE:
236      brace = tokenutil.SearchUntil(flag_token, [Type.DOC_START_BRACE],
237                                    Type.FLAG_ENDING_TYPES)
238      if brace:
239        end_token, contents = _GetMatchingEndBraceAndContents(brace)
240        self.type = contents
241        self.jstype = typeannotation.Parse(brace, end_token,
242                                           error_handler)
243        self.type_start_token = brace
244        self.type_end_token = end_token
245      elif (self.flag_type in self.TYPE_ONLY and
246            flag_token.next.type not in Type.FLAG_ENDING_TYPES and
247            flag_token.line_number == flag_token.next.line_number):
248        # b/10407058. If the flag is expected to be followed by a type then
249        # search for type in same line only. If no token after flag in same
250        # line then conclude that no type is specified.
251        self.type_start_token = flag_token.next
252        self.type_end_token, self.type = _GetEndTokenAndContents(
253            self.type_start_token)
254        if self.type is not None:
255          self.type = self.type.strip()
256          self.jstype = typeannotation.Parse(flag_token, self.type_end_token,
257                                             error_handler)
258
259    # Extract name, if applicable.
260    self.name_token = None
261    self.name = None
262    if self.flag_type in self.HAS_NAME:
263      # Handle bad case, name could be immediately after flag token.
264      self.name_token = _GetNextPartialIdentifierToken(flag_token)
265
266      # Handle good case, if found token is after type start, look for
267      # a identifier (substring to cover cases like [cnt] b/4197272) after
268      # type end, since types contain identifiers.
269      if (self.type and self.name_token and
270          tokenutil.Compare(self.name_token, self.type_start_token) > 0):
271        self.name_token = _GetNextPartialIdentifierToken(self.type_end_token)
272
273      if self.name_token:
274        self.name = self.name_token.string
275
276    # Extract description, if applicable.
277    self.description_start_token = None
278    self.description_end_token = None
279    self.description = None
280    if self.flag_type in self.HAS_DESCRIPTION:
281      search_start_token = flag_token
282      if self.name_token and self.type_end_token:
283        if tokenutil.Compare(self.type_end_token, self.name_token) > 0:
284          search_start_token = self.type_end_token
285        else:
286          search_start_token = self.name_token
287      elif self.name_token:
288        search_start_token = self.name_token
289      elif self.type:
290        search_start_token = self.type_end_token
291
292      interesting_token = tokenutil.Search(search_start_token,
293          Type.FLAG_DESCRIPTION_TYPES | Type.FLAG_ENDING_TYPES)
294      if interesting_token.type in Type.FLAG_DESCRIPTION_TYPES:
295        self.description_start_token = interesting_token
296        self.description_end_token, self.description = (
297            _GetEndTokenAndContents(interesting_token))
298
299  def HasType(self):
300    """Returns whether this flag should have a type annotation."""
301    return self.flag_type in self.HAS_TYPE
302
303  def __repr__(self):
304    return '<Flag: %s, type:%s>' % (self.flag_type, repr(self.jstype))
305
306
307class DocComment(object):
308  """JavaScript doc comment object.
309
310  Attributes:
311    ordered_params: Ordered list of parameters documented.
312    start_token: The token that starts the doc comment.
313    end_token: The token that ends the doc comment.
314    suppressions: Map of suppression type to the token that added it.
315  """
316  def __init__(self, start_token):
317    """Create the doc comment object.
318
319    Args:
320      start_token: The first token in the doc comment.
321    """
322    self.__flags = []
323    self.start_token = start_token
324    self.end_token = None
325    self.suppressions = {}
326    self.invalidated = False
327
328  @property
329  def ordered_params(self):
330    """Gives the list of parameter names as a list of strings."""
331    params = []
332    for flag in self.__flags:
333      if flag.flag_type == 'param' and flag.name:
334        params.append(flag.name)
335    return params
336
337  def Invalidate(self):
338    """Indicate that the JSDoc is well-formed but we had problems parsing it.
339
340    This is a short-circuiting mechanism so that we don't emit false
341    positives about well-formed doc comments just because we don't support
342    hot new syntaxes.
343    """
344    self.invalidated = True
345
346  def IsInvalidated(self):
347    """Test whether Invalidate() has been called."""
348    return self.invalidated
349
350  def AddSuppression(self, token):
351    """Add a new error suppression flag.
352
353    Args:
354      token: The suppression flag token.
355    """
356    flag = token and token.attached_object
357    if flag and flag.jstype:
358      for suppression in flag.jstype.IterIdentifiers():
359        self.suppressions[suppression] = token
360
361  def SuppressionOnly(self):
362    """Returns whether this comment contains only suppression flags."""
363    if not self.__flags:
364      return False
365
366    for flag in self.__flags:
367      if flag.flag_type != 'suppress':
368        return False
369
370    return True
371
372  def AddFlag(self, flag):
373    """Add a new document flag.
374
375    Args:
376      flag: DocFlag object.
377    """
378    self.__flags.append(flag)
379
380  def InheritsDocumentation(self):
381    """Test if the jsdoc implies documentation inheritance.
382
383    Returns:
384        True if documentation may be pulled off the superclass.
385    """
386    return self.HasFlag('inheritDoc') or self.HasFlag('override')
387
388  def HasFlag(self, flag_type):
389    """Test if the given flag has been set.
390
391    Args:
392      flag_type: The type of the flag to check.
393
394    Returns:
395      True if the flag is set.
396    """
397    for flag in self.__flags:
398      if flag.flag_type == flag_type:
399        return True
400    return False
401
402  def GetFlag(self, flag_type):
403    """Gets the last flag of the given type.
404
405    Args:
406      flag_type: The type of the flag to get.
407
408    Returns:
409      The last instance of the given flag type in this doc comment.
410    """
411    for flag in reversed(self.__flags):
412      if flag.flag_type == flag_type:
413        return flag
414
415  def GetDocFlags(self):
416    """Return the doc flags for this comment."""
417    return list(self.__flags)
418
419  def _YieldDescriptionTokens(self):
420    for token in self.start_token:
421
422      if (token is self.end_token or
423          token.type is javascripttokens.JavaScriptTokenType.DOC_FLAG or
424          token.type not in javascripttokens.JavaScriptTokenType.COMMENT_TYPES):
425        return
426
427      if token.type not in [
428          javascripttokens.JavaScriptTokenType.START_DOC_COMMENT,
429          javascripttokens.JavaScriptTokenType.END_DOC_COMMENT,
430          javascripttokens.JavaScriptTokenType.DOC_PREFIX]:
431        yield token
432
433  @property
434  def description(self):
435    return tokenutil.TokensToString(
436        self._YieldDescriptionTokens())
437
438  def GetTargetIdentifier(self):
439    """Returns the identifier (as a string) that this is a comment for.
440
441    Note that this uses method uses GetIdentifierForToken to get the full
442    identifier, even if broken up by whitespace, newlines, or comments,
443    and thus could be longer than GetTargetToken().string.
444
445    Returns:
446      The identifier for the token this comment is for.
447    """
448    token = self.GetTargetToken()
449    if token:
450      return tokenutil.GetIdentifierForToken(token)
451
452  def GetTargetToken(self):
453    """Get this comment's target token.
454
455    Returns:
456      The token that is the target of this comment, or None if there isn't one.
457    """
458
459    # File overviews describe the file, not a token.
460    if self.HasFlag('fileoverview'):
461      return
462
463    skip_types = frozenset([
464        Type.WHITESPACE,
465        Type.BLANK_LINE,
466        Type.START_PAREN])
467
468    target_types = frozenset([
469        Type.FUNCTION_NAME,
470        Type.IDENTIFIER,
471        Type.SIMPLE_LVALUE])
472
473    token = self.end_token.next
474    while token:
475      if token.type in target_types:
476        return token
477
478      # Handles the case of a comment on "var foo = ...'
479      if token.IsKeyword('var'):
480        next_code_token = tokenutil.CustomSearch(
481            token,
482            lambda t: t.type not in Type.NON_CODE_TYPES)
483
484        if (next_code_token and
485            next_code_token.IsType(Type.SIMPLE_LVALUE)):
486          return next_code_token
487
488        return
489
490      # Handles the case of a comment on "function foo () {}"
491      if token.type is Type.FUNCTION_DECLARATION:
492        next_code_token = tokenutil.CustomSearch(
493            token,
494            lambda t: t.type not in Type.NON_CODE_TYPES)
495
496        if next_code_token.IsType(Type.FUNCTION_NAME):
497          return next_code_token
498
499        return
500
501      # Skip types will end the search.
502      if token.type not in skip_types:
503        return
504
505      token = token.next
506
507  def CompareParameters(self, params):
508    """Computes the edit distance and list from the function params to the docs.
509
510    Uses the Levenshtein edit distance algorithm, with code modified from
511    http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#Python
512
513    Args:
514      params: The parameter list for the function declaration.
515
516    Returns:
517      The edit distance, the edit list.
518    """
519    source_len, target_len = len(self.ordered_params), len(params)
520    edit_lists = [[]]
521    distance = [[]]
522    for i in range(target_len+1):
523      edit_lists[0].append(['I'] * i)
524      distance[0].append(i)
525
526    for j in range(1, source_len+1):
527      edit_lists.append([['D'] * j])
528      distance.append([j])
529
530    for i in range(source_len):
531      for j in range(target_len):
532        cost = 1
533        if self.ordered_params[i] == params[j]:
534          cost = 0
535
536        deletion = distance[i][j+1] + 1
537        insertion = distance[i+1][j] + 1
538        substitution = distance[i][j] + cost
539
540        edit_list = None
541        best = None
542        if deletion <= insertion and deletion <= substitution:
543          # Deletion is best.
544          best = deletion
545          edit_list = list(edit_lists[i][j+1])
546          edit_list.append('D')
547
548        elif insertion <= substitution:
549          # Insertion is best.
550          best = insertion
551          edit_list = list(edit_lists[i+1][j])
552          edit_list.append('I')
553          edit_lists[i+1].append(edit_list)
554
555        else:
556          # Substitution is best.
557          best = substitution
558          edit_list = list(edit_lists[i][j])
559          if cost:
560            edit_list.append('S')
561          else:
562            edit_list.append('=')
563
564        edit_lists[i+1].append(edit_list)
565        distance[i+1].append(best)
566
567    return distance[source_len][target_len], edit_lists[source_len][target_len]
568
569  def __repr__(self):
570    """Returns a string representation of this object.
571
572    Returns:
573      A string representation of this object.
574    """
575    return '<DocComment: %s, %s>' % (
576        str(self.ordered_params), str(self.__flags))
577
578
579#
580# Helper methods used by DocFlag and DocComment to parse out flag information.
581#
582
583
584def _GetMatchingEndBraceAndContents(start_brace):
585  """Returns the matching end brace and contents between the two braces.
586
587  If any FLAG_ENDING_TYPE token is encountered before a matching end brace, then
588  that token is used as the matching ending token. Contents will have all
589  comment prefixes stripped out of them, and all comment prefixes in between the
590  start and end tokens will be split out into separate DOC_PREFIX tokens.
591
592  Args:
593    start_brace: The DOC_START_BRACE token immediately before desired contents.
594
595  Returns:
596    The matching ending token (DOC_END_BRACE or FLAG_ENDING_TYPE) and a string
597    of the contents between the matching tokens, minus any comment prefixes.
598  """
599  open_count = 1
600  close_count = 0
601  contents = []
602
603  # We don't consider the start brace part of the type string.
604  token = start_brace.next
605  while open_count != close_count:
606    if token.type == Type.DOC_START_BRACE:
607      open_count += 1
608    elif token.type == Type.DOC_END_BRACE:
609      close_count += 1
610
611    if token.type != Type.DOC_PREFIX:
612      contents.append(token.string)
613
614    if token.type in Type.FLAG_ENDING_TYPES:
615      break
616    token = token.next
617
618  #Don't include the end token (end brace, end doc comment, etc.) in type.
619  token = token.previous
620  contents = contents[:-1]
621
622  return token, ''.join(contents)
623
624
625def _GetNextPartialIdentifierToken(start_token):
626  """Returns the first token having identifier as substring after a token.
627
628  Searches each token after the start to see if it contains an identifier.
629  If found, token is returned. If no identifier is found returns None.
630  Search is abandoned when a FLAG_ENDING_TYPE token is found.
631
632  Args:
633    start_token: The token to start searching after.
634
635  Returns:
636    The token found containing identifier, None otherwise.
637  """
638  token = start_token.next
639
640  while token and token.type not in Type.FLAG_ENDING_TYPES:
641    match = javascripttokenizer.JavaScriptTokenizer.IDENTIFIER.search(
642        token.string)
643    if match is not None and token.type == Type.COMMENT:
644      return token
645
646    token = token.next
647
648  return None
649
650
651def _GetEndTokenAndContents(start_token):
652  """Returns last content token and all contents before FLAG_ENDING_TYPE token.
653
654  Comment prefixes are split into DOC_PREFIX tokens and stripped from the
655  returned contents.
656
657  Args:
658    start_token: The token immediately before the first content token.
659
660  Returns:
661    The last content token and a string of all contents including start and
662    end tokens, with comment prefixes stripped.
663  """
664  iterator = start_token
665  last_line = iterator.line_number
666  last_token = None
667  contents = ''
668  doc_depth = 0
669  while not iterator.type in Type.FLAG_ENDING_TYPES or doc_depth > 0:
670    if (iterator.IsFirstInLine() and
671        DocFlag.EMPTY_COMMENT_LINE.match(iterator.line)):
672      # If we have a blank comment line, consider that an implicit
673      # ending of the description. This handles a case like:
674      #
675      # * @return {boolean} True
676      # *
677      # * Note: This is a sentence.
678      #
679      # The note is not part of the @return description, but there was
680      # no definitive ending token. Rather there was a line containing
681      # only a doc comment prefix or whitespace.
682      break
683
684    # b/2983692
685    # don't prematurely match against a @flag if inside a doc flag
686    # need to think about what is the correct behavior for unterminated
687    # inline doc flags
688    if (iterator.type == Type.DOC_START_BRACE and
689        iterator.next.type == Type.DOC_INLINE_FLAG):
690      doc_depth += 1
691    elif (iterator.type == Type.DOC_END_BRACE and
692        doc_depth > 0):
693      doc_depth -= 1
694
695    if iterator.type in Type.FLAG_DESCRIPTION_TYPES:
696      contents += iterator.string
697      last_token = iterator
698
699    iterator = iterator.next
700    if iterator.line_number != last_line:
701      contents += '\n'
702      last_line = iterator.line_number
703
704  end_token = last_token
705  if DocFlag.EMPTY_STRING.match(contents):
706    contents = None
707  else:
708    # Strip trailing newline.
709    contents = contents[:-1]
710
711  return end_token, contents
712
713
714class Function(object):
715  """Data about a JavaScript function.
716
717  Attributes:
718    block_depth: Block depth the function began at.
719    doc: The DocComment associated with the function.
720    has_return: If the function has a return value.
721    has_this: If the function references the 'this' object.
722    is_assigned: If the function is part of an assignment.
723    is_constructor: If the function is a constructor.
724    name: The name of the function, whether given in the function keyword or
725        as the lvalue the function is assigned to.
726    start_token: First token of the function (the function' keyword token).
727    end_token: Last token of the function (the closing '}' token).
728    parameters: List of parameter names.
729  """
730
731  def __init__(self, block_depth, is_assigned, doc, name):
732    self.block_depth = block_depth
733    self.is_assigned = is_assigned
734    self.is_constructor = doc and doc.HasFlag('constructor')
735    self.is_interface = doc and doc.HasFlag('interface')
736    self.has_return = False
737    self.has_throw = False
738    self.has_this = False
739    self.name = name
740    self.doc = doc
741    self.start_token = None
742    self.end_token = None
743    self.parameters = None
744
745
746class StateTracker(object):
747  """EcmaScript state tracker.
748
749  Tracks block depth, function names, etc. within an EcmaScript token stream.
750  """
751
752  OBJECT_LITERAL = 'o'
753  CODE = 'c'
754
755  def __init__(self, doc_flag=DocFlag):
756    """Initializes a JavaScript token stream state tracker.
757
758    Args:
759      doc_flag: An optional custom DocFlag used for validating
760          documentation flags.
761    """
762    self._doc_flag = doc_flag
763    self.Reset()
764
765  def Reset(self):
766    """Resets the state tracker to prepare for processing a new page."""
767    self._block_depth = 0
768    self._is_block_close = False
769    self._paren_depth = 0
770    self._function_stack = []
771    self._functions_by_name = {}
772    self._last_comment = None
773    self._doc_comment = None
774    self._cumulative_params = None
775    self._block_types = []
776    self._last_non_space_token = None
777    self._last_line = None
778    self._first_token = None
779    self._documented_identifiers = set()
780    self._variables_in_scope = []
781
782  def DocFlagPass(self, start_token, error_handler):
783    """Parses doc flags.
784
785    This pass needs to be executed before the aliaspass and we don't want to do
786    a full-blown statetracker dry run for these.
787
788    Args:
789      start_token: The token at which to start iterating
790      error_handler: An error handler for error reporting.
791    """
792    if not start_token:
793      return
794    doc_flag_types = (Type.DOC_FLAG, Type.DOC_INLINE_FLAG)
795    for token in start_token:
796      if token.type in doc_flag_types:
797        token.attached_object = self._doc_flag(token, error_handler)
798
799  def InFunction(self):
800    """Returns true if the current token is within a function.
801
802    Returns:
803      True if the current token is within a function.
804    """
805    return bool(self._function_stack)
806
807  def InConstructor(self):
808    """Returns true if the current token is within a constructor.
809
810    Returns:
811      True if the current token is within a constructor.
812    """
813    return self.InFunction() and self._function_stack[-1].is_constructor
814
815  def InInterfaceMethod(self):
816    """Returns true if the current token is within an interface method.
817
818    Returns:
819      True if the current token is within an interface method.
820    """
821    if self.InFunction():
822      if self._function_stack[-1].is_interface:
823        return True
824      else:
825        name = self._function_stack[-1].name
826        prototype_index = name.find('.prototype.')
827        if prototype_index != -1:
828          class_function_name = name[0:prototype_index]
829          if (class_function_name in self._functions_by_name and
830              self._functions_by_name[class_function_name].is_interface):
831            return True
832
833    return False
834
835  def InTopLevelFunction(self):
836    """Returns true if the current token is within a top level function.
837
838    Returns:
839      True if the current token is within a top level function.
840    """
841    return len(self._function_stack) == 1 and self.InTopLevel()
842
843  def InAssignedFunction(self):
844    """Returns true if the current token is within a function variable.
845
846    Returns:
847      True if if the current token is within a function variable
848    """
849    return self.InFunction() and self._function_stack[-1].is_assigned
850
851  def IsFunctionOpen(self):
852    """Returns true if the current token is a function block open.
853
854    Returns:
855      True if the current token is a function block open.
856    """
857    return (self._function_stack and
858            self._function_stack[-1].block_depth == self._block_depth - 1)
859
860  def IsFunctionClose(self):
861    """Returns true if the current token is a function block close.
862
863    Returns:
864      True if the current token is a function block close.
865    """
866    return (self._function_stack and
867            self._function_stack[-1].block_depth == self._block_depth)
868
869  def InBlock(self):
870    """Returns true if the current token is within a block.
871
872    Returns:
873      True if the current token is within a block.
874    """
875    return bool(self._block_depth)
876
877  def IsBlockClose(self):
878    """Returns true if the current token is a block close.
879
880    Returns:
881      True if the current token is a block close.
882    """
883    return self._is_block_close
884
885  def InObjectLiteral(self):
886    """Returns true if the current token is within an object literal.
887
888    Returns:
889      True if the current token is within an object literal.
890    """
891    return self._block_depth and self._block_types[-1] == self.OBJECT_LITERAL
892
893  def InObjectLiteralDescendant(self):
894    """Returns true if the current token has an object literal ancestor.
895
896    Returns:
897      True if the current token has an object literal ancestor.
898    """
899    return self.OBJECT_LITERAL in self._block_types
900
901  def InParentheses(self):
902    """Returns true if the current token is within parentheses.
903
904    Returns:
905      True if the current token is within parentheses.
906    """
907    return bool(self._paren_depth)
908
909  def ParenthesesDepth(self):
910    """Returns the number of parens surrounding the token.
911
912    Returns:
913      The number of parenthesis surrounding the token.
914    """
915    return self._paren_depth
916
917  def BlockDepth(self):
918    """Returns the number of blocks in which the token is nested.
919
920    Returns:
921      The number of blocks in which the token is nested.
922    """
923    return self._block_depth
924
925  def FunctionDepth(self):
926    """Returns the number of functions in which the token is nested.
927
928    Returns:
929      The number of functions in which the token is nested.
930    """
931    return len(self._function_stack)
932
933  def InTopLevel(self):
934    """Whether we are at the top level in the class.
935
936    This function call is language specific.  In some languages like
937    JavaScript, a function is top level if it is not inside any parenthesis.
938    In languages such as ActionScript, a function is top level if it is directly
939    within a class.
940    """
941    raise TypeError('Abstract method InTopLevel not implemented')
942
943  def GetBlockType(self, token):
944    """Determine the block type given a START_BLOCK token.
945
946    Code blocks come after parameters, keywords  like else, and closing parens.
947
948    Args:
949      token: The current token. Can be assumed to be type START_BLOCK.
950    Returns:
951      Code block type for current token.
952    """
953    raise TypeError('Abstract method GetBlockType not implemented')
954
955  def GetParams(self):
956    """Returns the accumulated input params as an array.
957
958    In some EcmasSript languages, input params are specified like
959    (param:Type, param2:Type2, ...)
960    in other they are specified just as
961    (param, param2)
962    We handle both formats for specifying parameters here and leave
963    it to the compilers for each language to detect compile errors.
964    This allows more code to be reused between lint checkers for various
965    EcmaScript languages.
966
967    Returns:
968      The accumulated input params as an array.
969    """
970    params = []
971    if self._cumulative_params:
972      params = re.compile(r'\s+').sub('', self._cumulative_params).split(',')
973      # Strip out the type from parameters of the form name:Type.
974      params = map(lambda param: param.split(':')[0], params)
975
976    return params
977
978  def GetLastComment(self):
979    """Return the last plain comment that could be used as documentation.
980
981    Returns:
982      The last plain comment that could be used as documentation.
983    """
984    return self._last_comment
985
986  def GetDocComment(self):
987    """Return the most recent applicable documentation comment.
988
989    Returns:
990      The last applicable documentation comment.
991    """
992    return self._doc_comment
993
994  def HasDocComment(self, identifier):
995    """Returns whether the identifier has been documented yet.
996
997    Args:
998      identifier: The identifier.
999
1000    Returns:
1001      Whether the identifier has been documented yet.
1002    """
1003    return identifier in self._documented_identifiers
1004
1005  def InDocComment(self):
1006    """Returns whether the current token is in a doc comment.
1007
1008    Returns:
1009      Whether the current token is in a doc comment.
1010    """
1011    return self._doc_comment and self._doc_comment.end_token is None
1012
1013  def GetDocFlag(self):
1014    """Returns the current documentation flags.
1015
1016    Returns:
1017      The current documentation flags.
1018    """
1019    return self._doc_flag
1020
1021  def IsTypeToken(self, t):
1022    if self.InDocComment() and t.type not in (Type.START_DOC_COMMENT,
1023        Type.DOC_FLAG, Type.DOC_INLINE_FLAG, Type.DOC_PREFIX):
1024      f = tokenutil.SearchUntil(t, [Type.DOC_FLAG], [Type.START_DOC_COMMENT],
1025                                None, True)
1026      if (f and f.attached_object.type_start_token is not None and
1027          f.attached_object.type_end_token is not None):
1028        return (tokenutil.Compare(t, f.attached_object.type_start_token) > 0 and
1029                tokenutil.Compare(t, f.attached_object.type_end_token) < 0)
1030    return False
1031
1032  def GetFunction(self):
1033    """Return the function the current code block is a part of.
1034
1035    Returns:
1036      The current Function object.
1037    """
1038    if self._function_stack:
1039      return self._function_stack[-1]
1040
1041  def GetBlockDepth(self):
1042    """Return the block depth.
1043
1044    Returns:
1045      The current block depth.
1046    """
1047    return self._block_depth
1048
1049  def GetLastNonSpaceToken(self):
1050    """Return the last non whitespace token."""
1051    return self._last_non_space_token
1052
1053  def GetLastLine(self):
1054    """Return the last line."""
1055    return self._last_line
1056
1057  def GetFirstToken(self):
1058    """Return the very first token in the file."""
1059    return self._first_token
1060
1061  def IsVariableInScope(self, token_string):
1062    """Checks if string is variable in current scope.
1063
1064    For given string it checks whether the string is a defined variable
1065    (including function param) in current state.
1066
1067    E.g. if variables defined (variables in current scope) is docs
1068    then docs, docs.length etc will be considered as variable in current
1069    scope. This will help in avoding extra goog.require for variables.
1070
1071    Args:
1072      token_string: String to check if its is a variable in current scope.
1073
1074    Returns:
1075      true if given string is a variable in current scope.
1076    """
1077    for variable in self._variables_in_scope:
1078      if (token_string == variable
1079          or token_string.startswith(variable + '.')):
1080        return True
1081
1082    return False
1083
1084  def HandleToken(self, token, last_non_space_token):
1085    """Handles the given token and updates state.
1086
1087    Args:
1088      token: The token to handle.
1089      last_non_space_token:
1090    """
1091    self._is_block_close = False
1092
1093    if not self._first_token:
1094      self._first_token = token
1095
1096    # Track block depth.
1097    type = token.type
1098    if type == Type.START_BLOCK:
1099      self._block_depth += 1
1100
1101      # Subclasses need to handle block start very differently because
1102      # whether a block is a CODE or OBJECT_LITERAL block varies significantly
1103      # by language.
1104      self._block_types.append(self.GetBlockType(token))
1105
1106      # When entering a function body, record its parameters.
1107      if self.InFunction():
1108        function = self._function_stack[-1]
1109        if self._block_depth == function.block_depth + 1:
1110          function.parameters = self.GetParams()
1111
1112    # Track block depth.
1113    elif type == Type.END_BLOCK:
1114      self._is_block_close = not self.InObjectLiteral()
1115      self._block_depth -= 1
1116      self._block_types.pop()
1117
1118    # Track parentheses depth.
1119    elif type == Type.START_PAREN:
1120      self._paren_depth += 1
1121
1122    # Track parentheses depth.
1123    elif type == Type.END_PAREN:
1124      self._paren_depth -= 1
1125
1126    elif type == Type.COMMENT:
1127      self._last_comment = token.string
1128
1129    elif type == Type.START_DOC_COMMENT:
1130      self._last_comment = None
1131      self._doc_comment = DocComment(token)
1132
1133    elif type == Type.END_DOC_COMMENT:
1134      self._doc_comment.end_token = token
1135
1136    elif type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG):
1137      # Don't overwrite flags if they were already parsed in a previous pass.
1138      if token.attached_object is None:
1139        flag = self._doc_flag(token)
1140        token.attached_object = flag
1141      else:
1142        flag = token.attached_object
1143      self._doc_comment.AddFlag(flag)
1144
1145      if flag.flag_type == 'suppress':
1146        self._doc_comment.AddSuppression(token)
1147
1148    elif type == Type.FUNCTION_DECLARATION:
1149      last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, None,
1150                                         True)
1151      doc = None
1152      # Only top-level functions are eligible for documentation.
1153      if self.InTopLevel():
1154        doc = self._doc_comment
1155
1156      name = ''
1157      is_assigned = last_code and (last_code.IsOperator('=') or
1158          last_code.IsOperator('||') or last_code.IsOperator('&&') or
1159          (last_code.IsOperator(':') and not self.InObjectLiteral()))
1160      if is_assigned:
1161        # TODO(robbyw): This breaks for x[2] = ...
1162        # Must use loop to find full function name in the case of line-wrapped
1163        # declarations (bug 1220601) like:
1164        # my.function.foo.
1165        #   bar = function() ...
1166        identifier = tokenutil.Search(last_code, Type.SIMPLE_LVALUE, None, True)
1167        while identifier and tokenutil.IsIdentifierOrDot(identifier):
1168          name = identifier.string + name
1169          # Traverse behind us, skipping whitespace and comments.
1170          while True:
1171            identifier = identifier.previous
1172            if not identifier or not identifier.type in Type.NON_CODE_TYPES:
1173              break
1174
1175      else:
1176        next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
1177        while next_token and next_token.IsType(Type.FUNCTION_NAME):
1178          name += next_token.string
1179          next_token = tokenutil.Search(next_token, Type.FUNCTION_NAME, 2)
1180
1181      function = Function(self._block_depth, is_assigned, doc, name)
1182      function.start_token = token
1183
1184      self._function_stack.append(function)
1185      self._functions_by_name[name] = function
1186
1187      # Add a delimiter in stack for scope variables to define start of
1188      # function. This helps in popping variables of this function when
1189      # function declaration ends.
1190      self._variables_in_scope.append('')
1191
1192    elif type == Type.START_PARAMETERS:
1193      self._cumulative_params = ''
1194
1195    elif type == Type.PARAMETERS:
1196      self._cumulative_params += token.string
1197      self._variables_in_scope.extend(self.GetParams())
1198
1199    elif type == Type.KEYWORD and token.string == 'return':
1200      next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
1201      if not next_token.IsType(Type.SEMICOLON):
1202        function = self.GetFunction()
1203        if function:
1204          function.has_return = True
1205
1206    elif type == Type.KEYWORD and token.string == 'throw':
1207      function = self.GetFunction()
1208      if function:
1209        function.has_throw = True
1210
1211    elif type == Type.KEYWORD and token.string == 'var':
1212      function = self.GetFunction()
1213      next_token = tokenutil.Search(token, [Type.IDENTIFIER,
1214                                            Type.SIMPLE_LVALUE])
1215
1216      if next_token:
1217        if next_token.type == Type.SIMPLE_LVALUE:
1218          self._variables_in_scope.append(next_token.values['identifier'])
1219        else:
1220          self._variables_in_scope.append(next_token.string)
1221
1222    elif type == Type.SIMPLE_LVALUE:
1223      identifier = token.values['identifier']
1224      jsdoc = self.GetDocComment()
1225      if jsdoc:
1226        self._documented_identifiers.add(identifier)
1227
1228      self._HandleIdentifier(identifier, True)
1229
1230    elif type == Type.IDENTIFIER:
1231      self._HandleIdentifier(token.string, False)
1232
1233      # Detect documented non-assignments.
1234      next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES)
1235      if next_token and next_token.IsType(Type.SEMICOLON):
1236        if (self._last_non_space_token and
1237            self._last_non_space_token.IsType(Type.END_DOC_COMMENT)):
1238          self._documented_identifiers.add(token.string)
1239
1240  def _HandleIdentifier(self, identifier, is_assignment):
1241    """Process the given identifier.
1242
1243    Currently checks if it references 'this' and annotates the function
1244    accordingly.
1245
1246    Args:
1247      identifier: The identifer to process.
1248      is_assignment: Whether the identifer is being written to.
1249    """
1250    if identifier == 'this' or identifier.startswith('this.'):
1251      function = self.GetFunction()
1252      if function:
1253        function.has_this = True
1254
1255  def HandleAfterToken(self, token):
1256    """Handle updating state after a token has been checked.
1257
1258    This function should be used for destructive state changes such as
1259    deleting a tracked object.
1260
1261    Args:
1262      token: The token to handle.
1263    """
1264    type = token.type
1265    if type == Type.SEMICOLON or type == Type.END_PAREN or (
1266        type == Type.END_BRACKET and
1267        self._last_non_space_token.type not in (
1268            Type.SINGLE_QUOTE_STRING_END, Type.DOUBLE_QUOTE_STRING_END)):
1269      # We end on any numeric array index, but keep going for string based
1270      # array indices so that we pick up manually exported identifiers.
1271      self._doc_comment = None
1272      self._last_comment = None
1273
1274    elif type == Type.END_BLOCK:
1275      self._doc_comment = None
1276      self._last_comment = None
1277
1278      if self.InFunction() and self.IsFunctionClose():
1279        # TODO(robbyw): Detect the function's name for better errors.
1280        function = self._function_stack.pop()
1281        function.end_token = token
1282
1283        # Pop all variables till delimiter ('') those were defined in the
1284        # function being closed so make them out of scope.
1285        while self._variables_in_scope and self._variables_in_scope[-1]:
1286          self._variables_in_scope.pop()
1287
1288        # Pop delimiter
1289        if self._variables_in_scope:
1290          self._variables_in_scope.pop()
1291
1292    elif type == Type.END_PARAMETERS and self._doc_comment:
1293      self._doc_comment = None
1294      self._last_comment = None
1295
1296    if not token.IsAnyType(Type.WHITESPACE, Type.BLANK_LINE):
1297      self._last_non_space_token = token
1298
1299    self._last_line = token.line
1300