Index: third_party/closure_linter/closure_linter/typeannotation.py |
diff --git a/third_party/closure_linter/closure_linter/typeannotation.py b/third_party/closure_linter/closure_linter/typeannotation.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e8e270de241b968c5eabed5c1394499d21fe3400 |
--- /dev/null |
+++ b/third_party/closure_linter/closure_linter/typeannotation.py |
@@ -0,0 +1,410 @@ |
+#!/usr/bin/env python |
+#*-* coding: utf-8 |
+"""Closure typeannotation parsing and utilities.""" |
+ |
+ |
+ |
+from closure_linter import errors |
+from closure_linter import javascripttokens |
+from closure_linter.common import error |
+ |
+# Shorthand |
+TYPE = javascripttokens.JavaScriptTokenType |
+ |
+ |
+class TypeAnnotation(object): |
+ """Represents a structured view of a closure type annotation. |
+ |
+ Attribute: |
+ identifier: The name of the type. |
+ key_type: The name part before a colon. |
+ sub_types: The list of sub_types used e.g. for Array.<…> |
+ or_null: The '?' annotation |
+ not_null: The '!' annotation |
+ type_group: If this a a grouping (a|b), but does not include function(a). |
+ return_type: The return type of a function definition. |
+ alias: The actual type set by closurizednamespaceinfo if the identifier uses |
+ an alias to shorten the name. |
+ tokens: An ordered list of tokens used for this type. May contain |
+ TypeAnnotation instances for sub_types, key_type or return_type. |
+ """ |
+ |
+ IMPLICIT_TYPE_GROUP = 2 |
+ |
+ NULLABILITY_UNKNOWN = 2 |
+ |
+ FUNCTION_TYPE = 'function' |
+ NULL_TYPE = 'null' |
+ VAR_ARGS_TYPE = '...' |
+ |
+ # Frequently used known non-nullable types. |
+ NON_NULLABLE = frozenset([ |
+ 'boolean', FUNCTION_TYPE, 'number', 'string', 'undefined']) |
+ # Frequently used known nullable types. |
+ NULLABLE_TYPE_WHITELIST = frozenset([ |
+ 'Array', 'Document', 'Element', 'Function', 'Node', 'NodeList', |
+ 'Object']) |
+ |
+ def __init__(self): |
+ self.identifier = '' |
+ self.sub_types = [] |
+ self.or_null = False |
+ self.not_null = False |
+ self.type_group = False |
+ self.alias = None |
+ self.key_type = None |
+ self.record_type = False |
+ self.opt_arg = False |
+ self.return_type = None |
+ self.tokens = [] |
+ |
+ def IsFunction(self): |
+ """Determines whether this is a function definition.""" |
+ return self.identifier == TypeAnnotation.FUNCTION_TYPE |
+ |
+ def IsConstructor(self): |
+ """Determines whether this is a function definition for a constructor.""" |
+ key_type = self.sub_types and self.sub_types[0].key_type |
+ return self.IsFunction() and key_type.identifier == 'new' |
+ |
+ def IsRecordType(self): |
+ """Returns True if this type is a record type.""" |
+ return (self.record_type or |
+ any(t.IsRecordType() for t in self.sub_types)) |
+ |
+ def IsVarArgsType(self): |
+ """Determines if the type is a var_args type, i.e. starts with '...'.""" |
+ return self.identifier.startswith(TypeAnnotation.VAR_ARGS_TYPE) or ( |
+ self.type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP and |
+ self.sub_types[0].identifier.startswith(TypeAnnotation.VAR_ARGS_TYPE)) |
+ |
+ def IsEmpty(self): |
+ """Returns True if the type is empty.""" |
+ return not self.tokens |
+ |
+ def IsUnknownType(self): |
+ """Returns True if this is the unknown type {?}.""" |
+ return (self.or_null |
+ and not self.identifier |
+ and not self.sub_types |
+ and not self.return_type) |
+ |
+ def Append(self, item): |
+ """Adds a sub_type to this type and finalizes it. |
+ |
+ Args: |
+ item: The TypeAnnotation item to append. |
+ """ |
+ # item is a TypeAnnotation instance, so pylint: disable=protected-access |
+ self.sub_types.append(item._Finalize(self)) |
+ |
+ def __repr__(self): |
+ """Reconstructs the type definition.""" |
+ append = '' |
+ if self.sub_types: |
+ separator = (',' if not self.type_group else '|') |
+ if self.IsFunction(): |
+ surround = '(%s)' |
+ else: |
+ surround = {False: '{%s}' if self.record_type else '<%s>', |
+ True: '(%s)', |
+ TypeAnnotation.IMPLICIT_TYPE_GROUP: '%s'}[self.type_group] |
+ append = surround % separator.join(repr(t) for t in self.sub_types) |
+ if self.return_type: |
+ append += ':%s' % repr(self.return_type) |
+ append += '=' if self.opt_arg else '' |
+ prefix = '' + ('?' if self.or_null else '') + ('!' if self.not_null else '') |
+ keyword = '%s:' % repr(self.key_type) if self.key_type else '' |
+ return keyword + prefix + '%s' % (self.alias or self.identifier) + append |
+ |
+ def ToString(self): |
+ """Concats the type's tokens to form a string again.""" |
+ ret = [] |
+ for token in self.tokens: |
+ if not isinstance(token, TypeAnnotation): |
+ ret.append(token.string) |
+ else: |
+ ret.append(token.ToString()) |
+ return ''.join(ret) |
+ |
+ def Dump(self, indent=''): |
+ """Dumps this type's structure for debugging purposes.""" |
+ result = [] |
+ for t in self.tokens: |
+ if isinstance(t, TypeAnnotation): |
+ result.append(indent + str(t) + ' =>\n' + t.Dump(indent + ' ')) |
+ else: |
+ result.append(indent + str(t)) |
+ return '\n'.join(result) |
+ |
+ def IterIdentifiers(self): |
+ """Iterates over all identifiers in this type and its subtypes.""" |
+ if self.identifier: |
+ yield self.identifier |
+ for subtype in self.IterTypes(): |
+ for identifier in subtype.IterIdentifiers(): |
+ yield identifier |
+ |
+ def IterTypeGroup(self): |
+ """Iterates over all types in the type group including self. |
+ |
+ Yields: |
+ If this is a implicit or manual type-group: all sub_types. |
+ Otherwise: self |
+ E.g. for @type {Foo.<Bar>} this will yield only Foo.<Bar>, |
+ for @type {Foo|(Bar|Sample)} this will yield Foo, Bar and Sample. |
+ |
+ """ |
+ if self.type_group: |
+ for sub_type in self.sub_types: |
+ for sub_type in sub_type.IterTypeGroup(): |
+ yield sub_type |
+ else: |
+ yield self |
+ |
+ def IterTypes(self): |
+ """Iterates over each subtype as well as return and key types.""" |
+ if self.return_type: |
+ yield self.return_type |
+ |
+ if self.key_type: |
+ yield self.key_type |
+ |
+ for sub_type in self.sub_types: |
+ yield sub_type |
+ |
+ def GetNullability(self, modifiers=True): |
+ """Computes whether the type may be null. |
+ |
+ Args: |
+ modifiers: Whether the modifiers ? and ! should be considered in the |
+ evaluation. |
+ Returns: |
+ True if the type allows null, False if the type is strictly non nullable |
+ and NULLABILITY_UNKNOWN if the nullability cannot be determined. |
+ """ |
+ |
+ # Explicitly marked nullable types or 'null' are nullable. |
+ if ((modifiers and self.or_null) or |
+ self.identifier == TypeAnnotation.NULL_TYPE): |
+ return True |
+ |
+ # Explicitly marked non-nullable types or non-nullable base types: |
+ if ((modifiers and self.not_null) or self.record_type |
+ or self.identifier in TypeAnnotation.NON_NULLABLE): |
+ return False |
+ |
+ # A type group is nullable if any of its elements are nullable. |
+ if self.type_group: |
+ maybe_nullable = False |
+ for sub_type in self.sub_types: |
+ nullability = sub_type.GetNullability() |
+ if nullability == self.NULLABILITY_UNKNOWN: |
+ maybe_nullable = nullability |
+ elif nullability: |
+ return True |
+ return maybe_nullable |
+ |
+ # Whitelisted types are nullable. |
+ if self.identifier.rstrip('.') in TypeAnnotation.NULLABLE_TYPE_WHITELIST: |
+ return True |
+ |
+ # All other types are unknown (most should be nullable, but |
+ # enums are not and typedefs might not be). |
+ return TypeAnnotation.NULLABILITY_UNKNOWN |
+ |
+ def WillAlwaysBeNullable(self): |
+ """Computes whether the ! flag is illegal for this type. |
+ |
+ This is the case if this type or any of the subtypes is marked as |
+ explicitly nullable. |
+ |
+ Returns: |
+ True if the ! flag would be illegal. |
+ """ |
+ if self.or_null or self.identifier == TypeAnnotation.NULL_TYPE: |
+ return True |
+ |
+ if self.type_group: |
+ return any(t.WillAlwaysBeNullable() for t in self.sub_types) |
+ |
+ return False |
+ |
+ def _Finalize(self, parent): |
+ """Fixes some parsing issues once the TypeAnnotation is complete.""" |
+ |
+ # Normalize functions whose definition ended up in the key type because |
+ # they defined a return type after a colon. |
+ if (self.key_type and |
+ self.key_type.identifier == TypeAnnotation.FUNCTION_TYPE): |
+ current = self.key_type |
+ current.return_type = self |
+ self.key_type = None |
+ # opt_arg never refers to the return type but to the function itself. |
+ current.opt_arg = self.opt_arg |
+ self.opt_arg = False |
+ return current |
+ |
+ # If a typedef just specified the key, it will not end up in the key type. |
+ if parent.record_type and not self.key_type: |
+ current = TypeAnnotation() |
+ current.key_type = self |
+ current.tokens.append(self) |
+ return current |
+ return self |
+ |
+ def FirstToken(self): |
+ """Returns the first token used in this type or any of its subtypes.""" |
+ first = self.tokens[0] |
+ return first.FirstToken() if isinstance(first, TypeAnnotation) else first |
+ |
+ |
+def Parse(token, token_end, error_handler): |
+ """Parses a type annotation and returns a TypeAnnotation object.""" |
+ return TypeAnnotationParser(error_handler).Parse(token.next, token_end) |
+ |
+ |
+class TypeAnnotationParser(object): |
+ """A parser for type annotations constructing the TypeAnnotation object.""" |
+ |
+ def __init__(self, error_handler): |
+ self._stack = [] |
+ self._error_handler = error_handler |
+ self._closing_error = False |
+ |
+ def Parse(self, token, token_end): |
+ """Parses a type annotation and returns a TypeAnnotation object.""" |
+ root = TypeAnnotation() |
+ self._stack.append(root) |
+ current = TypeAnnotation() |
+ root.tokens.append(current) |
+ |
+ while token and token != token_end: |
+ if token.type in (TYPE.DOC_TYPE_START_BLOCK, TYPE.DOC_START_BRACE): |
+ if token.string == '(': |
+ if current.identifier and current.identifier not in [ |
+ TypeAnnotation.FUNCTION_TYPE, TypeAnnotation.VAR_ARGS_TYPE]: |
+ self.Error(token, |
+ 'Invalid identifier for (): "%s"' % current.identifier) |
+ current.type_group = ( |
+ current.identifier != TypeAnnotation.FUNCTION_TYPE) |
+ elif token.string == '{': |
+ current.record_type = True |
+ current.tokens.append(token) |
+ self._stack.append(current) |
+ current = TypeAnnotation() |
+ self._stack[-1].tokens.append(current) |
+ |
+ elif token.type in (TYPE.DOC_TYPE_END_BLOCK, TYPE.DOC_END_BRACE): |
+ prev = self._stack.pop() |
+ prev.Append(current) |
+ current = prev |
+ |
+ # If an implicit type group was created, close it as well. |
+ if prev.type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP: |
+ prev = self._stack.pop() |
+ prev.Append(current) |
+ current = prev |
+ current.tokens.append(token) |
+ |
+ elif token.type == TYPE.DOC_TYPE_MODIFIER: |
+ if token.string == '!': |
+ current.tokens.append(token) |
+ current.not_null = True |
+ elif token.string == '?': |
+ current.tokens.append(token) |
+ current.or_null = True |
+ elif token.string == ':': |
+ current.tokens.append(token) |
+ prev = current |
+ current = TypeAnnotation() |
+ prev.tokens.append(current) |
+ current.key_type = prev |
+ elif token.string == '=': |
+ # For implicit type groups the '=' refers to the parent. |
+ try: |
+ if self._stack[-1].type_group == TypeAnnotation.IMPLICIT_TYPE_GROUP: |
+ self._stack[-1].tokens.append(token) |
+ self._stack[-1].opt_arg = True |
+ else: |
+ current.tokens.append(token) |
+ current.opt_arg = True |
+ except IndexError: |
+ self.ClosingError(token) |
+ elif token.string == '|': |
+ # If a type group has explicitly been opened, do a normal append. |
+ # Otherwise we have to open the type group and move the current |
+ # type into it, before appending |
+ if not self._stack[-1].type_group: |
+ type_group = TypeAnnotation() |
+ if (current.key_type and |
+ current.key_type.identifier != TypeAnnotation.FUNCTION_TYPE): |
+ type_group.key_type = current.key_type |
+ current.key_type = None |
+ type_group.type_group = TypeAnnotation.IMPLICIT_TYPE_GROUP |
+ # Fix the token order |
+ prev = self._stack[-1].tokens.pop() |
+ self._stack[-1].tokens.append(type_group) |
+ type_group.tokens.append(prev) |
+ self._stack.append(type_group) |
+ self._stack[-1].tokens.append(token) |
+ self.Append(current, error_token=token) |
+ current = TypeAnnotation() |
+ self._stack[-1].tokens.append(current) |
+ elif token.string == ',': |
+ self.Append(current, error_token=token) |
+ current = TypeAnnotation() |
+ self._stack[-1].tokens.append(token) |
+ self._stack[-1].tokens.append(current) |
+ else: |
+ current.tokens.append(token) |
+ self.Error(token, 'Invalid token') |
+ |
+ elif token.type == TYPE.COMMENT: |
+ current.tokens.append(token) |
+ current.identifier += token.string.strip() |
+ |
+ elif token.type in [TYPE.DOC_PREFIX, TYPE.WHITESPACE]: |
+ current.tokens.append(token) |
+ |
+ else: |
+ current.tokens.append(token) |
+ self.Error(token, 'Unexpected token') |
+ |
+ token = token.next |
+ |
+ self.Append(current, error_token=token) |
+ try: |
+ ret = self._stack.pop() |
+ except IndexError: |
+ self.ClosingError(token) |
+ # The type is screwed up, but let's return something. |
+ return current |
+ |
+ if self._stack and (len(self._stack) != 1 or |
+ ret.type_group != TypeAnnotation.IMPLICIT_TYPE_GROUP): |
+ self.Error(token, 'Too many opening items.') |
+ |
+ return ret if len(ret.sub_types) > 1 else ret.sub_types[0] |
+ |
+ def Append(self, type_obj, error_token): |
+ """Appends a new TypeAnnotation object to the current parent.""" |
+ if self._stack: |
+ self._stack[-1].Append(type_obj) |
+ else: |
+ self.ClosingError(error_token) |
+ |
+ def ClosingError(self, token): |
+ """Reports an error about too many closing items, but only once.""" |
+ if not self._closing_error: |
+ self._closing_error = True |
+ self.Error(token, 'Too many closing items.') |
+ |
+ def Error(self, token, message): |
+ """Calls the error_handler to post an error message.""" |
+ if self._error_handler: |
+ self._error_handler.HandleError(error.Error( |
+ errors.JSDOC_DOES_NOT_PARSE, |
+ 'Error parsing jsdoc type at token "%s" (column: %d): %s' % |
+ (token.string, token.start_index, message), token)) |
+ |