From 8ab7a9a438cd051c11e099ed3c7a019db8aed62d Mon Sep 17 00:00:00 2001 From: vladar Date: Mon, 25 Apr 2016 17:43:02 +0600 Subject: [PATCH] Implemented covariant return types for interface fields (#23) --- src/Schema.php | 23 ++------- src/Utils/TypeInfo.php | 111 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index 6940d45..153d337 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -14,6 +14,7 @@ use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Definition\WrappingType; use GraphQL\Type\Introspection; +use GraphQL\Utils\TypeInfo; class Schema { @@ -165,9 +166,9 @@ class Schema $objectField = $objectFieldMap[$fieldName]; Utils::invariant( - $this->_isEqualType($ifaceField->getType(), $objectField->getType()), + TypeInfo::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()), "$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " . - "$object.$fieldName provides type \"{$objectField->getType()}" + "$object.$fieldName provides type \"{$objectField->getType()}\"." ); foreach ($ifaceField->args as $ifaceArg) { @@ -185,7 +186,7 @@ class Schema // Assert interface field arg type matches object field arg type. // (invariant) Utils::invariant( - $this->_isEqualType($ifaceArg->getType(), $objectArg->getType()), + TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()), "$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " . "but $object.$fieldName($argName:) provides " . "type \"{$objectArg->getType()}\"" @@ -205,22 +206,6 @@ class Schema } } - /** - * @param $typeA - * @param $typeB - * @return bool - */ - protected function _isEqualType($typeA, $typeB) - { - if ($typeA instanceof NonNull && $typeB instanceof NonNull) { - return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); - } - if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { - return $this->_isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); - } - return $typeA === $typeB; - } - /** * @return ObjectType */ diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 145b631..9c85704 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -8,6 +8,7 @@ use GraphQL\Language\AST\NamedType; use GraphQL\Language\AST\Node; use GraphQL\Language\AST\NonNullType; use GraphQL\Schema; +use GraphQL\Type\Definition\CompositeType; use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; @@ -24,6 +25,116 @@ use GraphQL\Utils; class TypeInfo { + /** + * Provided two types, return true if the types are equal (invariant). + */ + public static function isEqualType(Type $typeA, Type $typeB) + { + // Equivalent types are equal. + if ($typeA === $typeB) { + return true; + } + + // If either type is non-null, the other must also be non-null. + if ($typeA instanceof NonNull && $typeB instanceof NonNull) { + return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + } + + // If either type is a list, the other must also be a list. + if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) { + return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType()); + } + + // Otherwise the types are not equal. + return false; + } + + /** + * Provided a type and a super type, return true if the first type is either + * equal or a subset of the second super type (covariant). + */ + static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) + { + // Equivalent type is a valid subtype + if ($maybeSubType === $superType) { + return true; + } + + // If superType is non-null, maybeSubType must also be nullable. + if ($superType instanceof NonNull) { + if ($maybeSubType instanceof NonNull) { + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); + } + return false; + } else if ($maybeSubType instanceof NonNull) { + // If superType is nullable, maybeSubType may be non-null. + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); + } + + // If superType type is a list, maybeSubType type must also be a list. + if ($superType instanceof ListOfType) { + if ($maybeSubType instanceof ListOfType) { + return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); + } + return false; + } else if ($maybeSubType instanceof ListOfType) { + // If superType is not a list, maybeSubType must also be not a list. + return false; + } + + // If superType type is an abstract type, maybeSubType type may be a currently + // possible object type. + if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) { + return true; + } + + // Otherwise, the child type is not a valid subtype of the parent type. + return false; + } + + + /** + * Provided two composite types, determine if they "overlap". Two composite + * types overlap when the Sets of possible concrete types for each intersect. + * + * This is often used to determine if a fragment of a given type could possibly + * be visited in a context of another type. + * + * This function is commutative. + */ + static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) + { + // Equivalent types overlap + if ($typeA === $typeB) { + return true; + } + + if ($typeA instanceof InterfaceType || $typeA instanceof UnionType) { + if ($typeB instanceof InterfaceType || $typeB instanceof UnionType) { + // If both types are abstract, then determine if there is any intersection + // between possible concrete types of each. + foreach ($schema->getPossibleTypes($typeA) as $type) { + if ($schema->isPossibleType($typeB, $type)) { + return true; + } + } + return false; + } + + // Determine if the latter type is a possible concrete type of the former. + return $schema->isPossibleType($typeA, $typeB); + } + + if ($typeB instanceof InterfaceType || $typeB instanceof UnionType) { + // Determine if the former type is a possible concrete type of the latter. + return $schema->isPossibleType($typeB, $typeA); + } + + // Otherwise the types do not overlap. + return false; + } + + /** * @param Schema $schema * @param $inputTypeAst