Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(63)

Side by Side Diff: third_party/closure_linter/closure_linter/statetracker.py

Issue 2592193002: Remove closure_linter from Chrome (Closed)
Patch Set: Created 3 years, 12 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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
22 import re
23
24 from closure_linter import javascripttokenizer
25 from closure_linter import javascripttokens
26 from closure_linter import tokenutil
27 from closure_linter import typeannotation
28
29 # Shorthand
30 Type = javascripttokens.JavaScriptTokenType
31
32
33 class 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
307 class 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_di stance#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
584 def _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
625 def _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
651 def _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
714 class 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
746 class 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 Type.TEMPLATE_STRING_END)):
1270 # We end on any numeric array index, but keep going for string based
1271 # array indices so that we pick up manually exported identifiers.
1272 self._doc_comment = None
1273 self._last_comment = None
1274
1275 elif type == Type.END_BLOCK:
1276 self._doc_comment = None
1277 self._last_comment = None
1278
1279 if self.InFunction() and self.IsFunctionClose():
1280 # TODO(robbyw): Detect the function's name for better errors.
1281 function = self._function_stack.pop()
1282 function.end_token = token
1283
1284 # Pop all variables till delimiter ('') those were defined in the
1285 # function being closed so make them out of scope.
1286 while self._variables_in_scope and self._variables_in_scope[-1]:
1287 self._variables_in_scope.pop()
1288
1289 # Pop delimiter
1290 if self._variables_in_scope:
1291 self._variables_in_scope.pop()
1292
1293 elif type == Type.END_PARAMETERS and self._doc_comment:
1294 self._doc_comment = None
1295 self._last_comment = None
1296
1297 if not token.IsAnyType(Type.WHITESPACE, Type.BLANK_LINE):
1298 self._last_non_space_token = token
1299
1300 self._last_line = token.line
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698