From aa4f7192fb29e31e49488c512d462252aed32062 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Thu, 26 Nov 2020 19:09:09 +0100 Subject: [PATCH 01/10] Implement support for interfaces implementing interfaces Closes #728 --- .../AST/InterfaceTypeDefinitionNode.php | 3 + .../AST/InterfaceTypeExtensionNode.php | 3 + src/Language/Parser.php | 4 + src/Language/Printer.php | 2 + src/Language/Visitor.php | 4 +- src/Type/Definition/ImplementingType.php | 21 ++++ src/Type/Definition/InterfaceType.php | 59 ++++++++++- src/Type/Definition/ObjectType.php | 2 +- src/Type/Introspection.php | 4 +- src/Type/Schema.php | 3 +- src/Type/SchemaValidationContext.php | 100 ++++++++++-------- src/Utils/ASTDefinitionBuilder.php | 8 +- src/Utils/BreakingChangesFinder.php | 6 +- src/Utils/BuildClientSchema.php | 7 ++ src/Utils/SchemaExtender.php | 10 +- src/Utils/SchemaPrinter.php | 15 ++- src/Utils/TypeComparators.php | 5 +- src/Utils/TypeInfo.php | 3 +- tests/Executor/UnionInterfaceTest.php | 2 +- tests/Language/SchemaParserTest.php | 1 + tests/Type/ValidationTest.php | 2 +- tests/Utils/BuildSchemaTest.php | 3 +- 22 files changed, 203 insertions(+), 64 deletions(-) create mode 100644 src/Type/Definition/ImplementingType.php diff --git a/src/Language/AST/InterfaceTypeDefinitionNode.php b/src/Language/AST/InterfaceTypeDefinitionNode.php index e56c38459..7ea7b1f3e 100644 --- a/src/Language/AST/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/InterfaceTypeDefinitionNode.php @@ -15,6 +15,9 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NodeList|null */ public $directives; + /** @var NodeList|null */ + public $interfaces; + /** @var NodeList|null */ public $fields; diff --git a/src/Language/AST/InterfaceTypeExtensionNode.php b/src/Language/AST/InterfaceTypeExtensionNode.php index 38e340e15..127620bd9 100644 --- a/src/Language/AST/InterfaceTypeExtensionNode.php +++ b/src/Language/AST/InterfaceTypeExtensionNode.php @@ -15,6 +15,9 @@ class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode /** @var NodeList|null */ public $directives; + /** @var NodeList|null */ + public $interfaces; + /** @var NodeList|null */ public $fields; } diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 462de219a..2698f7eab 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -1346,12 +1346,14 @@ private function parseInterfaceTypeDefinition() : InterfaceTypeDefinitionNode $description = $this->parseDescription(); $this->expectKeyword('interface'); $name = $this->parseName(); + $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); return new InterfaceTypeDefinitionNode([ 'name' => $name, 'directives' => $directives, + 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), 'description' => $description, @@ -1622,6 +1624,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode $this->expectKeyword('extend'); $this->expectKeyword('interface'); $name = $this->parseName(); + $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); if (count($directives) === 0 && @@ -1633,6 +1636,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode return new InterfaceTypeExtensionNode([ 'name' => $name, 'directives' => $directives, + 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), ]); diff --git a/src/Language/Printer.php b/src/Language/Printer.php index 03c635848..0a95efc44 100644 --- a/src/Language/Printer.php +++ b/src/Language/Printer.php @@ -309,6 +309,7 @@ function (InterfaceTypeDefinitionNode $def) : string { [ 'interface', $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields), ], @@ -401,6 +402,7 @@ function (InterfaceTypeDefinitionNode $def) : string { [ 'extend interface', $def->name, + $this->wrap('implements ', $this->join($def->interfaces, ' & ')), $this->join($def->directives, ' '), $this->block($def->fields), ], diff --git a/src/Language/Visitor.php b/src/Language/Visitor.php index 242043c02..95031e8ab 100644 --- a/src/Language/Visitor.php +++ b/src/Language/Visitor.php @@ -151,7 +151,7 @@ class Visitor NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'], NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'], - NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'], + NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'], NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'], NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'], @@ -159,7 +159,7 @@ class Visitor NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'], NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], - NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'directives', 'fields'], + NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'], NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'], NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'], NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'], diff --git a/src/Type/Definition/ImplementingType.php b/src/Type/Definition/ImplementingType.php new file mode 100644 index 000000000..38c6d10b8 --- /dev/null +++ b/src/Type/Definition/ImplementingType.php @@ -0,0 +1,21 @@ +fields = FieldDefinition::defineFieldMap($this, $fields); } + public function implementsInterface(InterfaceType $interfaceType) : bool + { + if (! isset($this->interfaceMap)) { + $this->interfaceMap = []; + foreach ($this->getInterfaces() as $interface) { + /** @var Type&InterfaceType $interface */ + $interface = Schema::resolveType($interface); + $this->interfaceMap[$interface->name] = $interface; + } + } + + return isset($this->interfaceMap[$interfaceType->name]); + } + + /** + * @return InterfaceType[] + */ + public function getInterfaces() : array + { + if (! isset($this->interfaces)) { + $interfaces = $this->config['interfaces'] ?? []; + if (is_callable($interfaces)) { + $interfaces = $interfaces(); + } + + if ($interfaces !== null && ! is_array($interfaces)) { + throw new InvariantViolation( + sprintf('%s interfaces must be an Array or a callable which returns an Array.', $this->name) + ); + } + + /** @var InterfaceType[] $interfaces */ + $interfaces = array_map([Schema::class, 'resolveType'], $interfaces ?? []); + + $this->interfaces = $interfaces; + } + + return $this->interfaces; + } + /** * Resolves concrete ObjectType for given object value * diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 199d90df1..7518bcb85 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -55,7 +55,7 @@ * } * ]); */ -class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType +class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType, ImplementingType { /** @var ObjectTypeDefinitionNode|null */ public $astNode; diff --git a/src/Type/Introspection.php b/src/Type/Introspection.php index 4d523b39f..5f9599fa4 100644 --- a/src/Type/Introspection.php +++ b/src/Type/Introspection.php @@ -359,7 +359,7 @@ static function (FieldDefinition $field) : bool { 'interfaces' => [ 'type' => Type::listOf(Type::nonNull(self::_type())), 'resolve' => static function ($type) : ?array { - if ($type instanceof ObjectType) { + if ($type instanceof ObjectType || $type instanceof InterfaceType) { return $type->getInterfaces(); } @@ -446,7 +446,7 @@ public static function _typeKind() ], 'INTERFACE' => [ 'value' => TypeKind::INTERFACE, - 'description' => 'Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.', + 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', ], 'UNION' => [ 'value' => TypeKind::UNION, diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 628d94b26..dfb048e0c 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -12,6 +12,7 @@ use GraphQL\Language\AST\SchemaTypeExtensionNode; use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\Directive; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; @@ -454,7 +455,7 @@ private function getPossibleTypeMap() : array * * @api */ - public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool + public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool { if ($abstractType instanceof InterfaceType) { return $possibleType->implementsInterface($abstractType); diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index 97dca179c..531ab4ab6 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -27,6 +27,7 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectField; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; @@ -306,7 +307,7 @@ public function validateTypes() : void $this->validateFields($type); // Ensure objects implement the interfaces they claim to. - $this->validateObjectInterfaces($type); + $this->validateTypeInterfaces($type); // Ensure directives are valid $this->validateDirectivesAtLocation( @@ -317,6 +318,9 @@ public function validateTypes() : void // Ensure fields are valid. $this->validateFields($type); + // Ensure interfaces implement the interfaces they claim to. + $this->validateTypeInterfaces($type); + // Ensure directives are valid $this->validateDirectivesAtLocation( $this->getDirectives($type), @@ -654,30 +658,33 @@ private function getFieldArgNode($type, $fieldName, $argName) return $nodes[0] ?? null; } - private function validateObjectInterfaces(ObjectType $object) + /** + * @param ObjectType|InterfaceType $type + */ + private function validateTypeInterfaces(ImplementingType $type) { $implementedTypeNames = []; - foreach ($object->getInterfaces() as $iface) { + foreach ($type->getInterfaces() as $iface) { if (! $iface instanceof InterfaceType) { $this->reportError( sprintf( 'Type %s must only implement Interface types, it cannot implement %s.', - $object->name, + $type->name, Utils::printSafe($iface) ), - $this->getImplementsInterfaceNode($object, $iface) + $this->getImplementsInterfaceNode($type, $iface) ); continue; } if (isset($implementedTypeNames[$iface->name])) { $this->reportError( - sprintf('Type %s can only implement %s once.', $object->name, $iface->name), - $this->getAllImplementsInterfaceNodes($object, $iface) + sprintf('Type %s can only implement %s once.', $type->name, $iface->name), + $this->getAllImplementsInterfaceNodes($type, $iface) ); continue; } $implementedTypeNames[$iface->name] = true; - $this->validateObjectImplementsInterface($object, $iface); + $this->validateTypeImplementsInterface($type, $iface); } } @@ -694,11 +701,12 @@ private function getDirectives($object) } /** - * @param InterfaceType $iface + * @param ObjectType|InterfaceType $type + * @param InterfaceType $iface * * @return NamedTypeNode|null */ - private function getImplementsInterfaceNode(ObjectType $type, $iface) + private function getImplementsInterfaceNode($type, $iface) { $nodes = $this->getAllImplementsInterfaceNodes($type, $iface); @@ -706,11 +714,12 @@ private function getImplementsInterfaceNode(ObjectType $type, $iface) } /** - * @param InterfaceType $iface + * @param ObjectType|InterfaceType $type + * @param InterfaceType $iface * * @return NamedTypeNode[] */ - private function getAllImplementsInterfaceNodes(ObjectType $type, $iface) + private function getAllImplementsInterfaceNodes($type, $iface) { $subNodes = $this->getAllSubNodes($type, static function ($typeNode) { return $typeNode->interfaces; @@ -722,41 +731,42 @@ private function getAllImplementsInterfaceNodes(ObjectType $type, $iface) } /** - * @param InterfaceType $iface + * @param ObjectType|InterfaceType $type + * @param InterfaceType $iface */ - private function validateObjectImplementsInterface(ObjectType $object, $iface) + private function validateTypeImplementsInterface($type, $iface) { - $objectFieldMap = $object->getFields(); - $ifaceFieldMap = $iface->getFields(); + $typeFieldMap = $type->getFields(); + $ifaceFieldMap = $iface->getFields(); // Assert each interface field is implemented. foreach ($ifaceFieldMap as $fieldName => $ifaceField) { - $objectField = array_key_exists($fieldName, $objectFieldMap) - ? $objectFieldMap[$fieldName] + $typeField = array_key_exists($fieldName, $typeFieldMap) + ? $typeFieldMap[$fieldName] : null; - // Assert interface field exists on object. - if (! $objectField) { + // Assert interface field exists on type. + if (! $typeField) { $this->reportError( sprintf( 'Interface field %s.%s expected but %s does not provide it.', $iface->name, $fieldName, - $object->name + $type->name ), array_merge( [$this->getFieldNode($iface, $fieldName)], - $this->getAllNodes($object) + $this->getAllNodes($type) ) ); continue; } - // Assert interface field type is satisfied by object field type, by being + // Assert interface field type is satisfied by type field type, by being // a valid subtype. (covariant) if (! TypeComparators::isTypeSubTypeOf( $this->schema, - $objectField->getType(), + $typeField->getType(), $ifaceField->getType() ) ) { @@ -766,52 +776,52 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) $iface->name, $fieldName, $ifaceField->getType(), - $object->name, + $type->name, $fieldName, - Utils::printSafe($objectField->getType()) + Utils::printSafe($typeField->getType()) ), [ $this->getFieldTypeNode($iface, $fieldName), - $this->getFieldTypeNode($object, $fieldName), + $this->getFieldTypeNode($type, $fieldName), ] ); } // Assert each interface field arg is implemented. foreach ($ifaceField->args as $ifaceArg) { - $argName = $ifaceArg->name; - $objectArg = null; + $argName = $ifaceArg->name; + $typeArg = null; - foreach ($objectField->args as $arg) { + foreach ($typeField->args as $arg) { if ($arg->name === $argName) { - $objectArg = $arg; + $typeArg = $arg; break; } } - // Assert interface field arg exists on object field. - if (! $objectArg) { + // Assert interface field arg exists on type field. + if (! $typeArg) { $this->reportError( sprintf( 'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.', $iface->name, $fieldName, $argName, - $object->name, + $type->name, $fieldName ), [ $this->getFieldArgNode($iface, $fieldName, $argName), - $this->getFieldNode($object, $fieldName), + $this->getFieldNode($type, $fieldName), ] ); continue; } - // Assert interface field arg type matches object field arg type. + // Assert interface field arg type matches type field arg type. // (invariant) // TODO: change to contravariant? - if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) { + if (! TypeComparators::isEqualType($ifaceArg->getType(), $typeArg->getType())) { $this->reportError( sprintf( 'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.', @@ -819,14 +829,14 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) $fieldName, $argName, Utils::printSafe($ifaceArg->getType()), - $object->name, + $type->name, $fieldName, $argName, - Utils::printSafe($objectArg->getType()) + Utils::printSafe($typeArg->getType()) ), [ $this->getFieldArgTypeNode($iface, $fieldName, $argName), - $this->getFieldArgTypeNode($object, $fieldName, $argName), + $this->getFieldArgTypeNode($type, $fieldName, $argName), ] ); } @@ -834,8 +844,8 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) } // Assert additional arguments must not be required. - foreach ($objectField->args as $objectArg) { - $argName = $objectArg->name; + foreach ($typeField->args as $typeArg) { + $argName = $typeArg->name; $ifaceArg = null; foreach ($ifaceField->args as $arg) { @@ -845,21 +855,21 @@ private function validateObjectImplementsInterface(ObjectType $object, $iface) } } - if ($ifaceArg || ! $objectArg->isRequired()) { + if ($ifaceArg || ! $typeArg->isRequired()) { continue; } $this->reportError( sprintf( 'Object field %s.%s includes required argument %s that is missing from the Interface field %s.%s.', - $object->name, + $type->name, $fieldName, $argName, $iface->name, $fieldName ), [ - $this->getFieldArgNode($object, $fieldName, $argName), + $this->getFieldArgNode($type, $fieldName, $argName), $this->getFieldNode($iface, $fieldName), ] ); diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 9f44c4ce4..091fa9e57 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -329,7 +329,10 @@ private function getDeprecationReason($node) return $deprecated['reason'] ?? null; } - private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) + /** + * @param ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode $def + */ + private function makeImplementedInterfaces($def) { if ($def->interfaces !== null) { // Note: While this could make early assertions to get the correctly @@ -356,6 +359,9 @@ private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) 'fields' => function () use ($def) { return $this->makeFieldDefMap($def); }, + 'interfaces' => function () use ($def) { + return $this->makeImplementedInterfaces($def); + }, 'astNode' => $def, ]); } diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index e0d244366..01f15455b 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -11,6 +11,7 @@ use GraphQL\Type\Definition\Directive; use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -590,7 +591,7 @@ public static function findInterfacesRemovedFromObjectTypes( foreach ($oldTypeMap as $typeName => $oldType) { $newType = $newTypeMap[$typeName] ?? null; - if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { + if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) { continue; } @@ -857,7 +858,8 @@ public static function findInterfacesAddedToObjectTypes( foreach ($newTypeMap as $typeName => $newType) { $oldType = $oldTypeMap[$typeName] ?? null; - if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { + if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || + ! ($newType instanceof ObjectType || $newType instanceof InterfaceType)) { continue; } diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index a013c1ac1..68032e475 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -319,6 +319,13 @@ private function buildInterfaceDef(array $interface) : InterfaceType 'fields' => function () use ($interface) { return $this->buildFieldDefMap($interface); }, + 'interfaces' => function () use ($interface) : array { + return array_map( + [$this, 'getInterfaceType'], + // Legacy support for interfaces with null as interfaces field + $interface['interfaces'] ?? [] + ); + }, ]); } diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 951b8c3ee..3fce58300 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -22,6 +22,7 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\EnumValueDefinition; use GraphQL\Type\Definition\FieldArgument; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ListOfType; @@ -272,9 +273,11 @@ protected static function extendPossibleTypes(UnionType $type) : array } /** + * @param ObjectType|InterfaceType $type + * * @return InterfaceType[] */ - protected static function extendImplementedInterfaces(ObjectType $type) : array + protected static function extendImplementedInterfaces(ImplementingType $type) : array { $interfaces = array_map(static function (InterfaceType $interfaceType) { return static::extendNamedType($interfaceType); @@ -282,7 +285,7 @@ protected static function extendImplementedInterfaces(ObjectType $type) : array $extensions = static::$typeExtensionsMap[$type->name] ?? null; if ($extensions !== null) { - /** @var ObjectTypeExtensionNode $extension */ + /** @var ObjectTypeExtensionNode | InterfaceTypeExtensionNode $extension */ foreach ($extensions as $extension) { foreach ($extension->interfaces as $namedType) { $interfaces[] = static::$astBuilder->buildType($namedType); @@ -400,6 +403,9 @@ protected static function extendInterfaceType(InterfaceType $type) : InterfaceTy return new InterfaceType([ 'name' => $type->name, 'description' => $type->description, + 'interfaces' => static function () use ($type) : array { + return static::extendImplementedInterfaces($type); + }, 'fields' => static function () use ($type) : array { return static::extendFieldMap($type); }, diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 32555ce43..741a30cdd 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -414,8 +414,21 @@ protected static function printDeprecated($fieldOrEnumVal) : string */ protected static function printInterface(InterfaceType $type, array $options) : string { + $interfaces = $type->getInterfaces(); + $implementedInterfaces = count($interfaces) > 0 + ? ' implements ' . implode( + ' & ', + array_map( + static function (InterfaceType $interface) : string { + return $interface->name; + }, + $interfaces + ) + ) + : ''; + return self::printDescription($options, $type) . - sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type)); + sprintf("interface %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type)); } /** diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index fccc06f1c..01210cc2b 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -6,6 +6,7 @@ use GraphQL\Type\Definition\AbstractType; use GraphQL\Type\Definition\CompositeType; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ObjectType; @@ -82,9 +83,9 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type } // If superType type is an abstract type, maybeSubType type may be a currently - // possible object type. + // possible object or interface type. return Type::isAbstractType($superType) && - $maybeSubType instanceof ObjectType && + $maybeSubType instanceof ImplementingType && $schema->isPossibleType( $superType, $maybeSubType diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 6cd11ee6b..7ae3a880e 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -25,6 +25,7 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\FieldArgument; use GraphQL\Type\Definition\FieldDefinition; +use GraphQL\Type\Definition\ImplementingType; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\InputType; use GraphQL\Type\Definition\InterfaceType; @@ -192,7 +193,7 @@ public static function extractTypes($type, ?array $typeMap = null) if ($type instanceof UnionType) { $nestedTypes = $type->getTypes(); } - if ($type instanceof ObjectType) { + if ($type instanceof ImplementingType) { $nestedTypes = array_merge($nestedTypes, $type->getInterfaces()); } if ($type instanceof ObjectType || $type instanceof InterfaceType) { diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index 7c52cb29e..b2715d42a 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -147,7 +147,7 @@ enumValues { name } 'fields' => [ ['name' => 'name'], ], - 'interfaces' => null, + 'interfaces' => [], 'possibleTypes' => [ ['name' => 'Person'], ['name' => 'Dog'], diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 7f463e5e3..a3d4912eb 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -610,6 +610,7 @@ interface Hello { $loc(21, 34) ), ], + 'interfaces' => [], 'loc' => $loc(1, 36), 'description' => null, ], diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index b60474a3e..92d647dfc 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -2195,7 +2195,7 @@ interface AnotherInterface { $schema->validate(), [[ 'message' => - 'Object field AnotherObject.field includes required argument ' . + 'Field AnotherObject.field includes required argument ' . 'requiredArg that is missing from the Interface field ' . 'AnotherInterface.field.', 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index dde89942a..ad34cbfee 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -1301,7 +1301,8 @@ interface Hello { self::assertEquals('Hello', $defaultConfig['name']); self::assertInstanceOf(Closure::class, $defaultConfig['fields']); self::assertArrayHasKey('description', $defaultConfig); - self::assertCount(4, $defaultConfig); + self::assertArrayHasKey('interfaces', $defaultConfig); + self::assertCount(5, $defaultConfig); self::assertEquals(array_keys($allNodesMap), ['Query', 'Color', 'Hello']); self::assertEquals('My description of Hello', $schema->getType('Hello')->description); } From 39c2fb687ea86da54d67ccac13903930dd27a730 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Fri, 27 Nov 2020 12:56:48 +0100 Subject: [PATCH 02/10] Implement tests for interfaces implementing interfaces This ports the JavaScript tests for `RFC: Allow interfaces to implement other interfaces` to PHP. This should ensure that there is sufficient test coverage for the changes made to support interfaces implementing interfaces. Tests taken from https://github.com/graphql/graphql-js/pull/2084/files including any typoes in test description strings to aid in comparison. --- src/Utils/BreakingChangesFinder.php | 8 +- tests/Executor/TestClasses/Cat.php | 16 +- tests/Executor/TestClasses/Dog.php | 16 +- tests/Executor/UnionInterfaceTest.php | 185 +++++- tests/Language/SchemaParserTest.php | 254 +++++++- tests/Language/SchemaPrinterTest.php | 10 +- tests/Language/schema-kitchen-sink.graphql | 10 +- tests/Type/DefinitionTest.php | 62 ++ tests/Type/IntrospectionTest.php | 2 +- tests/Type/ValidationTest.php | 600 +++++++++++++++++- tests/Utils/BreakingChangesFinderTest.php | 100 ++- tests/Utils/BuildClientSchemaTest.php | 1 - tests/Utils/BuildSchemaTest.php | 48 ++ tests/Utils/SchemaExtenderTest.php | 100 ++- tests/Utils/SchemaPrinterTest.php | 66 +- .../FragmentsOnCompositeTypesTest.php | 17 + 16 files changed, 1420 insertions(+), 75 deletions(-) diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index 01f15455b..598d7db30 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -42,14 +42,14 @@ class BreakingChangesFinder public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND'; public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED'; public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED'; - public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'INTERFACE_REMOVED_FROM_OBJECT'; + public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED'; public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED'; public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED'; public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE'; public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM'; - public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'INTERFACE_ADDED_TO_OBJECT'; + public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED'; public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED'; public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED'; @@ -609,7 +609,7 @@ static function (InterfaceType $interface) use ($oldInterface) : bool { } $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name), ]; } @@ -878,7 +878,7 @@ static function (InterfaceType $interface) use ($newInterface) : bool { } $interfacesAddedToObjectTypes[] = [ - 'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => sprintf( '%s added to interfaces implemented by %s.', $newInterface->name, diff --git a/tests/Executor/TestClasses/Cat.php b/tests/Executor/TestClasses/Cat.php index 500e6a977..b2e93884f 100644 --- a/tests/Executor/TestClasses/Cat.php +++ b/tests/Executor/TestClasses/Cat.php @@ -12,9 +12,21 @@ class Cat /** @var bool */ public $meows; + /** @var Cat|null */ + public $mother; + + /** @var Cat|null */ + public $father; + + /** @var Cat[] */ + public $progeny; + public function __construct(string $name, bool $meows) { - $this->name = $name; - $this->meows = $meows; + $this->name = $name; + $this->meows = $meows; + $this->mother = NULL; + $this->father = NULL; + $this->progeny = []; } } diff --git a/tests/Executor/TestClasses/Dog.php b/tests/Executor/TestClasses/Dog.php index 46861c38b..02c68b451 100644 --- a/tests/Executor/TestClasses/Dog.php +++ b/tests/Executor/TestClasses/Dog.php @@ -12,9 +12,21 @@ class Dog /** @var bool */ public $woofs; + /** @var Dog|null */ + public $mother; + + /** @var Dog|null */ + public $father; + + /** @var Dog[] */ + public $progeny; + public function __construct(string $name, bool $woofs) { - $this->name = $name; - $this->woofs = $woofs; + $this->name = $name; + $this->woofs = $woofs; + $this->mother = NULL; + $this->father = NULL; + $this->progeny = []; } } diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index b2715d42a..1d58c5b1d 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -8,11 +8,15 @@ use GraphQL\Error\InvariantViolation; use GraphQL\Executor\Executor; use GraphQL\GraphQL; +use GraphQL\Language\AST\FieldDefinitionNode; +use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\NodeList; use GraphQL\Language\Parser; use GraphQL\Tests\Executor\TestClasses\Cat; use GraphQL\Tests\Executor\TestClasses\Dog; use GraphQL\Tests\Executor\TestClasses\Person; use GraphQL\Type\Definition\InterfaceType; +use GraphQL\Type\Definition\ListOfType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; @@ -46,12 +50,32 @@ public function setUp() : void ], ]); + $LifeType = new InterfaceType([ + 'name' => 'Life', + 'fields' => [ + 'progeny' => ['type' => new ListOfType(function () use (&$LifeType) { return $LifeType; })], + ], + ]); + + $MammalType = new InterfaceType([ + 'name' => 'Mammal', + 'interfaces' => [$LifeType], + 'fields' => [ + 'progeny' => ['type' => new ListOfType(function () use (&$MammalType) { return $MammalType; })], + 'mother' => ['type' => &$MammalType], + 'father' => ['type' => &$MammalType], + ] + ]); + $DogType = new ObjectType([ 'name' => 'Dog', - 'interfaces' => [$NamedType], + 'interfaces' => [$MammalType, $LifeType, $NamedType], 'fields' => [ - 'name' => ['type' => Type::string()], - 'woofs' => ['type' => Type::boolean()], + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + 'progeny' => ['type' => new ListOfType(function () use (&$DogType) {return $DogType; })], + 'mother' => ['type' => &$DogType], + 'father' => ['type' => &$DogType], ], 'isTypeOf' => static function ($value) : bool { return $value instanceof Dog; @@ -60,10 +84,13 @@ public function setUp() : void $CatType = new ObjectType([ 'name' => 'Cat', - 'interfaces' => [$NamedType], + 'interfaces' => [$MammalType, $LifeType, $NamedType], 'fields' => [ - 'name' => ['type' => Type::string()], - 'meows' => ['type' => Type::boolean()], + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + 'progeny' => ['type' => new ListOfType(function () use (&$CatType) { return $CatType; })], + 'mother' => ['type' => &$CatType], + 'father' => ['type' => &$CatType], ], 'isTypeOf' => static function ($value) : bool { return $value instanceof Cat; @@ -87,11 +114,14 @@ public function setUp() : void $PersonType = new ObjectType([ 'name' => 'Person', - 'interfaces' => [$NamedType], + 'interfaces' => [$NamedType, $MammalType, $LifeType], 'fields' => [ 'name' => ['type' => Type::string()], 'pets' => ['type' => Type::listOf($PetType)], 'friends' => ['type' => Type::listOf($NamedType)], + 'progeny' => ['type' => new ListOfType(function () use (&$PersonType) { return $PersonType; })], + 'mother' => ['type' => &$PersonType], + 'father' => ['type' => &$PersonType], ], 'isTypeOf' => static function ($value) : bool { return $value instanceof Person; @@ -103,8 +133,14 @@ public function setUp() : void 'types' => [$PetType], ]); - $this->garfield = new Cat('Garfield', false); - $this->odie = new Dog('Odie', true); + $this->garfield = new Cat('Garfield', false); + $this->garfield->mother = new Cat("Garfield's Mom", false); + $this->garfield->mother->progeny = [$this->garfield]; + + $this->odie = new Dog('Odie', true); + $this->odie->mother = new Dog("Odie's Mom", true); + $this->odie->mother->progeny = [$this->odie]; + $this->liz = new Person('Liz'); $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); } @@ -127,6 +163,15 @@ interfaces { name } enumValues { name } inputFields { name } } + Mammal: __type(name: "Mammal") { + kind + name + fields { name } + interfaces { name } + possibleTypes { name } + enumValues { name } + inputFields { name } + } Pet: __type(name: "Pet") { kind name @@ -156,6 +201,25 @@ enumValues { name } 'enumValues' => null, 'inputFields' => null, ], + 'Mammal' => [ + 'kind' => 'INTERFACE', + 'name' => 'Mammal', + 'fields' => [ + ['name' => 'progeny'], + ['name' => 'mother'], + ['name' => 'father'], + ], + 'interfaces' => [ + ['name' => 'Life'] + ], + 'possibleTypes' => [ + ['name' => 'Person'], + ['name' => 'Dog'], + ['name' => 'Cat'], + ], + 'enumValues' => null, + 'inputFields' => null, + ], 'Pet' => [ 'kind' => 'UNION', 'name' => 'Pet', @@ -196,8 +260,16 @@ public function testExecutesUsingUnionTypes() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true + ], ], ], ]; @@ -233,8 +305,16 @@ public function testExecutesUnionTypesWithInlineFragments() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true + ], ], ], @@ -293,6 +373,20 @@ public function testExecutesInterfaceTypesWithInlineFragments() : void ... on Cat { meows } + + ... on Mammal { + mother { + __typename + ... on Dog { + name + woofs + } + ... on Cat { + name + meows + } + } + } } } '); @@ -301,8 +395,21 @@ public function testExecutesInterfaceTypesWithInlineFragments() : void '__typename' => 'Person', 'name' => 'John', 'friends' => [ - ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Person', + 'name' => 'Liz', + 'mother' => null, + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + 'mother' => [ + '__typename' => 'Dog', + 'name' => "Odie's Mom", + 'woofs' => true, + ], + ], ], ], ]; @@ -319,7 +426,14 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void { __typename name - pets { ...PetFields } + pets { + ...PetFields, + ...on Mammal { + mother { + ...ProgenyFields + } + } + } friends { ...FriendFields } } @@ -345,6 +459,12 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void meows } } + + fragment ProgenyFields on Life { + progeny { + __typename + } + } '); $expected = [ @@ -352,12 +472,37 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void '__typename' => 'Person', 'name' => 'John', 'pets' => [ - ['__typename' => 'Cat', 'name' => 'Garfield', 'meows' => false], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Cat', + 'name' => 'Garfield', + 'meows' => false, + 'mother' => [ + 'progeny' => [ + ['__typename' => 'Cat'], + ], + ], + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true, + 'mother' => [ + 'progeny' => [ + ['__typename' => 'Dog'], + ], + ], + ], ], 'friends' => [ - ['__typename' => 'Person', 'name' => 'Liz'], - ['__typename' => 'Dog', 'name' => 'Odie', 'woofs' => true], + [ + '__typename' => 'Person', + 'name' => 'Liz' + ], + [ + '__typename' => 'Dog', + 'name' => 'Odie', + 'woofs' => true + ], ], ], ]; diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index a3d4912eb..48666da73 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -223,9 +223,9 @@ public function testSimpleExtension() : void } /** - * @see it('Extension without fields') + * @see it('Object extension without fields') */ - public function testExtensionWithoutFields() : void + public function testObjectExtensionWithoutFields() : void { $body = 'extend type Hello implements Greeting'; $doc = Parser::parse($body); @@ -253,9 +253,39 @@ public function testExtensionWithoutFields() : void } /** - * @see it('Extension without fields followed by extension') + * @see it('Interface extension without fields') */ - public function testExtensionWithoutFieldsFollowedByExtension() : void + public function testInterfaceExtensionWithoutFields() : void + { + $body = 'extend interface Hello implements Greeting'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', $loc(17, 22)), + 'interfaces' => [ + $this->typeNode('Greeting', $loc(34, 42)), + ], + 'directives' => [], + 'fields' => [], + 'loc' => $loc(0, 42), + ], + ], + 'loc' => $loc(0, 42), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + + /** + * @see it('Object extension without fields followed by extension') + */ + public function testObjectExtensionWithoutFieldsFollowedByExtension() : void { $body = ' extend type Hello implements Greeting @@ -267,7 +297,7 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void 'kind' => 'Document', 'definitions' => [ [ - 'kind' => 'ObjectTypeExtension', + 'kind' => NodeKind::OBJECT_TYPE_EXTENSION, 'name' => $this->nameNode('Hello', ['start' => 23, 'end' => 28]), 'interfaces' => [$this->typeNode('Greeting', ['start' => 40, 'end' => 48])], 'directives' => [], @@ -275,7 +305,7 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void 'loc' => ['start' => 11, 'end' => 48], ], [ - 'kind' => 'ObjectTypeExtension', + 'kind' => NodeKind::OBJECT_TYPE_EXTENSION, 'name' => $this->nameNode('Hello', ['start' => 76, 'end' => 81]), 'interfaces' => [$this->typeNode('SecondGreeting', ['start' => 93, 'end' => 107])], 'directives' => [], @@ -289,9 +319,45 @@ public function testExtensionWithoutFieldsFollowedByExtension() : void } /** - * @see it('Extension without anything throws') + * @see it('Interface extension without fields followed by extension') + */ + public function testInterfaceExtensionWithoutFieldsFollowedByExtension() : void + { + $body = ' + extend interface Hello implements Greeting + + extend interface Hello implements SecondGreeting + '; + $doc = Parser::parse($body); + $expected = [ + 'kind' => 'Document', + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', ['start' => 28, 'end' => 33]), + 'interfaces' => [$this->typeNode('Greeting', ['start' => 45, 'end' => 53])], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 11, 'end' => 53], + ], + [ + 'kind' => NodeKind::INTERFACE_TYPE_EXTENSION, + 'name' => $this->nameNode('Hello', ['start' => 82, 'end' => 87]), + 'interfaces' => [$this->typeNode('SecondGreeting', ['start' => 99, 'end' => 113])], + 'directives' => [], + 'fields' => [], + 'loc' => ['start' => 65, 'end' => 113], + ], + ], + 'loc' => ['start' => 0, 'end' => 122], + ]; + self::assertEquals($expected, $doc->toArray(true)); + } + + /** + * @see it('Object extension without anything throws') */ - public function testExtensionWithoutAnythingThrows() : void + public function testObjectExtensionWithoutAnythingThrows() : void { $this->expectSyntaxError( 'extend type Hello', @@ -300,6 +366,18 @@ public function testExtensionWithoutAnythingThrows() : void ); } + /** + * @see it('Interface extension without anything throws') + */ + public function testInterfaceExtensionWithoutAnythingThrows() : void + { + $this->expectSyntaxError( + 'extend interface Hello', + 'Unexpected ', + $this->loc(1, 23) + ); + } + private function expectSyntaxError($text, $message, $location) { $this->expectException(SyntaxError::class); @@ -318,9 +396,9 @@ private function loc($line, $column) } /** - * @see it('Extension do not include descriptions') + * @see it('Object extension do not include descriptions') */ - public function testExtensionDoNotIncludeDescriptions() : void + public function testObjectExtensionDoNotIncludeDescriptions() : void { $body = ' "Description" @@ -335,9 +413,26 @@ public function testExtensionDoNotIncludeDescriptions() : void } /** - * @see it('Extension do not include descriptions') + * @see it('Interface extension do not include descriptions') + */ + public function testInterfaceExtensionDoNotIncludeDescriptions() : void + { + $body = ' + "Description" + extend interface Hello { + world: String + }'; + $this->expectSyntaxError( + $body, + 'Unexpected Name "extend"', + $this->loc(3, 7) + ); + } + + /** + * @see it('Object Extension do not include descriptions') */ - public function testExtensionDoNotIncludeDescriptions2() : void + public function testObjectExtensionDoNotIncludeDescriptions2() : void { $body = ' extend "Description" type Hello { @@ -351,6 +446,24 @@ public function testExtensionDoNotIncludeDescriptions2() : void ); } + + /** + * @see it('Interface Extension do not include descriptions') + */ + public function testInterfaceExtensionDoNotIncludeDescriptions2() : void + { + $body = ' + extend "Description" interface Hello { + world: String + } +}'; + $this->expectSyntaxError( + $body, + 'Unexpected String "Description"', + $this->loc(2, 14) + ); + } + /** * @see it('Simple non-null type') */ @@ -395,6 +508,44 @@ public function testSimpleNonNullType() : void self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting interface') + */ + public function testSimpleInterfaceInheritingInterface() : void + { + $body = 'interface Hello implements World { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('World', $loc(27, 32)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(35, 40)), + $this->typeNode('String', $loc(42, 48)), + $loc(35, 48) + ), + ], + 'loc' => $loc(0, 50), + 'description' => null, + ], + ], + 'loc' => $loc(0, 50), + ]; + + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Simple type inheriting interface') */ @@ -472,6 +623,45 @@ public function testSimpleTypeInheritingMultipleInterfaces() : void self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting multiple interfaces') + */ + public function testSimpleInterfaceInheritingMultipleInterfaces() : void + { + $body = 'interface Hello implements Wo & rld { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => NodeKind::DOCUMENT, + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('Wo', $loc(27, 29)), + $this->typeNode('rld', $loc(32, 35)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(38, 43)), + $this->typeNode('String', $loc(45, 51)), + $loc(38, 51) + ), + ], + 'loc' => $loc(0, 53), + 'description' => null, + ], + ], + 'loc' => $loc(0, 53), + ]; + + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Simple type inheriting multiple interfaces with leading ampersand') */ @@ -487,7 +677,7 @@ public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() 'kind' => 'Document', 'definitions' => [ [ - 'kind' => 'ObjectTypeDefinition', + 'kind' => NodeKind::OBJECT_TYPE_DEFINITION, 'name' => $this->nameNode('Hello', $loc(5, 10)), 'interfaces' => [ $this->typeNode('Wo', $loc(24, 26)), @@ -510,6 +700,44 @@ public function testSimpleTypeInheritingMultipleInterfacesWithLeadingAmpersand() self::assertEquals($expected, TestUtils::nodeToArray($doc)); } + /** + * @see it('Simple interface inheriting multiple interfaces with leading ampersand') + */ + public function testSimpleInterfaceInheritingMultipleInterfacesWithLeadingAmpersand() : void + { + $body = 'interface Hello implements & Wo & rld { field: String }'; + $doc = Parser::parse($body); + $loc = static function ($start, $end) : array { + return TestUtils::locArray($start, $end); + }; + + $expected = [ + 'kind' => 'Document', + 'definitions' => [ + [ + 'kind' => NodeKind::INTERFACE_TYPE_DEFINITION, + 'name' => $this->nameNode('Hello', $loc(10, 15)), + 'interfaces' => [ + $this->typeNode('Wo', $loc(29, 31)), + $this->typeNode('rld', $loc(34, 37)), + ], + 'directives' => [], + 'fields' => [ + $this->fieldNode( + $this->nameNode('field', $loc(40, 45)), + $this->typeNode('String', $loc(47, 53)), + $loc(40, 53) + ), + ], + 'loc' => $loc(0, 55), + 'description' => null, + ], + ], + 'loc' => $loc(0, 55), + ]; + self::assertEquals($expected, TestUtils::nodeToArray($doc)); + } + /** * @see it('Single value enum') */ diff --git a/tests/Language/SchemaPrinterTest.php b/tests/Language/SchemaPrinterTest.php index 05a326189..0c62e337d 100644 --- a/tests/Language/SchemaPrinterTest.php +++ b/tests/Language/SchemaPrinterTest.php @@ -71,7 +71,7 @@ public function testPrintsKitchenSink() : void This is a description of the `Foo` type. """ -type Foo implements Bar & Baz { +type Foo implements Bar & Baz & Two { one: Type """ This is a description of the `two` field. @@ -112,12 +112,18 @@ interface AnnotatedInterface @onInterface { interface UndefinedInterface -extend interface Bar { +extend interface Bar implements Two { two(argument: InputType!): Type } extend interface Bar @onInterface +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B diff --git a/tests/Language/schema-kitchen-sink.graphql b/tests/Language/schema-kitchen-sink.graphql index b912f06d5..2b0f54f11 100644 --- a/tests/Language/schema-kitchen-sink.graphql +++ b/tests/Language/schema-kitchen-sink.graphql @@ -12,7 +12,7 @@ schema { This is a description of the `Foo` type. """ -type Foo implements Bar & Baz { +type Foo implements Bar & Baz & Two { one: Type """ This is a description of the `two` field. @@ -53,12 +53,18 @@ interface AnnotatedInterface @onInterface { interface UndefinedInterface - extend interface Bar { +extend interface Bar implements Two{ two(argument: InputType!): Type } extend interface Bar @onInterface +interface Baz implements Bar & Two { + one: Type + two(argument: InputType!): Type + four(argument: String = "string"): String +} + union Feed = Story | Article | Advert union AnnotatedUnion @onUnion = A | B diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index 5426b4816..e4217443f 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -1138,6 +1138,68 @@ public function testAcceptsAnInterfaceTypeDefiningResolveType() : void ); } + /** + * @see it('accepts an Interface type with an array of interfaces') + */ + public function testAcceptsAnInterfaceTypeWithAnArrayOfInterfaces() : void + { + $interfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'fields' => [], + 'interfaces' => [$this->interfaceType], + ]); + self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); + } + + /** + * @see it('accepts an Interface type with interfaces as a function returning an array') + */ + public function testAcceptsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnArray() : void + { + $interfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'fields' => [], + 'interfaces' => function () { return [$this->interfaceType]; }, + ]); + self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); + } + + /** + * @see it('rejects an Interface type with incorrectly typed interfaces') + */ + public function testRejectsAnInterfaceTypeWithIncorrectlyTypedInterfaces() : void + { + $objType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'interfaces' => new stdClass(), + 'fields' => [], + ]); + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage( + 'AnotherInterface interfaces must be an Array or a callable which returns an Array.' + ); + $objType->getInterfaces(); + } + + /** + * @see it('rejects an Interface type with interfaces as a function returning an incorrect type') + */ + public function testRejectsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnIncorrectType() : void + { + $objType = new ObjectType([ + 'name' => 'AnotherInterface', + 'interfaces' => static function () : stdClass { + return new stdClass(); + }, + 'fields' => [], + ]); + $this->expectException(InvariantViolation::class); + $this->expectExceptionMessage( + 'AnotherInterface interfaces must be an Array or a callable which returns an Array.' + ); + $objType->getInterfaces(); + } + private function schemaWithFieldType($type) { $schema = new Schema([ diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index de8363172..39ae4ea35 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -1583,7 +1583,7 @@ enumValues { ], [ 'description' => 'Indicates this type is an interface. ' . - '`fields` and `possibleTypes` are valid fields.', + '`fields`, `interfaces`, and `possibleTypes` are valid fields.', 'name' => 'INTERFACE', ], [ diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 92d647dfc..15680cf9a 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -2195,7 +2195,7 @@ interface AnotherInterface { $schema->validate(), [[ 'message' => - 'Field AnotherObject.field includes required argument ' . + 'Object field AnotherObject.field includes required argument ' . 'requiredArg that is missing from the Interface field ' . 'AnotherInterface.field.', 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], @@ -2338,6 +2338,604 @@ interface AnotherInterface { ); } + /** + * @see it('rejects an Object missing a transitive interface') + */ + public function testRejectsAnObjectMissingATransitiveInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: AnotherObject + } + + interface SuperInterface { + field: String! + } + + interface AnotherInterface implements SuperInterface { + field: String! + } + + type AnotherObject implements AnotherInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type AnotherObject must implement SuperInterface ' . + 'because it is implemented by AnotherInterface.', + 'locations' => [['line' => 10, 'column' => 45], ['line' => 14, 'column' => 37]], + ], + ] + ); + } + + /** + * @see it('accepts an Interface which implements an Interface') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an Interface which implements an Interface along with more fields') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterfaceAlongWithMoreFields() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): String + anotherField: String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an Interface which implements an Interface field along with additional optional arguments') + */ + public function testAcceptsAnInterfaceWhichImplementsAnInterfaceFieldAlongWithAdditionalOptionalArguments() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String, anotherInput: String): String + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface missing an Interface field') + */ + public function testRejectsAnInterfaceMissingAnInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + anotherField: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expected ' . + 'but ChildInterface does not provide it.', + 'locations' => [['line' => 7, 'column' => 9], ['line' => 10, 'column' => 7]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with an incorrectly typed Interface field') + */ + public function testRejectsAnInterfaceWithAnIncorrectlyTypedInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: String): Int + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type Int.', + 'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 31]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with a differently typed Interface field') + */ + public function testRejectsAnInterfaceWithADifferentlyTypedInterfaceField() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + type A { foo: String } + type B { foo: String } + + interface ParentInterface { + field: A + } + + interface ChildInterface implements ParentInterface { + field: B + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type A ' . + 'but ChildInterface.field is type B.', + 'locations' => [['line' => 10, 'column' => 16], ['line' => 14, 'column' => 16]], + ], + ] + ); + } + + /** + * @see it('accepts an interface with a subtyped Interface field (interface)') + */ + public function testAcceptsAnInterfaceWithASubtypedInterfaceFieldInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: ParentInterface + } + + interface ChildInterface implements ParentInterface { + field: ChildInterface + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('accepts an interface with a subtyped Interface field (union)') + */ + public function testAcceptsAnInterfaceWithASubtypedInterfaceFieldUnion() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + type SomeObject { + field: String + } + union SomeUnionType = SomeObject + + interface ParentInterface { + field: SomeUnionType + } + + interface ChildInterface implements ParentInterface { + field: SomeObject + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with an Interface argument') + */ + public function testRejectsAnInterfaceMissingAnInterfaceArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field argument ParentInterface.field(input:) expected ' . + 'but ChildInterface.field does not provide it.', + 'locations' => [['line' => 7, 'column' => 15], ['line' => 11, 'column' => 9]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with an incorrectly typed Interface argument') + */ + public function testRejectsAnInterfaceWithAnIncorrectlyTypedInterfaceArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . + 'but ChildInterface.field(input:) is type Int.', + 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], + ], + ] + ); + } + + /** + * @see it('rejects an Interface with both an incorrectly typed field and argument') + */ + public function testRejectsAnInterfaceWithBothAnIncorrectlyTypedFieldAndArgument() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(input: String): String + } + + interface ChildInterface implements ParentInterface { + field(input: Int): Int + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type Int.', + 'locations' => [['line' => 7, 'column' => 31], ['line' => 11, 'column' => 28]], + ], + [ + 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . + 'but ChildInterface.field(input:) is type Int.', + 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], + ] + ] + ); + } + + /** + * @see it('rejects an Interface which implements an Interface field along with additional required arguments') + */ + public function testRejectsAnInterfaceWhichImplementsAnInterfaceFieldAlongWithAdditionalRequiredArguments() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field(baseArg: String): String + } + + interface ChildInterface implements ParentInterface { + field( + baseArg: String, + requiredArg: String! + optionalArg1: String, + optionalArg2: String = "", + ): String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Object field ChildInterface.field includes required argument requiredArg ' . + 'that is missing from the Interface field ParentInterface.field.', + 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], + ]] + ); + } + + /** + * @see it('accepts an Interface with an equivalently wrapped Interface field type') + */ + public function testAcceptsAnInterfaceWithAnEquivalentlyWrappedInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String]! + } + + interface ChildInterface implements ParentInterface { + field: [String]! + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with a non-list Interface field list type') + */ + public function testRejectsAnInterfaceWithANonListInterfaceFieldListType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: [String] + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type [String] ' . + 'but ChildInterface.field is type String.', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ]] + ); + } + + /** + * @see it('rejects an Interface with a list Interface field non-list type') + */ + public function testRejectsAnInterfaceWithAListInterfaceFieldNonListType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: [String] + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String ' . + 'but ChildInterface.field is type [String].', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ]] + ); + } + + /** + * @see it('accepts an Interface with a subset non-null Interface field type') + */ + public function testAcceptsAnInterfaceWithASubsetNonNullInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String + } + + interface ChildInterface implements ParentInterface { + field: String! + } + '); + + self::assertEquals([], $schema->validate()); + } + + /** + * @see it('rejects an Interface with a superset nullable interface field type') + */ + public function testRejectsAnInterfaceWithASupsersetNullableInterfaceFieldType() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface ParentInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Interface field ParentInterface.field expects type String! ' . + 'but ChildInterface.field is type String.', + 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], + ]] + ); + } + + /** + * @see it('rejects an Object missing a transitive interface') + */ + public function testRejectsAnInterfaceMissingATransitiveInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: ChildInterface + } + + interface SuperInterface { + field: String! + } + + interface ParentInterface implements SuperInterface { + field: String! + } + + interface ChildInterface implements ParentInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type ChildInterface must implement SuperInterface ' . + 'because it is implemented by ParentInterface.', + 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], + ]] + ); + } + + /** + * @see it('rejects a self reference interface') + */ + public function testRejectsASelfReferenceInterface() : void + { + $schema = BuildSchema::build(' + type Query { + test: FooInterface + } + + interface FooInterface implements FooInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [[ + 'message' => 'Type FooInterface cannot implement itself ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], + ]] + ); + } + + /** + * @see it('rejects a circulare Interface implementation') + */ + public function testRejectsACircularInterfaceImplementation() : void + { + $schema = BuildSchema::build(' + type Query { + test: FooInterface + } + + interface FooInterface implements BarInterface { + field: String! + } + + interface BarInterface implements FooInterface { + field: String! + } + '); + + $this->assertMatchesValidationMessage( + $schema->validate(), + [ + [ + 'message' => 'Type FooInterface cannot implement BarInterface ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 10, 'column' => 41], ['line' => 6, 'column' => 41]], + ], + [ + 'message' => 'Type BarInterface cannot implement FooInterface ' . + 'because it would create a circular reference.', + 'locations' => [['line' => 6, 'column' => 41], ['line' => 10, 'column' => 41]], + ], + ] + ); + } + public function testRejectsDifferentInstancesOfTheSameType() : void { // Invalid: always creates new instance vs returning one from registry diff --git a/tests/Utils/BreakingChangesFinderTest.php b/tests/Utils/BreakingChangesFinderTest.php index dc09e58fa..c681993b2 100644 --- a/tests/Utils/BreakingChangesFinderTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -1037,7 +1037,7 @@ public function testShouldDetectInterfacesRemovedFromTypes() : void $expected = [ [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => 'Type1 no longer implements interface Interface1.', ], ]; @@ -1048,6 +1048,54 @@ public function testShouldDetectInterfacesRemovedFromTypes() : void ); } + /** + * @see it('should detect interfaces removed from interfaces') + */ + public function testShouldDetectInterfacesRemovedFromInterfaces() : void + { + $interface1 = new InterfaceType([ + 'name' => 'Interface1', + 'fields' => [ + 'field1' => Type::string(), + ], + ]); + + $oldInterface2 = new InterfaceType([ + 'name' => 'Interface2', + 'fields' => [ + 'field1' => Type::string(), + ], + 'interfaces' => [$interface1], + ]); + $newInterface2 = new InterfaceType([ + 'name' => 'Interface2', + 'fields' => [ + 'field1' => Type::string(), + ], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$interface1, $oldInterface2], + ]); + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$interface1, $newInterface2], + ]); + + $expected = [ + [ + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, + 'description' => 'Interface2 no longer implements interface Interface1.', + ], + ]; + + self::assertEquals( + $expected, + BreakingChangesFinder::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) + ); + } + /** * @see it('should detect all breaking changes') */ @@ -1291,7 +1339,7 @@ public function testShouldDetectAllBreakingChanges() : void 'description' => 'ArgThatChanges.field1 arg id has changed type from Int to String', ], [ - 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED, 'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.', ], [ @@ -1659,7 +1707,7 @@ public function testShouldDetectInterfacesAddedToTypes() : void $expected = [ [ - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => 'Interface1 added to interfaces implemented by Type1.', ], ]; @@ -1670,6 +1718,50 @@ public function testShouldDetectInterfacesAddedToTypes() : void ); } + /** + * @see it('should detect interfaces added to interfaces') + */ + public function testShouldDetectInterfacesAddedToInterfaces() : void + { + $oldInterface = new InterfaceType([ + 'name' => 'OldInterface', + ]); + $newInterface = new InterfaceType([ + 'name' => 'NewInterface', + ]); + + $oldInterface1 = new InterfaceType([ + 'name' => 'Interface1', + 'interfaces' => [$oldInterface], + ]); + $newInterface1 = new InterfaceType([ + 'name' => 'Interface1', + 'interfaces' => [$oldInterface, $newInterface], + ]); + + $oldSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$oldInterface1], + ]); + + $newSchema = new Schema([ + 'query' => $this->queryType, + 'types' => [$newInterface1], + ]); + + $expected = [ + [ + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, + 'description' => 'NewInterface added to interfaces implemented by Interface1.', + ], + ]; + + self::assertEquals( + $expected, + BreakingChangesFinder::findInterfacesAddedToObjectTypes($oldSchema, $newSchema) + ); + } + /** * @see it('should detect if a type was added to a union type') */ @@ -1903,7 +1995,7 @@ public function testShouldFindAllDangerousChanges() : void 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, ], [ - 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED, 'description' => 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.', ], [ diff --git a/tests/Utils/BuildClientSchemaTest.php b/tests/Utils/BuildClientSchemaTest.php index 5592f4b14..152ad8b33 100644 --- a/tests/Utils/BuildClientSchemaTest.php +++ b/tests/Utils/BuildClientSchemaTest.php @@ -244,7 +244,6 @@ interface Friendly { */ public function testBuildsASchemaWithAnInterfaceHierarchy() : void { - self::markTestSkipped('Will work only once intermediate interfaces are possible'); self::assertCycleIntrospection(' type Dog implements Friendly & Named { bestFriend: Friendly diff --git a/tests/Utils/BuildSchemaTest.php b/tests/Utils/BuildSchemaTest.php index ad34cbfee..6ac487691 100644 --- a/tests/Utils/BuildSchemaTest.php +++ b/tests/Utils/BuildSchemaTest.php @@ -344,6 +344,32 @@ interface WorldInterface { self::assertEquals($output, $body); } + /** + * @see it('Simple interface heirarchy') + */ + public function testSimpleInterfaceHeirarchy() : void + { + $body = ' +schema { + query: Child +} + +interface Child implements Parent { + str: String +} + +type Hello implements Parent & Child { + str: String +} + +interface Parent { + str: String +} +'; + $output = $this->cycleOutput($body); + self::assertEquals($output, $body); + } + /** * @see it('Simple output enum') */ @@ -716,6 +742,28 @@ interface Iface { self::assertEquals($output, $body); } + /** + * @see it('Unreferenced interface implementing referenced interface') + */ + public function testUnreferencedInterfaceImplementingReferencedInterface() : void + { + $body = ' +interface Child implements Parent { + key: String +} + +interface Parent { + key: String +} + +type Query { + iface: Parent +} +'; + $output = $this->cycleOutput($body); + self::assertEquals($output, $body); + } + /** * @see it('Unreferenced type implementing referenced union') */ diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index ea5fbc476..1c965f894 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -70,19 +70,29 @@ public function setUp() : void 'name' => 'SomeInterface', 'fields' => static function () use (&$SomeInterfaceType) : array { return [ - 'name' => [ 'type' => Type::string()], 'some' => [ 'type' => $SomeInterfaceType], ]; }, ]); + $AnotherInterfaceType = new InterfaceType([ + 'name' => 'AnotherInterface', + 'interfaces' => [$SomeInterfaceType], + 'fields' => static function () use (&$AnotherInterfaceType) : array { + return [ + 'name' => [ 'type' => Type::string()], + 'some' => [ 'type' => $AnotherInterfaceType], + ]; + } + ]); + $FooType = new ObjectType([ 'name' => 'Foo', - 'interfaces' => [$SomeInterfaceType], - 'fields' => static function () use ($SomeInterfaceType, &$FooType) : array { + 'interfaces' => [$AnotherInterfaceType, $SomeInterfaceType], + 'fields' => static function () use ($AnotherInterfaceType, &$FooType) : array { return [ 'name' => [ 'type' => Type::string() ], - 'some' => [ 'type' => $SomeInterfaceType ], + 'some' => [ 'type' => $AnotherInterfaceType ], 'tree' => [ 'type' => Type::nonNull(Type::listOf($FooType))], ]; }, @@ -93,7 +103,6 @@ public function setUp() : void 'interfaces' => [$SomeInterfaceType], 'fields' => static function () use ($SomeInterfaceType, $FooType) : array { return [ - 'name' => [ 'type' => Type::string() ], 'some' => [ 'type' => $SomeInterfaceType ], 'foo' => [ 'type' => $FooType ], ]; @@ -345,9 +354,9 @@ public function testExtendsObjectsByAddingNewFields() self::assertEquals( $this->printTestSchemaChanges($extendedSchema), $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } @@ -781,9 +790,9 @@ public function testExtendsObjectsByAddingNewFieldsWithArguments() self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: String, arg2: NewInputObj!): String } @@ -811,9 +820,9 @@ public function testExtendsObjectsByAddingNewFieldsWithExistingTypes() self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField(arg1: SomeEnum!): SomeEnum } @@ -885,9 +894,9 @@ enum NewEnum { self::assertEquals( $this->dedent(' - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newObject: NewObject newInterface: NewInterface @@ -939,9 +948,9 @@ interface NewInterface { self::assertEquals( $this->dedent(' - type Foo implements SomeInterface & NewInterface { + type Foo implements AnotherInterface & SomeInterface & NewInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! baz: String } @@ -1059,6 +1068,10 @@ public function testExtendsInterfacesByAddingNewFields() extend interface SomeInterface { newField: String } + + extend interface AnotherInterface { + newField: String + } extend type Bar { newField: String @@ -1071,22 +1084,26 @@ public function testExtendsInterfacesByAddingNewFields() self::assertEquals( $this->dedent(' - type Bar implements SomeInterface { + interface AnotherInterface implements SomeInterface { name: String + some: AnotherInterface + newField: String + } + + type Bar implements SomeInterface { some: SomeInterface foo: Foo newField: String } - type Foo implements SomeInterface { + type Foo implements AnotherInterface & SomeInterface { name: String - some: SomeInterface + some: AnotherInterface tree: [Foo]! newField: String } interface SomeInterface { - name: String some: SomeInterface newField: String } @@ -1095,6 +1112,48 @@ interface SomeInterface { ); } + /** + * @see it('extends interfaces by adding new implemted interfaces') + */ + public function testExtendsInterfacesByAddingNewImplementedInterfaces() + { + $extendedSchema = $this->extendTestSchema(' + interface NewInterface { + newField: String + } + + extend interface AnotherInterface implements NewInterface { + newField: String + } + + extend type Foo implements NewInterface { + newField: String + } + '); + + self::assertEquals( + $this->dedent(' + interface AnotherInterface implements SomeInterface & NewInterface { + name: String + some: AnotherInterface + newField: String + } + + type Foo implements AnotherInterface & SomeInterface & NewInterface { + name: String + some: AnotherInterface + tree: [Foo]! + newField: String + } + + interface NewInterface { + newField: String + } + '), + $this->printTestSchemaChanges($extendedSchema) + ); + } + /** * @see it('allows extension of interface with missing Object fields') */ @@ -1112,7 +1171,6 @@ public function testAllowsExtensionOfInterfaceWithMissingObjectFields() self::assertEquals( $this->dedent(' interface SomeInterface { - name: String some: SomeInterface newField: String } @@ -1130,6 +1188,7 @@ public function testExtendsInterfacesMultipleTimes() extend interface SomeInterface { newFieldA: Int } + extend interface SomeInterface { newFieldB(test: Boolean): String } @@ -1138,7 +1197,6 @@ public function testExtendsInterfacesMultipleTimes() self::assertEquals( $this->dedent(' interface SomeInterface { - name: String some: SomeInterface newFieldA: Int newFieldB(test: Boolean): String diff --git a/tests/Utils/SchemaPrinterTest.php b/tests/Utils/SchemaPrinterTest.php index 9ffc51437..cd874e613 100644 --- a/tests/Utils/SchemaPrinterTest.php +++ b/tests/Utils/SchemaPrinterTest.php @@ -524,6 +524,68 @@ interface Foo { str: String } +type Query { + bar: Bar +} +', + $output + ); + } + + /** + * @see it('Print Hierarchical Interface') + */ + public function testPrintHierarchicalInterface() : void + { + $FooType = new InterfaceType([ + 'name' => 'Foo', + 'fields' => ['str' => ['type' => Type::string()]], + ]); + + $BaazType = new InterfaceType([ + 'name' => 'Baaz', + 'interfaces' => [$FooType], + 'fields' => [ + 'int' => ['type' => Type::int()], + 'str' => ['type' => Type::string()], + ], + ]); + + $BarType = new ObjectType([ + 'name' => 'Bar', + 'fields' => [ + 'str' => ['type' => Type::string()], + 'int' => ['type' => Type::int()], + ], + 'interfaces' => [$FooType, $BaazType], + ]); + + $query = new ObjectType([ + 'name' => 'Query', + 'fields' => ['bar' => ['type' => $BarType]], + ]); + + $schema = new Schema([ + 'query' => $query, + 'types' => [$BarType], + ]); + $output = $this->printForTest($schema); + self::assertEquals( + ' +interface Baaz implements Foo { + int: Int + str: String +} + +type Bar implements Foo & Baaz { + str: String + int: Int +} + +interface Foo { + str: String +} + type Query { bar: Bar } @@ -1063,7 +1125,7 @@ enum __TypeKind { OBJECT """ - Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. """ INTERFACE @@ -1283,7 +1345,7 @@ enum __TypeKind { # Indicates this type is an object. `fields` and `interfaces` are valid fields. OBJECT - # Indicates this type is an interface. `fields` and `possibleTypes` are valid fields. + # Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields. INTERFACE # Indicates this type is a union. `possibleTypes` is a valid field. diff --git a/tests/Validator/FragmentsOnCompositeTypesTest.php b/tests/Validator/FragmentsOnCompositeTypesTest.php index e8a77800a..50ad1712a 100644 --- a/tests/Validator/FragmentsOnCompositeTypesTest.php +++ b/tests/Validator/FragmentsOnCompositeTypesTest.php @@ -59,6 +59,23 @@ public function testObjectIsValidInlineFragmentType() : void ); } + /** + * @see it('interface is valid inline fragment type') + */ + public function testInterfaceIsValidInlineFragmentType() : void + { + $this->expectPassesRule( + new FragmentsOnCompositeTypes(), + ' + fragment validFragment on Mammal { + ... on Canine { + name + } + } + ' + ); + } + /** * @see it('inline fragment without type is valid') */ From 951d8829c17d27fa34fd620877d8402ce1d8250b Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Fri, 27 Nov 2020 20:03:48 +0100 Subject: [PATCH 03/10] Fix extend implement interface in Parser This is part of the update to allow interfaces to implement interfaces. A single extend statement to add an implementation of an interface without field declarations is valid. This was caught by tests and brings in a change from https://github.com/graphql/graphql-js/pull/2084 --- src/Language/Parser.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Language/Parser.php b/src/Language/Parser.php index 2698f7eab..a85f7cda5 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -1627,7 +1627,9 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); - if (count($directives) === 0 && + if ( + count($interfaces) === 0 && + count($directives) === 0 && count($fields) === 0 ) { throw $this->unexpected(); @@ -1636,7 +1638,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode return new InterfaceTypeExtensionNode([ 'name' => $name, 'directives' => $directives, - 'interfaces' => $interfaces, + 'interfaces' => $interfaces, 'fields' => $fields, 'loc' => $this->loc($start), ]); From d52514575a5a69be7879cba3306de412f0145c31 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Fri, 27 Nov 2020 20:04:59 +0100 Subject: [PATCH 04/10] Validate interface implemented ancestors Part of the work done to implement interfaces implementing interfaces. This was caught by test and improves on the previously done changes for the SchemaValidationContext by implementing `validateTypeImplementsAncestors` which was missing. --- src/Type/SchemaValidationContext.php | 44 ++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index 531ab4ab6..bcb65b8d8 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -307,7 +307,7 @@ public function validateTypes() : void $this->validateFields($type); // Ensure objects implement the interfaces they claim to. - $this->validateTypeInterfaces($type); + $this->validateInterfaces($type); // Ensure directives are valid $this->validateDirectivesAtLocation( @@ -319,7 +319,7 @@ public function validateTypes() : void $this->validateFields($type); // Ensure interfaces implement the interfaces they claim to. - $this->validateTypeInterfaces($type); + $this->validateInterfaces($type); // Ensure directives are valid $this->validateDirectivesAtLocation( @@ -661,9 +661,9 @@ private function getFieldArgNode($type, $fieldName, $argName) /** * @param ObjectType|InterfaceType $type */ - private function validateTypeInterfaces(ImplementingType $type) + private function validateInterfaces(ImplementingType $type) { - $implementedTypeNames = []; + $ifaceTypeNames = []; foreach ($type->getInterfaces() as $iface) { if (! $iface instanceof InterfaceType) { $this->reportError( @@ -676,14 +676,16 @@ private function validateTypeInterfaces(ImplementingType $type) ); continue; } - if (isset($implementedTypeNames[$iface->name])) { + if (isset($ifaceTypeNames[$iface->name])) { $this->reportError( sprintf('Type %s can only implement %s once.', $type->name, $iface->name), $this->getAllImplementsInterfaceNodes($type, $iface) ); continue; } - $implementedTypeNames[$iface->name] = true; + $ifaceTypeNames[$iface->name] = true; + + $this->validateTypeImplementsAncestors($type, $iface); $this->validateTypeImplementsInterface($type, $iface); } } @@ -877,6 +879,36 @@ private function validateTypeImplementsInterface($type, $iface) } } + /** + * @param ObjectType|InterfaceType $type + * @param InterfaceType $iface + */ + private function validateTypeImplementsAncestors(ImplementingType $type, $iface) { + $typeInterfaces = $type->getInterfaces(); + foreach ($iface->getInterfaces() as $transitive) { + if (!in_array($transitive, $typeInterfaces)) { + $this->reportError( + $transitive === $type ? + sprintf( + "Type %s cannot implement %s because it would create a circular reference.", + $type->name, + $iface->name + ) : + sprintf( + "Type %s must implement %s because it is implemented by %s.", + $type->name, + $transitive->name, + $iface->name + ), + array_merge( + $this->getAllImplementsInterfaceNodes($iface, $transitive), + $this->getAllImplementsInterfaceNodes($type, $iface) + ) + ); + } + } + } + private function validateUnionMembers(UnionType $union) { $memberTypes = $union->getTypes(); From a8f94b6a8c66b528ddc6e64272228c78d4198c4a Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Mon, 30 Nov 2020 10:23:59 +0100 Subject: [PATCH 05/10] Properly apply Schema changes for interface extension support This redoes the work done for the Schema class since it was previously guessed at. It now more closely follows graphql/graphql-js/pull/2084 --- src/Executor/ReferenceExecutor.php | 4 +- src/Experimental/Executor/Collector.php | 4 +- .../Executor/CoroutineExecutor.php | 2 +- src/Language/Parser.php | 3 +- src/Type/Schema.php | 107 ++++++++++++++---- src/Type/SchemaValidationContext.php | 57 ++++++---- src/Utils/BuildClientSchema.php | 36 +++--- src/Utils/InterfaceImplementations.php | 50 ++++++++ src/Utils/TypeComparators.php | 8 +- .../Rules/PossibleFragmentSpreads.php | 4 +- tests/Executor/TestClasses/Cat.php | 4 +- tests/Executor/TestClasses/Dog.php | 4 +- tests/Executor/UnionInterfaceTest.php | 88 +++++++------- tests/Language/SchemaParserTest.php | 1 - tests/Type/DefinitionTest.php | 4 +- tests/Type/LazyTypeLoaderTest.php | 14 ++- tests/Type/TypeLoaderTest.php | 16 ++- tests/Type/ValidationTest.php | 22 ++-- tests/Utils/BreakingChangesFinderTest.php | 8 +- tests/Utils/BuildClientSchemaTest.php | 2 +- tests/Utils/SchemaExtenderTest.php | 2 +- 21 files changed, 305 insertions(+), 135 deletions(-) create mode 100644 src/Utils/InterfaceImplementations.php diff --git a/src/Executor/ReferenceExecutor.php b/src/Executor/ReferenceExecutor.php index b5dea24b4..31b862968 100644 --- a/src/Executor/ReferenceExecutor.php +++ b/src/Executor/ReferenceExecutor.php @@ -448,7 +448,7 @@ private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : return true; } if ($conditionalType instanceof AbstractType) { - return $this->exeContext->schema->isPossibleType($conditionalType, $type); + return $this->exeContext->schema->isSubType($conditionalType, $type); } return false; @@ -1283,7 +1283,7 @@ private function ensureValidRuntimeType( ) ); } - if (! $this->exeContext->schema->isPossibleType($returnType, $runtimeType)) { + if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) { throw new InvariantViolation( sprintf('Runtime Object type "%s" is not a possible type for "%s".', $runtimeType, $returnType) ); diff --git a/src/Experimental/Executor/Collector.php b/src/Experimental/Executor/Collector.php index fe79eb9d0..dc1b49f73 100644 --- a/src/Experimental/Executor/Collector.php +++ b/src/Experimental/Executor/Collector.php @@ -244,7 +244,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel continue; } } elseif ($conditionType instanceof AbstractType) { - if (! $this->schema->isPossibleType($conditionType, $runtimeType)) { + if (! $this->schema->isSubType($conditionType, $runtimeType)) { continue; } } @@ -269,7 +269,7 @@ private function doCollectFields(ObjectType $runtimeType, ?SelectionSetNode $sel continue; } } elseif ($conditionType instanceof AbstractType) { - if (! $this->schema->isPossibleType($conditionType, $runtimeType)) { + if (! $this->schema->isSubType($conditionType, $runtimeType)) { continue; } } diff --git a/src/Experimental/Executor/CoroutineExecutor.php b/src/Experimental/Executor/CoroutineExecutor.php index f35cbb882..749810052 100644 --- a/src/Experimental/Executor/CoroutineExecutor.php +++ b/src/Experimental/Executor/CoroutineExecutor.php @@ -745,7 +745,7 @@ private function completeValue(CoroutineContext $ctx, Type $type, $value, array $returnValue = null; goto CHECKED_RETURN; - } elseif (! $this->schema->isPossibleType($type, $objectType)) { + } elseif (! $this->schema->isSubType($type, $objectType)) { $this->addError(Error::createLocatedError( new InvariantViolation(sprintf( 'Runtime Object type "%s" is not a possible type for "%s".', diff --git a/src/Language/Parser.php b/src/Language/Parser.php index a85f7cda5..e564253f0 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -1627,8 +1627,7 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); - if ( - count($interfaces) === 0 && + if (count($interfaces) === 0 && count($directives) === 0 && count($fields) === 0 ) { diff --git a/src/Type/Schema.php b/src/Type/Schema.php index dfb048e0c..649f14d7a 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -17,9 +17,11 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; use GraphQL\Type\Definition\UnionType; +use GraphQL\Utils\InterfaceImplementations; use GraphQL\Utils\TypeInfo; use GraphQL\Utils\Utils; use Traversable; +use function array_map; use function array_values; use function implode; use function is_array; @@ -63,7 +65,14 @@ class Schema * * @var array> */ - private $possibleTypeMap; + private $subTypeMap; + + /** + * Lazily initialised + * + * @var array + */ + private $implementationsMap; /** * True when $resolvedTypes contain all possible schema types @@ -417,55 +426,105 @@ public static function resolveType($type) : Type */ public function getPossibleTypes(Type $abstractType) : array { - $possibleTypeMap = $this->getPossibleTypeMap(); + return $abstractType instanceof UnionType + ? $abstractType->getTypes() + : $this->getImplementations($abstractType)->objects(); + } - return array_values($possibleTypeMap[$abstractType->name] ?? []); + /** + * Returns all types that implement a given interface type. + * + * This operations requires full schema scan. Do not use in production environment. + * + * @api + */ + public function getImplementations(InterfaceType $abstractType) : InterfaceImplementations + { + return $this->collectImplementations()[$abstractType->name]; } /** - * @return array> + * @return array */ - private function getPossibleTypeMap() : array + private function collectImplementations() : array { - if (! isset($this->possibleTypeMap)) { - $this->possibleTypeMap = []; + if (! isset($this->implementationsMap)) { + $foundImplementations = []; foreach ($this->getTypeMap() as $type) { - if ($type instanceof ObjectType) { - foreach ($type->getInterfaces() as $interface) { - if (! ($interface instanceof InterfaceType)) { - continue; - } + if ($type instanceof InterfaceType) { + if (! isset($foundImplementations[$type->name])) { + $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []]; + } - $this->possibleTypeMap[$interface->name][$type->name] = $type; + foreach ($type->getInterfaces() as $iface) { + if (! isset($foundImplementations[$iface->name])) { + $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; + } + $foundImplementations[$iface->name]['interfaces'][] = $type; } - } elseif ($type instanceof UnionType) { - foreach ($type->getTypes() as $innerType) { - $this->possibleTypeMap[$type->name][$innerType->name] = $innerType; + } elseif ($type instanceof ObjectType) { + foreach ($type->getInterfaces() as $iface) { + if (! isset($foundImplementations[$iface->name])) { + $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []]; + } + $foundImplementations[$iface->name]['objects'][] = $type; } } } + $this->implementationsMap = array_map( + static function (array $implementations) : InterfaceImplementations { + return new InterfaceImplementations($implementations['objects'], $implementations['interfaces']); + }, + $foundImplementations + ); } - return $this->possibleTypeMap; + return $this->implementationsMap; } /** + * @deprecated as of 14.4.0 use isSubType instead, will be removed in 15.0.0. + * * Returns true if object type is concrete type of given abstract type * (implementation for interfaces and members of union type for unions) * * @api + * @codeCoverageIgnore */ - public function isPossibleType(AbstractType $abstractType, ImplementingType $possibleType) : bool + public function isPossibleType(AbstractType $abstractType, ObjectType $possibleType) : bool { - if ($abstractType instanceof InterfaceType) { - return $possibleType->implementsInterface($abstractType); - } + return $this->isSubType($abstractType, $possibleType); + } - if ($abstractType instanceof UnionType) { - return $abstractType->isPossibleType($possibleType); + /** + * Returns true if maybe sub type is a sub type of given abstract type. + * + * @param UnionType|InterfaceType $abstractType + * @param ObjectType|InterfaceType $maybeSubType + * + * @api + */ + public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool + { + if (! isset($this->subTypeMap[$abstractType->name])) { + $this->subTypeMap[$abstractType->name] = []; + + if ($abstractType instanceof UnionType) { + foreach ($abstractType->getTypes() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + } else { + $implementations = $this->getImplementations($abstractType); + foreach ($implementations->objects() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + foreach ($implementations->interfaces() as $type) { + $this->subTypeMap[$abstractType->name][$type->name] = true; + } + } } - throw InvariantViolation::shouldNotHappen(); + return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]); } /** diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index bcb65b8d8..eb042123d 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -44,6 +44,7 @@ use function array_key_exists; use function array_merge; use function count; +use function in_array; use function is_array; use function is_object; use function sprintf; @@ -676,6 +677,18 @@ private function validateInterfaces(ImplementingType $type) ); continue; } + + if ($type === $iface) { + $this->reportError( + sprintf( + 'Type %s cannot implement itself because it would create a circular reference.', + $type->name + ), + $this->getImplementsInterfaceNode($type, $iface) + ); + continue; + } + if (isset($ifaceTypeNames[$iface->name])) { $this->reportError( sprintf('Type %s can only implement %s once.', $type->name, $iface->name), @@ -883,29 +896,33 @@ private function validateTypeImplementsInterface($type, $iface) * @param ObjectType|InterfaceType $type * @param InterfaceType $iface */ - private function validateTypeImplementsAncestors(ImplementingType $type, $iface) { + private function validateTypeImplementsAncestors(ImplementingType $type, $iface) + { $typeInterfaces = $type->getInterfaces(); foreach ($iface->getInterfaces() as $transitive) { - if (!in_array($transitive, $typeInterfaces)) { - $this->reportError( - $transitive === $type ? - sprintf( - "Type %s cannot implement %s because it would create a circular reference.", - $type->name, - $iface->name - ) : - sprintf( - "Type %s must implement %s because it is implemented by %s.", - $type->name, - $transitive->name, - $iface->name - ), - array_merge( - $this->getAllImplementsInterfaceNodes($iface, $transitive), - $this->getAllImplementsInterfaceNodes($type, $iface) - ) - ); + if (in_array($transitive, $typeInterfaces, true)) { + continue; } + + $error = $transitive === $type ? + sprintf( + 'Type %s cannot implement %s because it would create a circular reference.', + $type->name, + $iface->name + ) : + sprintf( + 'Type %s must implement %s because it is implemented by %s.', + $type->name, + $transitive->name, + $iface->name + ); + $this->reportError( + $error, + array_merge( + $this->getAllImplementsInterfaceNodes($iface, $transitive), + $this->getAllImplementsInterfaceNodes($type, $iface) + ) + ); } } diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index 68032e475..348d92591 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -284,23 +284,35 @@ private function buildScalarDef(array $scalar) : ScalarType } /** - * @param array $object + * @param array $implementingIntrospection */ - private function buildObjectDef(array $object) : ObjectType + private function buildImplementationsList(array $implementingIntrospection) { - if (! array_key_exists('interfaces', $object)) { - throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($object) . '.'); + // TODO: Temprorary workaround until GraphQL ecosystem will fully support + // 'interfaces' on interface types. + if (array_key_exists('interfaces', $implementingIntrospection) && + $implementingIntrospection['interfaces'] === null && + $implementingIntrospection['kind'] === TypeKind::INTERFACE) { + return []; + } + + if (! array_key_exists('interfaces', $implementingIntrospection)) { + throw new InvariantViolation('Introspection result missing interfaces: ' . json_encode($implementingIntrospection) . '.'); } + return array_map([$this, 'getInterfaceType'], $implementingIntrospection['interfaces']); + } + + /** + * @param array $object + */ + private function buildObjectDef(array $object) : ObjectType + { return new ObjectType([ 'name' => $object['name'], 'description' => $object['description'], 'interfaces' => function () use ($object) : array { - return array_map( - [$this, 'getInterfaceType'], - // Legacy support for interfaces with null as interfaces field - $object['interfaces'] ?? [] - ); + return $this->buildImplementationsList($object); }, 'fields' => function () use ($object) { return $this->buildFieldDefMap($object); @@ -320,11 +332,7 @@ private function buildInterfaceDef(array $interface) : InterfaceType return $this->buildFieldDefMap($interface); }, 'interfaces' => function () use ($interface) : array { - return array_map( - [$this, 'getInterfaceType'], - // Legacy support for interfaces with null as interfaces field - $interface['interfaces'] ?? [] - ); + return $this->buildImplementationsList($interface); }, ]); } diff --git a/src/Utils/InterfaceImplementations.php b/src/Utils/InterfaceImplementations.php new file mode 100644 index 000000000..582ad43d2 --- /dev/null +++ b/src/Utils/InterfaceImplementations.php @@ -0,0 +1,50 @@ +objects = $objects; + $this->interfaces = $interfaces; + } + + /** + * @return ObjectType[] + */ + public function objects() : array + { + return $this->objects; + } + + /** + * @return InterfaceType[] + */ + public function interfaces() : array + { + return $this->interfaces; + } +} diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 01210cc2b..7033eee70 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -86,7 +86,7 @@ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type // possible object or interface type. return Type::isAbstractType($superType) && $maybeSubType instanceof ImplementingType && - $schema->isPossibleType( + $schema->isSubType( $superType, $maybeSubType ); @@ -115,7 +115,7 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp // 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)) { + if ($schema->isSubType($typeB, $type)) { return true; } } @@ -124,12 +124,12 @@ public static function doTypesOverlap(Schema $schema, CompositeType $typeA, Comp } // Determine if the latter type is a possible concrete type of the former. - return $schema->isPossibleType($typeA, $typeB); + return $schema->isSubType($typeA, $typeB); } if ($typeB instanceof AbstractType) { // Determine if the former type is a possible concrete type of the latter. - return $schema->isPossibleType($typeB, $typeA); + return $schema->isSubType($typeB, $typeA); } // Otherwise the types do not overlap. diff --git a/src/Validator/Rules/PossibleFragmentSpreads.php b/src/Validator/Rules/PossibleFragmentSpreads.php index 184f58bec..4251400ba 100644 --- a/src/Validator/Rules/PossibleFragmentSpreads.php +++ b/src/Validator/Rules/PossibleFragmentSpreads.php @@ -68,12 +68,12 @@ private function doTypesOverlap(Schema $schema, CompositeType $fragType, Composi // Parent type is interface or union, fragment type is object type if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) { - return $schema->isPossibleType($parentType, $fragType); + return $schema->isSubType($parentType, $fragType); } // Parent type is object type, fragment type is interface (or rather rare - union) if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) { - return $schema->isPossibleType($fragType, $parentType); + return $schema->isSubType($fragType, $parentType); } // Both are object types: diff --git a/tests/Executor/TestClasses/Cat.php b/tests/Executor/TestClasses/Cat.php index b2e93884f..877386503 100644 --- a/tests/Executor/TestClasses/Cat.php +++ b/tests/Executor/TestClasses/Cat.php @@ -25,8 +25,8 @@ public function __construct(string $name, bool $meows) { $this->name = $name; $this->meows = $meows; - $this->mother = NULL; - $this->father = NULL; + $this->mother = null; + $this->father = null; $this->progeny = []; } } diff --git a/tests/Executor/TestClasses/Dog.php b/tests/Executor/TestClasses/Dog.php index 02c68b451..f60d1f364 100644 --- a/tests/Executor/TestClasses/Dog.php +++ b/tests/Executor/TestClasses/Dog.php @@ -25,8 +25,8 @@ public function __construct(string $name, bool $woofs) { $this->name = $name; $this->woofs = $woofs; - $this->mother = NULL; - $this->father = NULL; + $this->mother = null; + $this->father = null; $this->progeny = []; } } diff --git a/tests/Executor/UnionInterfaceTest.php b/tests/Executor/UnionInterfaceTest.php index 1d58c5b1d..cd47d1e6f 100644 --- a/tests/Executor/UnionInterfaceTest.php +++ b/tests/Executor/UnionInterfaceTest.php @@ -52,31 +52,37 @@ public function setUp() : void $LifeType = new InterfaceType([ 'name' => 'Life', - 'fields' => [ - 'progeny' => ['type' => new ListOfType(function () use (&$LifeType) { return $LifeType; })], - ], + 'fields' => static function () use (&$LifeType) : array { + return [ + 'progeny' => ['type' => Type::listOf($LifeType)], + ]; + }, ]); $MammalType = new InterfaceType([ 'name' => 'Mammal', 'interfaces' => [$LifeType], - 'fields' => [ - 'progeny' => ['type' => new ListOfType(function () use (&$MammalType) { return $MammalType; })], - 'mother' => ['type' => &$MammalType], - 'father' => ['type' => &$MammalType], - ] + 'fields' => static function () use (&$MammalType) : array { + return [ + 'progeny' => ['type' => Type::listOf($MammalType)], + 'mother' => ['type' => &$MammalType], + 'father' => ['type' => &$MammalType], + ]; + }, ]); $DogType = new ObjectType([ 'name' => 'Dog', 'interfaces' => [$MammalType, $LifeType, $NamedType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'woofs' => ['type' => Type::boolean()], - 'progeny' => ['type' => new ListOfType(function () use (&$DogType) {return $DogType; })], - 'mother' => ['type' => &$DogType], - 'father' => ['type' => &$DogType], - ], + 'fields' => static function () use (&$DogType) : array { + return [ + 'name' => ['type' => Type::string()], + 'woofs' => ['type' => Type::boolean()], + 'progeny' => ['type' => Type::listOf($DogType)], + 'mother' => ['type' => &$DogType], + 'father' => ['type' => &$DogType], + ]; + }, 'isTypeOf' => static function ($value) : bool { return $value instanceof Dog; }, @@ -85,13 +91,15 @@ public function setUp() : void $CatType = new ObjectType([ 'name' => 'Cat', 'interfaces' => [$MammalType, $LifeType, $NamedType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'meows' => ['type' => Type::boolean()], - 'progeny' => ['type' => new ListOfType(function () use (&$CatType) { return $CatType; })], - 'mother' => ['type' => &$CatType], - 'father' => ['type' => &$CatType], - ], + 'fields' => static function () use (&$CatType) : array { + return [ + 'name' => ['type' => Type::string()], + 'meows' => ['type' => Type::boolean()], + 'progeny' => ['type' => Type::listOf($CatType)], + 'mother' => ['type' => &$CatType], + 'father' => ['type' => &$CatType], + ]; + }, 'isTypeOf' => static function ($value) : bool { return $value instanceof Cat; }, @@ -115,14 +123,16 @@ public function setUp() : void $PersonType = new ObjectType([ 'name' => 'Person', 'interfaces' => [$NamedType, $MammalType, $LifeType], - 'fields' => [ - 'name' => ['type' => Type::string()], - 'pets' => ['type' => Type::listOf($PetType)], - 'friends' => ['type' => Type::listOf($NamedType)], - 'progeny' => ['type' => new ListOfType(function () use (&$PersonType) { return $PersonType; })], - 'mother' => ['type' => &$PersonType], - 'father' => ['type' => &$PersonType], - ], + 'fields' => static function () use (&$PetType, &$NamedType, &$PersonType) : array { + return [ + 'name' => ['type' => Type::string()], + 'pets' => ['type' => Type::listOf($PetType)], + 'friends' => ['type' => Type::listOf($NamedType)], + 'progeny' => ['type' => Type::listOf($PersonType)], + 'mother' => ['type' => $PersonType], + 'father' => ['type' => $PersonType], + ]; + }, 'isTypeOf' => static function ($value) : bool { return $value instanceof Person; }, @@ -141,8 +151,8 @@ public function setUp() : void $this->odie->mother = new Dog("Odie's Mom", true); $this->odie->mother->progeny = [$this->odie]; - $this->liz = new Person('Liz'); - $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); + $this->liz = new Person('Liz'); + $this->john = new Person('John', [$this->garfield, $this->odie], [$this->liz, $this->odie]); } // Execute: Union and intersection types @@ -210,7 +220,7 @@ enumValues { name } ['name' => 'father'], ], 'interfaces' => [ - ['name' => 'Life'] + ['name' => 'Life'], ], 'possibleTypes' => [ ['name' => 'Person'], @@ -263,12 +273,12 @@ public function testExecutesUsingUnionTypes() : void [ '__typename' => 'Cat', 'name' => 'Garfield', - 'meows' => false + 'meows' => false, ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], ], @@ -308,12 +318,12 @@ public function testExecutesUnionTypesWithInlineFragments() : void [ '__typename' => 'Cat', 'name' => 'Garfield', - 'meows' => false + 'meows' => false, ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], @@ -496,12 +506,12 @@ public function testAllowsFragmentConditionsToBeAbstractTypes() : void 'friends' => [ [ '__typename' => 'Person', - 'name' => 'Liz' + 'name' => 'Liz', ], [ '__typename' => 'Dog', 'name' => 'Odie', - 'woofs' => true + 'woofs' => true, ], ], ], diff --git a/tests/Language/SchemaParserTest.php b/tests/Language/SchemaParserTest.php index 48666da73..d03e3937a 100644 --- a/tests/Language/SchemaParserTest.php +++ b/tests/Language/SchemaParserTest.php @@ -446,7 +446,6 @@ public function testObjectExtensionDoNotIncludeDescriptions2() : void ); } - /** * @see it('Interface Extension do not include descriptions') */ diff --git a/tests/Type/DefinitionTest.php b/tests/Type/DefinitionTest.php index e4217443f..8e1513f90 100644 --- a/tests/Type/DefinitionTest.php +++ b/tests/Type/DefinitionTest.php @@ -1159,7 +1159,9 @@ public function testAcceptsAnInterfaceTypeWithInterfacesAsAFunctionReturningAnAr $interfaceType = new InterfaceType([ 'name' => 'AnotherInterface', 'fields' => [], - 'interfaces' => function () { return [$this->interfaceType]; }, + 'interfaces' => function () : array { + return [$this->interfaceType]; + }, ]); self::assertSame($this->interfaceType, $interfaceType->getInterfaces()[0]); } diff --git a/tests/Type/LazyTypeLoaderTest.php b/tests/Type/LazyTypeLoaderTest.php index efece81a6..1e208f994 100644 --- a/tests/Type/LazyTypeLoaderTest.php +++ b/tests/Type/LazyTypeLoaderTest.php @@ -285,7 +285,19 @@ public function testWorksWithTypeLoader() : void Schema::resolveType($this->blogStory) ); self::assertTrue($result); - self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + self::assertEquals( + [ + 'Node', + 'Content', + 'PostStoryMutationInput', + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ], + $this->calls + ); } public function testOnlyCallsLoaderOnce() : void diff --git a/tests/Type/TypeLoaderTest.php b/tests/Type/TypeLoaderTest.php index 516b4cb72..18ae90376 100644 --- a/tests/Type/TypeLoaderTest.php +++ b/tests/Type/TypeLoaderTest.php @@ -236,9 +236,21 @@ public function testWorksWithTypeLoader() : void self::assertSame($this->postStoryMutationInput, $input); self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); - $result = $schema->isPossibleType($this->node, $this->blogStory); + $result = $schema->isSubType($this->node, $this->blogStory); self::assertTrue($result); - self::assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls); + self::assertEquals( + [ + 'Node', + 'Content', + 'PostStoryMutationInput', + 'Query.fields', + 'Content.fields', + 'Node.fields', + 'Mutation.fields', + 'BlogStory.fields', + ], + $this->calls + ); } public function testOnlyCallsLoaderOnce() : void diff --git a/tests/Type/ValidationTest.php b/tests/Type/ValidationTest.php index 15680cf9a..aef50fc86 100644 --- a/tests/Type/ValidationTest.php +++ b/tests/Type/ValidationTest.php @@ -2672,7 +2672,7 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field argument ParentInterface.field(input:) expects type String ' . 'but ChildInterface.field(input:) is type Int.', 'locations' => [['line' => 7, 'column' => 22], ['line' => 11, 'column' => 22]], - ] + ], ] ); } @@ -2707,7 +2707,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Object field ChildInterface.field includes required argument requiredArg ' . 'that is missing from the Interface field ParentInterface.field.', 'locations' => [['line' => 13, 'column' => 11], ['line' => 7, 'column' => 9]], - ]] + ], + ] ); } @@ -2758,7 +2759,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type [String] ' . 'but ChildInterface.field is type String.', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2787,7 +2789,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type String ' . 'but ChildInterface.field is type [String].', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2838,7 +2841,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Interface field ParentInterface.field expects type String! ' . 'but ChildInterface.field is type String.', 'locations' => [['line' => 7, 'column' => 16], ['line' => 11, 'column' => 16]], - ]] + ], + ] ); } @@ -2871,7 +2875,8 @@ interface ChildInterface implements ParentInterface { 'message' => 'Type ChildInterface must implement SuperInterface ' . 'because it is implemented by ParentInterface.', 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], - ]] + ], + ] ); } @@ -2895,8 +2900,9 @@ interface FooInterface implements FooInterface { [[ 'message' => 'Type FooInterface cannot implement itself ' . 'because it would create a circular reference.', - 'locations' => [['line' => 10, 'column' => 44], ['line' => 14, 'column' => 43]], - ]] + 'locations' => [['line' => 6, 'column' => 41]], + ], + ] ); } diff --git a/tests/Utils/BreakingChangesFinderTest.php b/tests/Utils/BreakingChangesFinderTest.php index c681993b2..5253035cd 100644 --- a/tests/Utils/BreakingChangesFinderTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -1723,12 +1723,8 @@ public function testShouldDetectInterfacesAddedToTypes() : void */ public function testShouldDetectInterfacesAddedToInterfaces() : void { - $oldInterface = new InterfaceType([ - 'name' => 'OldInterface', - ]); - $newInterface = new InterfaceType([ - 'name' => 'NewInterface', - ]); + $oldInterface = new InterfaceType(['name' => 'OldInterface']); + $newInterface = new InterfaceType(['name' => 'NewInterface']); $oldInterface1 = new InterfaceType([ 'name' => 'Interface1', diff --git a/tests/Utils/BuildClientSchemaTest.php b/tests/Utils/BuildClientSchemaTest.php index 152ad8b33..528592bdc 100644 --- a/tests/Utils/BuildClientSchemaTest.php +++ b/tests/Utils/BuildClientSchemaTest.php @@ -766,7 +766,7 @@ public function testLegacySupportForInterfacesWithNullAsInterfacesField() : void $introspection = Introspection::fromSchema($dummySchema); $queryTypeIntrospection = null; foreach ($introspection['__schema']['types'] as &$type) { - if ($type['name'] !== 'Query') { + if ($type['name'] !== 'SomeInterface') { continue; } diff --git a/tests/Utils/SchemaExtenderTest.php b/tests/Utils/SchemaExtenderTest.php index 1c965f894..150f8eaba 100644 --- a/tests/Utils/SchemaExtenderTest.php +++ b/tests/Utils/SchemaExtenderTest.php @@ -83,7 +83,7 @@ public function setUp() : void 'name' => [ 'type' => Type::string()], 'some' => [ 'type' => $AnotherInterfaceType], ]; - } + }, ]); $FooType = new ObjectType([ From da07108d282b101356e54a0f0537a98d40674c34 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Mon, 30 Nov 2020 17:43:48 +0100 Subject: [PATCH 06/10] Improve interface extension related typehints Co-authored-by: Benedikt Franke --- src/Type/Definition/ImplementingType.php | 2 +- src/Type/Definition/InterfaceType.php | 6 +++--- src/Type/Definition/ObjectType.php | 6 +++--- src/Type/Schema.php | 2 +- src/Type/SchemaValidationContext.php | 10 +++------- src/Utils/ASTDefinitionBuilder.php | 8 +++++--- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Type/Definition/ImplementingType.php b/src/Type/Definition/ImplementingType.php index 38c6d10b8..9038e5ab9 100644 --- a/src/Type/Definition/ImplementingType.php +++ b/src/Type/Definition/ImplementingType.php @@ -15,7 +15,7 @@ interface ImplementingType public function implementsInterface(InterfaceType $interfaceType) : bool; /** - * @return InterfaceType[] + * @return array */ public function getInterfaces() : array; } diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index eef5e2b0f..dddcf3910 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -33,14 +33,14 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT /** * Lazily initialized. * - * @var InterfaceType[] + * @var array */ private $interfaces; /** * Lazily initialized. * - * @var InterfaceType[] + * @var array */ private $interfaceMap; @@ -131,7 +131,7 @@ public function implementsInterface(InterfaceType $interfaceType) : bool } /** - * @return InterfaceType[] + * @return array */ public function getInterfaces() : array { diff --git a/src/Type/Definition/ObjectType.php b/src/Type/Definition/ObjectType.php index 7518bcb85..f5dfd339f 100644 --- a/src/Type/Definition/ObjectType.php +++ b/src/Type/Definition/ObjectType.php @@ -76,14 +76,14 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType /** * Lazily initialized. * - * @var InterfaceType[] + * @var array */ private $interfaces; /** * Lazily initialized. * - * @var InterfaceType[] + * @var array */ private $interfaceMap; @@ -180,7 +180,7 @@ public function implementsInterface(InterfaceType $interfaceType) : bool } /** - * @return InterfaceType[] + * @return array */ public function getInterfaces() : array { diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 649f14d7a..841877475 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -497,7 +497,7 @@ public function isPossibleType(AbstractType $abstractType, ObjectType $possibleT } /** - * Returns true if maybe sub type is a sub type of given abstract type. + * Returns true if the given type is a sub type of the given abstract type. * * @param UnionType|InterfaceType $abstractType * @param ObjectType|InterfaceType $maybeSubType diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index eb042123d..c3fc3e94a 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -662,7 +662,7 @@ private function getFieldArgNode($type, $fieldName, $argName) /** * @param ObjectType|InterfaceType $type */ - private function validateInterfaces(ImplementingType $type) + private function validateInterfaces(ImplementingType $type) : void { $ifaceTypeNames = []; foreach ($type->getInterfaces() as $iface) { @@ -717,11 +717,8 @@ private function getDirectives($object) /** * @param ObjectType|InterfaceType $type - * @param InterfaceType $iface - * - * @return NamedTypeNode|null */ - private function getImplementsInterfaceNode($type, $iface) + private function getImplementsInterfaceNode($type, Type $iface) : ?NamedTypeNode { $nodes = $this->getAllImplementsInterfaceNodes($type, $iface); @@ -894,9 +891,8 @@ private function validateTypeImplementsInterface($type, $iface) /** * @param ObjectType|InterfaceType $type - * @param InterfaceType $iface */ - private function validateTypeImplementsAncestors(ImplementingType $type, $iface) + private function validateTypeImplementsAncestors(ImplementingType $type, InterfaceType $iface) : void { $typeInterfaces = $type->getInterfaces(); foreach ($iface->getInterfaces() as $transitive) { diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 091fa9e57..c977513ba 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -278,7 +278,7 @@ private function makeTypeDef(ObjectTypeDefinitionNode $def) 'fields' => function () use ($def) { return $this->makeFieldDefMap($def); }, - 'interfaces' => function () use ($def) { + 'interfaces' => function () use ($def) : ?array { return $this->makeImplementedInterfaces($def); }, 'astNode' => $def, @@ -331,8 +331,10 @@ private function getDeprecationReason($node) /** * @param ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode $def + * + * @return array | null */ - private function makeImplementedInterfaces($def) + private function makeImplementedInterfaces($def) : ?array { if ($def->interfaces !== null) { // Note: While this could make early assertions to get the correctly @@ -359,7 +361,7 @@ private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) 'fields' => function () use ($def) { return $this->makeFieldDefMap($def); }, - 'interfaces' => function () use ($def) { + 'interfaces' => function () use ($def) : ?array { return $this->makeImplementedInterfaces($def); }, 'astNode' => $def, From dc34eaeb36e2a6cf98d2cc250dcf4191d41a3922 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Sun, 13 Dec 2020 15:59:43 +0100 Subject: [PATCH 07/10] Refine types --- phpstan-baseline.neon | 6 ++--- .../AST/InterfaceTypeDefinitionNode.php | 2 +- .../AST/InterfaceTypeExtensionNode.php | 2 +- src/Language/Parser.php | 6 ++--- src/Type/Definition/ImplementingType.php | 5 ++-- src/Type/Definition/InterfaceType.php | 10 ++++---- src/Type/Schema.php | 3 ++- src/Type/SchemaValidationContext.php | 23 ++++++++----------- src/Utils/ASTDefinitionBuilder.php | 8 +++---- src/Utils/BreakingChangesFinder.php | 4 ++-- src/Utils/BuildClientSchema.php | 7 +++--- src/Utils/InterfaceImplementations.php | 14 +++++------ src/Utils/SchemaExtender.php | 4 ++-- tests/Executor/TestClasses/Cat.php | 2 +- tests/Executor/TestClasses/Dog.php | 2 +- 15 files changed, 48 insertions(+), 50 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a1c6dd677..eb52a7736 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -136,12 +136,12 @@ parameters: path: src/Type/Schema.php - - message: "#^Only booleans are allowed in a negated boolean, \\(callable\\)\\|null given\\.$#" + message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" count: 1 path: src/Type/Schema.php - - message: "#^Only booleans are allowed in an if condition, array\\ given\\.$#" + message: "#^Only booleans are allowed in a negated boolean, \\(callable\\)\\|null given\\.$#" count: 1 path: src/Type/Schema.php @@ -446,7 +446,7 @@ parameters: path: src/Validator/Rules/KnownDirectives.php - - message: "#^Only booleans are allowed in a negated boolean, array|null given\\.$#" + message: "#^Only booleans are allowed in a negated boolean, array\\|null given\\.$#" count: 1 path: src/Validator/Rules/KnownDirectives.php diff --git a/src/Language/AST/InterfaceTypeDefinitionNode.php b/src/Language/AST/InterfaceTypeDefinitionNode.php index 7ea7b1f3e..3ece2df35 100644 --- a/src/Language/AST/InterfaceTypeDefinitionNode.php +++ b/src/Language/AST/InterfaceTypeDefinitionNode.php @@ -15,7 +15,7 @@ class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode /** @var NodeList|null */ public $directives; - /** @var NodeList|null */ + /** @var NodeList */ public $interfaces; /** @var NodeList|null */ diff --git a/src/Language/AST/InterfaceTypeExtensionNode.php b/src/Language/AST/InterfaceTypeExtensionNode.php index 127620bd9..01d3a65e8 100644 --- a/src/Language/AST/InterfaceTypeExtensionNode.php +++ b/src/Language/AST/InterfaceTypeExtensionNode.php @@ -15,7 +15,7 @@ class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode /** @var NodeList|null */ public $directives; - /** @var NodeList|null */ + /** @var NodeList */ public $interfaces; /** @var NodeList|null */ diff --git a/src/Language/Parser.php b/src/Language/Parser.php index e564253f0..755aacf25 100644 --- a/src/Language/Parser.php +++ b/src/Language/Parser.php @@ -1627,9 +1627,9 @@ private function parseInterfaceTypeExtension() : InterfaceTypeExtensionNode $interfaces = $this->parseImplementsInterfaces(); $directives = $this->parseDirectives(true); $fields = $this->parseFieldsDefinition(); - if (count($interfaces) === 0 && - count($directives) === 0 && - count($fields) === 0 + if (count($interfaces) === 0 + && count($directives) === 0 + && count($fields) === 0 ) { throw $this->unexpected(); } diff --git a/src/Type/Definition/ImplementingType.php b/src/Type/Definition/ImplementingType.php index 9038e5ab9..94bf77179 100644 --- a/src/Type/Definition/ImplementingType.php +++ b/src/Type/Definition/ImplementingType.php @@ -4,12 +4,11 @@ namespace GraphQL\Type\Definition; -/* +/** export type GraphQLImplementingType = GraphQLObjectType | GraphQLInterfaceType; -*/ - + */ interface ImplementingType { public function implementsInterface(InterfaceType $interfaceType) : bool; diff --git a/src/Type/Definition/InterfaceType.php b/src/Type/Definition/InterfaceType.php index dddcf3910..845f04d0c 100644 --- a/src/Type/Definition/InterfaceType.php +++ b/src/Type/Definition/InterfaceType.php @@ -20,13 +20,13 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT /** @var InterfaceTypeDefinitionNode|null */ public $astNode; - /** @var InterfaceTypeExtensionNode[] */ + /** @var array */ public $extensionASTNodes; /** * Lazily initialized. * - * @var FieldDefinition[] + * @var array */ private $fields; @@ -147,8 +147,10 @@ public function getInterfaces() : array ); } - /** @var InterfaceType[] $interfaces */ - $interfaces = array_map([Schema::class, 'resolveType'], $interfaces ?? []); + /** @var array $interfaces */ + $interfaces = $interfaces === null + ? [] + : array_map([Schema::class, 'resolveType'], $interfaces); $this->interfaces = $interfaces; } diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 841877475..e93b519b6 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -68,7 +68,7 @@ class Schema private $subTypeMap; /** - * Lazily initialised + * Lazily initialised. * * @var array */ @@ -449,6 +449,7 @@ public function getImplementations(InterfaceType $abstractType) : InterfaceImple private function collectImplementations() : array { if (! isset($this->implementationsMap)) { + /** @var array, interfaces: array}> $foundImplementations */ $foundImplementations = []; foreach ($this->getTypeMap() as $type) { if ($type instanceof InterfaceType) { diff --git a/src/Type/SchemaValidationContext.php b/src/Type/SchemaValidationContext.php index c3fc3e94a..7c5704393 100644 --- a/src/Type/SchemaValidationContext.php +++ b/src/Type/SchemaValidationContext.php @@ -542,10 +542,8 @@ private function getAllNodes($obj) /** * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|Directive $obj - * - * @return NodeList */ - private function getAllSubNodes($obj, callable $getter) + private function getAllSubNodes($obj, callable $getter) : NodeList { $result = new NodeList([]); foreach ($this->getAllNodes($obj) as $astNode) { @@ -718,35 +716,34 @@ private function getDirectives($object) /** * @param ObjectType|InterfaceType $type */ - private function getImplementsInterfaceNode($type, Type $iface) : ?NamedTypeNode + private function getImplementsInterfaceNode(ImplementingType $type, Type $shouldBeInterface) : ?NamedTypeNode { - $nodes = $this->getAllImplementsInterfaceNodes($type, $iface); + $nodes = $this->getAllImplementsInterfaceNodes($type, $shouldBeInterface); return $nodes[0] ?? null; } /** * @param ObjectType|InterfaceType $type - * @param InterfaceType $iface * - * @return NamedTypeNode[] + * @return array */ - private function getAllImplementsInterfaceNodes($type, $iface) + private function getAllImplementsInterfaceNodes(ImplementingType $type, Type $shouldBeInterface) : array { - $subNodes = $this->getAllSubNodes($type, static function ($typeNode) { + $subNodes = $this->getAllSubNodes($type, static function (Node $typeNode) : NodeList { + /** @var ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $typeNode */ return $typeNode->interfaces; }); - return Utils::filter($subNodes, static function ($ifaceNode) use ($iface) : bool { - return $ifaceNode->name->value === $iface->name; + return Utils::filter($subNodes, static function (NamedTypeNode $ifaceNode) use ($shouldBeInterface) : bool { + return $ifaceNode->name->value === $shouldBeInterface->name; }); } /** * @param ObjectType|InterfaceType $type - * @param InterfaceType $iface */ - private function validateTypeImplementsInterface($type, $iface) + private function validateTypeImplementsInterface(ImplementingType $type, InterfaceType $iface) { $typeFieldMap = $type->getFields(); $ifaceFieldMap = $iface->getFields(); diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index c977513ba..7eadc6d33 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -318,7 +318,7 @@ public function buildField(FieldDefinitionNode $field) * Given a collection of directives, returns the string value for the * deprecation reason. * - * @param EnumValueDefinitionNode | FieldDefinitionNode $node + * @param EnumValueDefinitionNode|FieldDefinitionNode $node * * @return string */ @@ -330,11 +330,11 @@ private function getDeprecationReason($node) } /** - * @param ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode $def + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode $def * - * @return array | null + * @return array|null */ - private function makeImplementedInterfaces($def) : ?array + private function makeImplementedInterfaces(Node $def) : ?array { if ($def->interfaces !== null) { // Note: While this could make early assertions to get the correctly diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index 598d7db30..37aba752c 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -858,8 +858,8 @@ public static function findInterfacesAddedToObjectTypes( foreach ($newTypeMap as $typeName => $newType) { $oldType = $oldTypeMap[$typeName] ?? null; - if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || - ! ($newType instanceof ObjectType || $newType instanceof InterfaceType)) { + if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) + || ! ($newType instanceof ObjectType || $newType instanceof InterfaceType)) { continue; } diff --git a/src/Utils/BuildClientSchema.php b/src/Utils/BuildClientSchema.php index 348d92591..99e8988ee 100644 --- a/src/Utils/BuildClientSchema.php +++ b/src/Utils/BuildClientSchema.php @@ -285,11 +285,12 @@ private function buildScalarDef(array $scalar) : ScalarType /** * @param array $implementingIntrospection + * + * @return array */ - private function buildImplementationsList(array $implementingIntrospection) + private function buildImplementationsList(array $implementingIntrospection) : array { - // TODO: Temprorary workaround until GraphQL ecosystem will fully support - // 'interfaces' on interface types. + // TODO: Temporary workaround until GraphQL ecosystem will fully support 'interfaces' on interface types. if (array_key_exists('interfaces', $implementingIntrospection) && $implementingIntrospection['interfaces'] === null && $implementingIntrospection['kind'] === TypeKind::INTERFACE) { diff --git a/src/Utils/InterfaceImplementations.php b/src/Utils/InterfaceImplementations.php index 582ad43d2..eca7fd262 100644 --- a/src/Utils/InterfaceImplementations.php +++ b/src/Utils/InterfaceImplementations.php @@ -14,17 +14,15 @@ */ class InterfaceImplementations { - /** @var ObjectType[] */ + /** @var array */ private $objects; - /** @var InterfaceType[] */ + /** @var array */ private $interfaces; /** - * Create a new InterfaceImplementations instance. - * - * @param ObjectType[] $objects - * @param InterfaceType[] $interfaces + * @param array $objects + * @param array $interfaces */ public function __construct(array $objects, array $interfaces) { @@ -33,7 +31,7 @@ public function __construct(array $objects, array $interfaces) } /** - * @return ObjectType[] + * @return array */ public function objects() : array { @@ -41,7 +39,7 @@ public function objects() : array } /** - * @return InterfaceType[] + * @return array */ public function interfaces() : array { diff --git a/src/Utils/SchemaExtender.php b/src/Utils/SchemaExtender.php index 3fce58300..93ac77ca7 100644 --- a/src/Utils/SchemaExtender.php +++ b/src/Utils/SchemaExtender.php @@ -275,7 +275,7 @@ protected static function extendPossibleTypes(UnionType $type) : array /** * @param ObjectType|InterfaceType $type * - * @return InterfaceType[] + * @return array */ protected static function extendImplementedInterfaces(ImplementingType $type) : array { @@ -285,7 +285,7 @@ protected static function extendImplementedInterfaces(ImplementingType $type) : $extensions = static::$typeExtensionsMap[$type->name] ?? null; if ($extensions !== null) { - /** @var ObjectTypeExtensionNode | InterfaceTypeExtensionNode $extension */ + /** @var ObjectTypeExtensionNode|InterfaceTypeExtensionNode $extension */ foreach ($extensions as $extension) { foreach ($extension->interfaces as $namedType) { $interfaces[] = static::$astBuilder->buildType($namedType); diff --git a/tests/Executor/TestClasses/Cat.php b/tests/Executor/TestClasses/Cat.php index 877386503..cc3caada0 100644 --- a/tests/Executor/TestClasses/Cat.php +++ b/tests/Executor/TestClasses/Cat.php @@ -18,7 +18,7 @@ class Cat /** @var Cat|null */ public $father; - /** @var Cat[] */ + /** @var array */ public $progeny; public function __construct(string $name, bool $meows) diff --git a/tests/Executor/TestClasses/Dog.php b/tests/Executor/TestClasses/Dog.php index f60d1f364..332d00a3f 100644 --- a/tests/Executor/TestClasses/Dog.php +++ b/tests/Executor/TestClasses/Dog.php @@ -18,7 +18,7 @@ class Dog /** @var Dog|null */ public $father; - /** @var Dog[] */ + /** @var array */ public $progeny; public function __construct(string $name, bool $woofs) From 602bf4eb8ad208f5deedf43f24a3d57ce1f75d44 Mon Sep 17 00:00:00 2001 From: Benedikt Franke Date: Tue, 15 Dec 2020 02:50:34 +0100 Subject: [PATCH 08/10] Remove complex array shape type --- src/Type/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Schema.php b/src/Type/Schema.php index e93b519b6..e45207c05 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -449,7 +449,7 @@ public function getImplementations(InterfaceType $abstractType) : InterfaceImple private function collectImplementations() : array { if (! isset($this->implementationsMap)) { - /** @var array, interfaces: array}> $foundImplementations */ + /** @var array> $foundImplementations */ $foundImplementations = []; foreach ($this->getTypeMap() as $type) { if ($type instanceof InterfaceType) { From 9ed1d9d4d3c152c13636ae58595f801fca57ed8e Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Sat, 23 Jan 2021 11:52:08 +0100 Subject: [PATCH 09/10] Don't remove but deprecate DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED Removing a public constant is a breaking change and can not be implemented in a minor version. Instead the internal value is changed to ensure that existing code keeps working with the new interface implementation logic. --- src/Utils/BreakingChangesFinder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index 37aba752c..2f2b8258a 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -53,6 +53,8 @@ class BreakingChangesFinder public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED'; public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED'; + /** @deprecated use DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED instead, will be removed in v15.0.0. */ + public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'IMPLEMENTED_INTERFACE_ADDED'; /** * Given two schemas, returns an Array containing descriptions of all the types From 1f2a1e48409eb6313924f1d4926b64ddc249bfa9 Mon Sep 17 00:00:00 2001 From: Alexander Varwijk Date: Sat, 23 Jan 2021 13:02:37 +0100 Subject: [PATCH 10/10] Don't remove but deprecate BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT --- src/Utils/BreakingChangesFinder.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Utils/BreakingChangesFinder.php b/src/Utils/BreakingChangesFinder.php index 2f2b8258a..234f114d5 100644 --- a/src/Utils/BreakingChangesFinder.php +++ b/src/Utils/BreakingChangesFinder.php @@ -53,6 +53,8 @@ class BreakingChangesFinder public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION'; public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED'; public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED'; + /** @deprecated use BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED instead, will be removed in v15.0.0. */ + public const BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT = 'IMPLEMENTED_INTERFACE_REMOVED'; /** @deprecated use DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED instead, will be removed in v15.0.0. */ public const DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT = 'IMPLEMENTED_INTERFACE_ADDED';