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