blob: e8e270de241b968c5eabed5c1394499d21fe3400 [file] [log] [blame]
#!/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))