diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 4e4a5c1..a211422 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -3,7 +3,7 @@ build: analysis: environment: php: - version: 5.6 + version: 7.1 cache: disabled: false directories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6599e..1941edf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Spec compliance: error extensions are displayed under `extensions` key - `AbstractValidationRule` renamed to `ValidationRule` (NS `GraphQL\Validator\Rules`) - `AbstractQuerySecurity` renamed to `QuerySecurityRule` (NS `GraphQL\Validator\Rules`) +- `FindBreakingChanges` renamed to `BreakingChangesFinder` (NS `GraphQL\Utils`) #### v0.12.5 - Execution performance optimization for lists diff --git a/src/Utils/AST.php b/src/Utils/AST.php index faa60f1..1f3082a 100644 --- a/src/Utils/AST.php +++ b/src/Utils/AST.php @@ -81,9 +81,8 @@ class AST * * @api * @param mixed[] $node - * @return Node */ - public static function fromArray(array $node) + public static function fromArray(array $node) : Node { if (! isset($node['kind']) || ! isset(NodeKind::$classMap[$node['kind']])) { throw new InvariantViolation('Unexpected node structure: ' . Utils::printSafeJson($node)); @@ -457,6 +456,19 @@ class AST throw new Error('Unknown type: ' . Utils::printSafe($type) . '.'); } + /** + * Returns true if the provided valueNode is a variable which is not defined + * in the set of variables. + * @param ValueNode $valueNode + * @param mixed[] $variables + * @return bool + */ + private static function isMissingVariable($valueNode, $variables) + { + return $valueNode instanceof VariableNode && + (count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables)); + } + /** * Produces a PHP value given a GraphQL Value AST. * @@ -552,19 +564,6 @@ class AST throw new Error('Unexpected type kind: ' . $inputTypeNode->kind . '.'); } - /** - * Returns true if the provided valueNode is a variable which is not defined - * in the set of variables. - * @param ValueNode $valueNode - * @param mixed[] $variables - * @return bool - */ - private static function isMissingVariable($valueNode, $variables) - { - return $valueNode instanceof VariableNode && - (count($variables) === 0 || ! array_key_exists($valueNode->name->value, $variables)); - } - /** * Returns operation type ("query", "mutation" or "subscription") given a document and operation name * diff --git a/src/Utils/ASTDefinitionBuilder.php b/src/Utils/ASTDefinitionBuilder.php index 8ca7d16..5112851 100644 --- a/src/Utils/ASTDefinitionBuilder.php +++ b/src/Utils/ASTDefinitionBuilder.php @@ -75,36 +75,112 @@ class ASTDefinitionBuilder $this->cache = Type::getAllBuiltInTypes(); } - /** - * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode - * @return Type - */ - private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) + public function buildDirective(DirectiveDefinitionNode $directiveNode) { - if ($inputTypeNode->kind === NodeKind::LIST_TYPE) { - return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); - } - if ($inputTypeNode->kind === NodeKind::NON_NULL_TYPE) { - $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); - - return Type::nonNull(NonNull::assertNullableType($wrappedType)); - } - - return $innerType; + return new Directive([ + 'name' => $directiveNode->name->value, + 'description' => $this->getDescription($directiveNode), + 'locations' => Utils::map( + $directiveNode->locations, + function ($node) { + return $node->value; + } + ), + 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, + 'astNode' => $directiveNode, + ]); } /** - * @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode - * @return TypeNode + * Given an ast node, returns its string description. */ - private function getNamedTypeNode(TypeNode $typeNode) + private function getDescription($node) { - $namedType = $typeNode; - while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { - $namedType = $namedType->type; + if ($node->description) { + return $node->description->value; + } + if (isset($this->options['commentDescriptions'])) { + $rawValue = $this->getLeadingCommentBlock($node); + if ($rawValue !== null) { + return BlockString::value("\n" . $rawValue); + } } - return $namedType; + return null; + } + + private function getLeadingCommentBlock($node) + { + $loc = $node->loc; + if (! $loc || ! $loc->startToken) { + return null; + } + $comments = []; + $token = $loc->startToken->prev; + while ($token && + $token->kind === Token::COMMENT && + $token->next && $token->prev && + $token->line + 1 === $token->next->line && + $token->line !== $token->prev->line + ) { + $value = $token->value; + $comments[] = $value; + $token = $token->prev; + } + + return implode("\n", array_reverse($comments)); + } + + private function makeInputValues($values) + { + return Utils::keyValMap( + $values, + function ($value) { + return $value->name->value; + }, + function ($value) { + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + $type = $this->internalBuildWrappedType($value->type); + $config = [ + 'name' => $value->name->value, + 'type' => $type, + 'description' => $this->getDescription($value), + 'astNode' => $value, + ]; + if (isset($value->defaultValue)) { + $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); + } + + return $config; + } + ); + } + + /** + * @return Type|InputType + * @throws Error + */ + private function internalBuildWrappedType(TypeNode $typeNode) + { + $typeDef = $this->buildType($this->getNamedTypeNode($typeNode)); + + return $this->buildWrappedType($typeDef, $typeNode); + } + + /** + * @param string|NamedTypeNode $ref + * @return Type + * @throws Error + */ + public function buildType($ref) + { + if (is_string($ref)) { + return $this->internalBuildType($ref); + } + + return $this->internalBuildType($ref->name->value, $ref); } /** @@ -154,61 +230,6 @@ class ASTDefinitionBuilder return $this->cache[$typeName]; } - /** - * @param string|NamedTypeNode $ref - * @return Type - * @throws Error - */ - public function buildType($ref) - { - if (is_string($ref)) { - return $this->internalBuildType($ref); - } - - return $this->internalBuildType($ref->name->value, $ref); - } - - /** - * @return Type|InputType - * @throws Error - */ - private function internalBuildWrappedType(TypeNode $typeNode) - { - $typeDef = $this->buildType($this->getNamedTypeNode($typeNode)); - - return $this->buildWrappedType($typeDef, $typeNode); - } - - public function buildDirective(DirectiveDefinitionNode $directiveNode) - { - return new Directive([ - 'name' => $directiveNode->name->value, - 'description' => $this->getDescription($directiveNode), - 'locations' => Utils::map( - $directiveNode->locations, - function ($node) { - return $node->value; - } - ), - 'args' => $directiveNode->arguments ? FieldArgument::createMap($this->makeInputValues($directiveNode->arguments)) : null, - 'astNode' => $directiveNode, - ]); - } - - public function buildField(FieldDefinitionNode $field) - { - return [ - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - 'type' => $this->internalBuildWrappedType($field->type), - 'description' => $this->getDescription($field), - 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, - 'deprecationReason' => $this->getDeprecationReason($field), - 'astNode' => $field, - ]; - } - /** * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeDefinitionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode|UnionTypeDefinitionNode $def * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType @@ -237,35 +258,6 @@ class ASTDefinitionBuilder } } - /** - * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeExtensionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode $def - * @param mixed[] $config - * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType - * @throws Error - */ - private function makeSchemaDefFromConfig($def, array $config) - { - if (! $def) { - throw new Error('def must be defined.'); - } - switch ($def->kind) { - case NodeKind::OBJECT_TYPE_DEFINITION: - return new ObjectType($config); - case NodeKind::INTERFACE_TYPE_DEFINITION: - return new InterfaceType($config); - case NodeKind::ENUM_TYPE_DEFINITION: - return new EnumType($config); - case NodeKind::UNION_TYPE_DEFINITION: - return new UnionType($config); - case NodeKind::SCALAR_TYPE_DEFINITION: - return new CustomScalarType($config); - case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: - return new InputObjectType($config); - default: - throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); - } - } - private function makeTypeDef(ObjectTypeDefinitionNode $def) { $typeName = $def->name->value; @@ -298,6 +290,34 @@ class ASTDefinitionBuilder : []; } + public function buildField(FieldDefinitionNode $field) + { + return [ + // Note: While this could make assertions to get the correctly typed + // value, that would throw immediately while type system validation + // with validateSchema() will produce more actionable results. + 'type' => $this->internalBuildWrappedType($field->type), + 'description' => $this->getDescription($field), + 'args' => $field->arguments ? $this->makeInputValues($field->arguments) : null, + 'deprecationReason' => $this->getDeprecationReason($field), + 'astNode' => $field, + ]; + } + + /** + * Given a collection of directives, returns the string value for the + * deprecation reason. + * + * @param EnumValueDefinitionNode | FieldDefinitionNode $node + * @return string + */ + private function getDeprecationReason($node) + { + $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); + + return $deprecated['reason'] ?? null; + } + private function makeImplementedInterfaces(ObjectTypeDefinitionNode $def) { if ($def->interfaces) { @@ -315,33 +335,6 @@ class ASTDefinitionBuilder return null; } - private function makeInputValues($values) - { - return Utils::keyValMap( - $values, - function ($value) { - return $value->name->value; - }, - function ($value) { - // Note: While this could make assertions to get the correctly typed - // value, that would throw immediately while type system validation - // with validateSchema() will produce more actionable results. - $type = $this->internalBuildWrappedType($value->type); - $config = [ - 'name' => $value->name->value, - 'type' => $type, - 'description' => $this->getDescription($value), - 'astNode' => $value, - ]; - if (isset($value->defaultValue)) { - $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type); - } - - return $config; - } - ); - } - private function makeInterfaceDef(InterfaceTypeDefinitionNode $def) { $typeName = $def->name->value; @@ -427,56 +420,63 @@ class ASTDefinitionBuilder } /** - * Given a collection of directives, returns the string value for the - * deprecation reason. - * - * @param EnumValueDefinitionNode | FieldDefinitionNode $node - * @return string + * @param ObjectTypeDefinitionNode|InterfaceTypeDefinitionNode|EnumTypeExtensionNode|ScalarTypeDefinitionNode|InputObjectTypeDefinitionNode $def + * @param mixed[] $config + * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType + * @throws Error */ - private function getDeprecationReason($node) + private function makeSchemaDefFromConfig($def, array $config) { - $deprecated = Values::getDirectiveValues(Directive::deprecatedDirective(), $node); - - return $deprecated['reason'] ?? null; + if (! $def) { + throw new Error('def must be defined.'); + } + switch ($def->kind) { + case NodeKind::OBJECT_TYPE_DEFINITION: + return new ObjectType($config); + case NodeKind::INTERFACE_TYPE_DEFINITION: + return new InterfaceType($config); + case NodeKind::ENUM_TYPE_DEFINITION: + return new EnumType($config); + case NodeKind::UNION_TYPE_DEFINITION: + return new UnionType($config); + case NodeKind::SCALAR_TYPE_DEFINITION: + return new CustomScalarType($config); + case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: + return new InputObjectType($config); + default: + throw new Error(sprintf('Type kind of %s not supported.', $def->kind)); + } } /** - * Given an ast node, returns its string description. + * @param TypeNode|ListTypeNode|NonNullTypeNode $typeNode + * @return TypeNode */ - private function getDescription($node) + private function getNamedTypeNode(TypeNode $typeNode) { - if ($node->description) { - return $node->description->value; - } - if (isset($this->options['commentDescriptions'])) { - $rawValue = $this->getLeadingCommentBlock($node); - if ($rawValue !== null) { - return BlockString::value("\n" . $rawValue); - } + $namedType = $typeNode; + while ($namedType->kind === NodeKind::LIST_TYPE || $namedType->kind === NodeKind::NON_NULL_TYPE) { + $namedType = $namedType->type; } - return null; + return $namedType; } - private function getLeadingCommentBlock($node) + /** + * @param TypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode + * @return Type + */ + private function buildWrappedType(Type $innerType, TypeNode $inputTypeNode) { - $loc = $node->loc; - if (! $loc || ! $loc->startToken) { - return null; + if ($inputTypeNode->kind === NodeKind::LIST_TYPE) { + return Type::listOf($this->buildWrappedType($innerType, $inputTypeNode->type)); } - $comments = []; - $token = $loc->startToken->prev; - while ($token && - $token->kind === Token::COMMENT && - $token->next && $token->prev && - $token->line + 1 === $token->next->line && - $token->line !== $token->prev->line - ) { - $value = $token->value; - $comments[] = $value; - $token = $token->prev; + if ($inputTypeNode->kind === NodeKind::NON_NULL_TYPE) { + $wrappedType = $this->buildWrappedType($innerType, $inputTypeNode->type); + + return Type::nonNull(NonNull::assertNullableType($wrappedType)); } - return implode("\n", array_reverse($comments)); + return $innerType; } } diff --git a/src/Utils/BlockString.php b/src/Utils/BlockString.php index 5bc40e4..a81760c 100644 --- a/src/Utils/BlockString.php +++ b/src/Utils/BlockString.php @@ -1,43 +1,58 @@ = mb_strlen($line) || + ($commonIndent !== null && $indent >= $commonIndent) ) { - $commonIndent = $indent; - if ($commonIndent === 0) { - break; - } + continue; + } + + $commonIndent = $indent; + if ($commonIndent === 0) { + break; } } - + if ($commonIndent) { for ($i = 1; $i < $linesLength; $i++) { - $line = $lines[$i]; + $line = $lines[$i]; $lines[$i] = mb_substr($line, $commonIndent); } } - + // Remove leading and trailing blank lines. while (count($lines) > 0 && trim($lines[0], " \t") === '') { array_shift($lines); @@ -50,7 +65,8 @@ class BlockString { return implode("\n", $lines); } - private static function leadingWhitespace($str) { + private static function leadingWhitespace($str) + { $i = 0; while ($i < mb_strlen($str) && ($str[$i] === ' ' || $str[$i] === '\t')) { $i++; diff --git a/src/Utils/FindBreakingChanges.php b/src/Utils/BreakingChangesFinder.php similarity index 54% rename from src/Utils/FindBreakingChanges.php rename to src/Utils/BreakingChangesFinder.php index ecad9f8..5e17e23 100644 --- a/src/Utils/FindBreakingChanges.php +++ b/src/Utils/BreakingChangesFinder.php @@ -1,4 +1,7 @@ getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach (array_keys($oldTypeMap) as $typeName) { + if (isset($newTypeMap[$typeName])) { + continue; + } + + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_TYPE_REMOVED, + 'description' => "${typeName} was removed.", + ]; + } + + return $breakingChanges; + } + + /** + * Given two schemas, returns an Array containing descriptions of any breaking + * changes in the newSchema related to changing the type of a type. + * + * @return string[][] + */ + public static function findTypesThatChangedKind( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + if (! isset($newTypeMap[$typeName])) { + continue; + } + $newType = $newTypeMap[$typeName]; + if ($oldType instanceof $newType) { + continue; + } + + $oldTypeKindName = self::typeKindName($oldType); + $newTypeKindName = self::typeKindName($newType); + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND, + 'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}.", + ]; + } + + return $breakingChanges; + } + + /** + * @return string + * + * @throws \TypeError + */ + private static function typeKindName(Type $type) + { + if ($type instanceof ScalarType) { + return 'a Scalar type'; + } + + if ($type instanceof ObjectType) { + return 'an Object type'; + } + + if ($type instanceof InterfaceType) { + return 'an Interface type'; + } + + if ($type instanceof UnionType) { + return 'a Union type'; + } + + if ($type instanceof EnumType) { + return 'an Enum type'; + } + + if ($type instanceof InputObjectType) { + return 'an Input type'; + } + + throw new \TypeError('unknown type ' . $type->name); + } + + /** + * @return string[][] + */ + public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || + ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) || + ! ($newType instanceof $oldType) + ) { + continue; + } + + $oldTypeFieldsDef = $oldType->getFields(); + $newTypeFieldsDef = $newType->getFields(); + foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { + // Check if the field is missing on the type in the new schema. + if (! isset($newTypeFieldsDef[$fieldName])) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => "${typeName}.${fieldName} was removed.", + ]; + } else { + $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); + $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); + $isSafe = self::isChangeSafeForObjectOrInterfaceField( + $oldFieldType, + $newFieldType + ); + if (! $isSafe) { + $oldFieldTypeString = $oldFieldType instanceof NamedType + ? $oldFieldType->name + : $oldFieldType; + $newFieldTypeString = $newFieldType instanceof NamedType + ? $newFieldType->name + : $newFieldType; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.", + ]; + } + } + } + } + + return $breakingChanges; + } + + /** + * @return bool + */ + private static function isChangeSafeForObjectOrInterfaceField( + Type $oldType, + Type $newType + ) { + if ($oldType instanceof NamedType) { + return // if they're both named types, see if their names are equivalent + ($newType instanceof NamedType && $oldType->name === $newType->name) || + // moving from nullable to non-null of the same underlying type is safe + ($newType instanceof NonNull && + self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()) + ); + } + + if ($oldType instanceof ListOfType) { + return // if they're both lists, make sure the underlying types are compatible + ($newType instanceof ListOfType && + self::isChangeSafeForObjectOrInterfaceField( + $oldType->getWrappedType(), + $newType->getWrappedType() + )) || + // moving from nullable to non-null of the same underlying type is safe + ($newType instanceof NonNull && + self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())); + } + + if ($oldType instanceof NonNull) { + // if they're both non-null, make sure the underlying types are compatible + return $newType instanceof NonNull && + self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType()); + } + + return false; + } + + /** + * @return string[][] + */ + public static function findFieldsThatChangedTypeOnInputObjectTypes( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + $dangerousChanges = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) { + continue; + } + + $oldTypeFieldsDef = $oldType->getFields(); + $newTypeFieldsDef = $newType->getFields(); + foreach (array_keys($oldTypeFieldsDef) as $fieldName) { + if (! isset($newTypeFieldsDef[$fieldName])) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, + 'description' => "${typeName}.${fieldName} was removed.", + ]; + } else { + $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); + $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); + + $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( + $oldFieldType, + $newFieldType + ); + if (! $isSafe) { + $oldFieldTypeString = $oldFieldType instanceof NamedType + ? $oldFieldType->name + : $oldFieldType; + $newFieldTypeString = $newFieldType instanceof NamedType + ? $newFieldType->name + : $newFieldType; + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}.", + ]; + } + } + } + // Check if a field was added to the input object type + foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { + if (isset($oldTypeFieldsDef[$fieldName])) { + continue; + } + + $newTypeName = $newType->name; + if ($fieldDef->getType() instanceof NonNull) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, + 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added.", + ]; + } else { + $dangerousChanges[] = [ + 'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, + 'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added.", + ]; + } + } + } + + return [ + 'breakingChanges' => $breakingChanges, + 'dangerousChanges' => $dangerousChanges, + ]; + } + + /** + * + * @return bool + */ + private static function isChangeSafeForInputObjectFieldOrFieldArg( + Type $oldType, + Type $newType + ) { + if ($oldType instanceof NamedType) { + // if they're both named types, see if their names are equivalent + return $newType instanceof NamedType && $oldType->name === $newType->name; + } + + if ($oldType instanceof ListOfType) { + // if they're both lists, make sure the underlying types are compatible + return $newType instanceof ListOfType && + self::isChangeSafeForInputObjectFieldOrFieldArg( + $oldType->getWrappedType(), + $newType->getWrappedType() + ); + } + + if ($oldType instanceof NonNull) { + return // if they're both non-null, make sure the underlying types are + // compatible + ($newType instanceof NonNull && + self::isChangeSafeForInputObjectFieldOrFieldArg( + $oldType->getWrappedType(), + $newType->getWrappedType() + )) || + // moving from non-null to nullable of the same underlying type is safe + (! ($newType instanceof NonNull) && + self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType)); + } + + return false; + } + + /** + * Given two schemas, returns an Array containing descriptions of any breaking + * changes in the newSchema related to removing types from a union type. + * + * @return string[][] + */ + public static function findTypesRemovedFromUnions( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $typesRemovedFromUnion = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) { + continue; + } + $typeNamesInNewUnion = []; + foreach ($newType->getTypes() as $type) { + $typeNamesInNewUnion[$type->name] = true; + } + foreach ($oldType->getTypes() as $type) { + if (isset($typeNamesInNewUnion[$type->name])) { + continue; + } + + $typesRemovedFromUnion[] = [ + 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, + 'description' => sprintf('%s was removed from union type %s.', $type->name, $typeName), + ]; + } + } + + return $typesRemovedFromUnion; + } + + /** + * Given two schemas, returns an Array containing descriptions of any breaking + * changes in the newSchema related to removing values from an enum type. + * + * @return string[][] + */ + public static function findValuesRemovedFromEnums( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $valuesRemovedFromEnums = []; + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) { + continue; + } + $valuesInNewEnum = []; + foreach ($newType->getValues() as $value) { + $valuesInNewEnum[$value->name] = true; + } + foreach ($oldType->getValues() as $value) { + if (isset($valuesInNewEnum[$value->name])) { + continue; + } + + $valuesRemovedFromEnums[] = [ + 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, + 'description' => sprintf('%s was removed from enum type %s.', $value->name, $typeName), + ]; + } + } + + return $valuesRemovedFromEnums; + } + + /** + * Given two schemas, returns an Array containing descriptions of any + * breaking or dangerous changes in the newSchema related to arguments + * (such as removal or change of type of an argument, or a change in an + * argument's default value). + * + * @return string[][] + */ + public static function findArgChanges( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + + $breakingChanges = []; + $dangerousChanges = []; + + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || + ! ($newType instanceof ObjectType || $newType instanceof InterfaceType) || + ! ($newType instanceof $oldType) + ) { + continue; + } + + $oldTypeFields = $oldType->getFields(); + $newTypeFields = $newType->getFields(); + + foreach ($oldTypeFields as $fieldName => $oldField) { + if (! isset($newTypeFields[$fieldName])) { + continue; + } + + foreach ($oldField->args as $oldArgDef) { + $newArgs = $newTypeFields[$fieldName]->args; + $newArgDef = Utils::find( + $newArgs, + function ($arg) use ($oldArgDef) { + return $arg->name === $oldArgDef->name; + } + ); + if ($newArgDef) { + $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( + $oldArgDef->getType(), + $newArgDef->getType() + ); + $oldArgType = $oldArgDef->getType(); + $oldArgName = $oldArgDef->name; + if (! $isSafe) { + $newArgType = $newArgDef->getType(); + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}", + ]; + } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) { + $dangerousChanges[] = [ + 'type' => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, + 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue", + ]; + } + } else { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_ARG_REMOVED, + 'description' => sprintf( + '%s.%s arg %s was removed', + $typeName, + $fieldName, + $oldArgDef->name + ), + ]; + } + // Check if a non-null arg was added to the field + foreach ($newTypeFields[$fieldName]->args as $newArgDef) { + $oldArgs = $oldTypeFields[$fieldName]->args; + $oldArgDef = Utils::find( + $oldArgs, + function ($arg) use ($newArgDef) { + return $arg->name === $newArgDef->name; + } + ); + + if ($oldArgDef) { + continue; + } + + $newTypeName = $newType->name; + $newArgName = $newArgDef->name; + if ($newArgDef->getType() instanceof NonNull) { + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, + 'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added", + ]; + } else { + $dangerousChanges[] = [ + 'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, + 'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added", + ]; + } + } + } + } + } + + return [ + 'breakingChanges' => $breakingChanges, + 'dangerousChanges' => $dangerousChanges, + ]; + } + + /** + * @return string[][] + */ + public static function findInterfacesRemovedFromObjectTypes( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); + $breakingChanges = []; + + foreach ($oldTypeMap as $typeName => $oldType) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { + continue; + } + + $oldInterfaces = $oldType->getInterfaces(); + $newInterfaces = $newType->getInterfaces(); + foreach ($oldInterfaces as $oldInterface) { + if (Utils::find( + $newInterfaces, + function (InterfaceType $interface) use ($oldInterface) { + return $interface->name === $oldInterface->name; + } + )) { + continue; + } + + $breakingChanges[] = [ + 'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'description' => sprintf('%s no longer implements interface %s.', $typeName, $oldInterface->name), + ]; + } + } + + return $breakingChanges; + } + + /** + * @return string[][] + */ + public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema) + { + $removedDirectives = []; + + $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema); + foreach ($oldSchema->getDirectives() as $directive) { + if (isset($newSchemaDirectiveMap[$directive->name])) { + continue; + } + + $removedDirectives[] = [ + 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED, + 'description' => sprintf('%s was removed', $directive->name), + ]; + } + + return $removedDirectives; + } + + private static function getDirectiveMapForSchema(Schema $schema) + { + return Utils::keyMap( + $schema->getDirectives(), + function ($dir) { + return $dir->name; + } + ); + } + + public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema) + { + $removedDirectiveArgs = []; + $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); + + foreach ($newSchema->getDirectives() as $newDirective) { + if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { + continue; + } + + foreach (self::findRemovedArgsForDirectives( + $oldSchemaDirectiveMap[$newDirective->name], + $newDirective + ) as $arg) { + $removedDirectiveArgs[] = [ + 'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, + 'description' => sprintf('%s was removed from %s', $arg->name, $newDirective->name), + ]; + } + } + + return $removedDirectiveArgs; + } + + public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective) + { + $removedArgs = []; + $newArgMap = self::getArgumentMapForDirective($newDirective); + foreach ((array) $oldDirective->args as $arg) { + if (isset($newArgMap[$arg->name])) { + continue; + } + + $removedArgs[] = $arg; + } + + return $removedArgs; + } + + private static function getArgumentMapForDirective(Directive $directive) + { + return Utils::keyMap( + $directive->args ?: [], + function ($arg) { + return $arg->name; + } + ); + } + + public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema) + { + $addedNonNullableArgs = []; + $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); + + foreach ($newSchema->getDirectives() as $newDirective) { + if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { + continue; + } + + foreach (self::findAddedArgsForDirective( + $oldSchemaDirectiveMap[$newDirective->name], + $newDirective + ) as $arg) { + if (! $arg->getType() instanceof NonNull) { + continue; + } + $addedNonNullableArgs[] = [ + 'type' => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, + 'description' => sprintf( + 'A non-null arg %s on directive %s was added', + $arg->name, + $newDirective->name + ), + ]; + } + } + + return $addedNonNullableArgs; + } + + /** + * @return FieldArgument[] + */ + public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective) + { + $addedArgs = []; + $oldArgMap = self::getArgumentMapForDirective($oldDirective); + foreach ((array) $newDirective->args as $arg) { + if (isset($oldArgMap[$arg->name])) { + continue; + } + + $addedArgs[] = $arg; + } + + return $addedArgs; + } + + /** + * @return string[][] + */ + public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema) + { + $removedLocations = []; + $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); + + foreach ($newSchema->getDirectives() as $newDirective) { + if (! isset($oldSchemaDirectiveMap[$newDirective->name])) { + continue; + } + + foreach (self::findRemovedLocationsForDirective( + $oldSchemaDirectiveMap[$newDirective->name], + $newDirective + ) as $location) { + $removedLocations[] = [ + 'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, + 'description' => sprintf('%s was removed from %s', $location, $newDirective->name), + ]; + } + } + + return $removedLocations; + } + + public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective) + { + $removedLocations = []; + $newLocationSet = array_flip($newDirective->locations); + foreach ($oldDirective->locations as $oldLocation) { + if (array_key_exists($oldLocation, $newLocationSet)) { + continue; + } + + $removedLocations[] = $oldLocation; + } + + return $removedLocations; + } + /** * Given two schemas, returns an Array containing descriptions of all the types * of potentially dangerous changes covered by the other functions down below. * - * @return array + * @return string[][] */ public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema) { @@ -85,490 +792,11 @@ class FindBreakingChanges ); } - /** - * Given two schemas, returns an Array containing descriptions of any breaking - * changes in the newSchema related to removing an entire type. - * - * @return array - */ - public static function findRemovedTypes( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $breakingChanges = []; - foreach (array_keys($oldTypeMap) as $typeName) { - if (!isset($newTypeMap[$typeName])) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_TYPE_REMOVED, - 'description' => "${typeName} was removed." - ]; - } - } - return $breakingChanges; - } - - /** - * Given two schemas, returns an Array containing descriptions of any breaking - * changes in the newSchema related to changing the type of a type. - * - * @return array - */ - public static function findTypesThatChangedKind( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $breakingChanges = []; - foreach ($oldTypeMap as $typeName => $oldType) { - if (!isset($newTypeMap[$typeName])) { - continue; - } - $newType = $newTypeMap[$typeName]; - if (!($oldType instanceof $newType)) { - $oldTypeKindName = self::typeKindName($oldType); - $newTypeKindName = self::typeKindName($newType); - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND, - 'description' => "${typeName} changed from ${oldTypeKindName} to ${newTypeKindName}." - ]; - } - } - return $breakingChanges; - } - - /** - * Given two schemas, returns an Array containing descriptions of any - * breaking or dangerous changes in the newSchema related to arguments - * (such as removal or change of type of an argument, or a change in an - * argument's default value). - * - * @return array - */ - public static function findArgChanges( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $breakingChanges = []; - $dangerousChanges = []; - - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if ( - !($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || - !($newType instanceof ObjectType || $newType instanceof InterfaceType) || - !($newType instanceof $oldType) - ) { - continue; - } - - $oldTypeFields = $oldType->getFields(); - $newTypeFields = $newType->getFields(); - - foreach ($oldTypeFields as $fieldName => $oldField) { - if (!isset($newTypeFields[$fieldName])) { - continue; - } - - foreach ($oldField->args as $oldArgDef) { - $newArgs = $newTypeFields[$fieldName]->args; - $newArgDef = Utils::find( - $newArgs, - function ($arg) use ($oldArgDef) { - return $arg->name === $oldArgDef->name; - } - ); - if (!$newArgDef) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_ARG_REMOVED, - 'description' => "${typeName}.${fieldName} arg {$oldArgDef->name} was removed" - ]; - } else { - $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( - $oldArgDef->getType(), - $newArgDef->getType() - ); - $oldArgType = $oldArgDef->getType(); - $oldArgName = $oldArgDef->name; - if (!$isSafe) { - $newArgType = $newArgDef->getType(); - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND, - 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed type from ${oldArgType} to ${newArgType}" - ]; - } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) { - $dangerousChanges[] = [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, - 'description' => "${typeName}.${fieldName} arg ${oldArgName} has changed defaultValue" - ]; - } - } - // Check if a non-null arg was added to the field - foreach ($newTypeFields[$fieldName]->args as $newArgDef) { - $oldArgs = $oldTypeFields[$fieldName]->args; - $oldArgDef = Utils::find( - $oldArgs, - function ($arg) use ($newArgDef) { - return $arg->name === $newArgDef->name; - } - ); - - if (!$oldArgDef) { - $newTypeName = $newType->name; - $newArgName = $newArgDef->name; - if ($newArgDef->getType() instanceof NonNull) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_ARG_ADDED, - 'description' => "A non-null arg ${newArgName} on ${newTypeName}.${fieldName} was added" - ]; - } else { - $dangerousChanges[] = [ - 'type' => self::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, - 'description' => "A nullable arg ${newArgName} on ${newTypeName}.${fieldName} was added" - ]; - } - } - } - } - } - } - - return [ - 'breakingChanges' => $breakingChanges, - 'dangerousChanges' => $dangerousChanges, - ]; - } - - /** - * @param Type $type - * @return string - * - * @throws \TypeError - */ - private static function typeKindName(Type $type) - { - if ($type instanceof ScalarType) { - return 'a Scalar type'; - } elseif ($type instanceof ObjectType) { - return 'an Object type'; - } elseif ($type instanceof InterfaceType) { - return 'an Interface type'; - } elseif ($type instanceof UnionType) { - return 'a Union type'; - } elseif ($type instanceof EnumType) { - return 'an Enum type'; - } elseif ($type instanceof InputObjectType) { - return 'an Input type'; - } - - throw new \TypeError('unknown type ' . $type->name); - } - - public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $breakingChanges = []; - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if ( - !($oldType instanceof ObjectType || $oldType instanceof InterfaceType) || - !($newType instanceof ObjectType || $newType instanceof InterfaceType) || - !($newType instanceof $oldType) - ) { - continue; - } - - $oldTypeFieldsDef = $oldType->getFields(); - $newTypeFieldsDef = $newType->getFields(); - foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) { - // Check if the field is missing on the type in the new schema. - if (!isset($newTypeFieldsDef[$fieldName])) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, - 'description' => "${typeName}.${fieldName} was removed." - ]; - } else { - $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); - $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); - $isSafe = self::isChangeSafeForObjectOrInterfaceField( - $oldFieldType, - $newFieldType - ); - if (!$isSafe) { - $oldFieldTypeString = $oldFieldType instanceof NamedType - ? $oldFieldType->name - : $oldFieldType; - $newFieldTypeString = $newFieldType instanceof NamedType - ? $newFieldType->name - : $newFieldType; - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, - 'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}." - ]; - } - } - } - } - return $breakingChanges; - } - - public static function findFieldsThatChangedTypeOnInputObjectTypes( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $breakingChanges = []; - $dangerousChanges = []; - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if (!($oldType instanceof InputObjectType) || !($newType instanceof InputObjectType)) { - continue; - } - - $oldTypeFieldsDef = $oldType->getFields(); - $newTypeFieldsDef = $newType->getFields(); - foreach (array_keys($oldTypeFieldsDef) as $fieldName) { - if (!isset($newTypeFieldsDef[$fieldName])) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_FIELD_REMOVED, - 'description' => "${typeName}.${fieldName} was removed." - ]; - } else { - $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType(); - $newFieldType = $newTypeFieldsDef[$fieldName]->getType(); - - $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg( - $oldFieldType, - $newFieldType - ); - if (!$isSafe) { - $oldFieldTypeString = $oldFieldType instanceof NamedType - ? $oldFieldType->name - : $oldFieldType; - $newFieldTypeString = $newFieldType instanceof NamedType - ? $newFieldType->name - : $newFieldType; - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND, - 'description' => "${typeName}.${fieldName} changed type from ${oldFieldTypeString} to ${newFieldTypeString}."]; - } - } - } - // Check if a field was added to the input object type - foreach ($newTypeFieldsDef as $fieldName => $fieldDef) { - if (!isset($oldTypeFieldsDef[$fieldName])) { - $newTypeName = $newType->name; - if ($fieldDef->getType() instanceof NonNull) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, - 'description' => "A non-null field ${fieldName} on input type ${newTypeName} was added." - ]; - } else { - $dangerousChanges[] = [ - 'type' => self::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, - 'description' => "A nullable field ${fieldName} on input type ${newTypeName} was added." - ]; - } - } - } - } - - return [ - 'breakingChanges' => $breakingChanges, - 'dangerousChanges' => $dangerousChanges, - ]; - - } - - private static function isChangeSafeForObjectOrInterfaceField( - Type $oldType, - Type $newType - ) { - if ($oldType instanceof NamedType) { - return ( - // if they're both named types, see if their names are equivalent - ($newType instanceof NamedType && $oldType->name === $newType->name) || - // moving from nullable to non-null of the same underlying type is safe - ($newType instanceof NonNull && - self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()) - ) - ); - } elseif ($oldType instanceof ListOfType) { - return ( - // if they're both lists, make sure the underlying types are compatible - ($newType instanceof ListOfType && - self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType())) || - // moving from nullable to non-null of the same underlying type is safe - ($newType instanceof NonNull && - self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType())) - ); - } elseif ($oldType instanceof NonNull) { - // if they're both non-null, make sure the underlying types are compatible - return ( - $newType instanceof NonNull && - self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType()) - ); - } - return false; - } - - /** - * @param Type $oldType - * @param Type $newType - * - * @return bool - */ - private static function isChangeSafeForInputObjectFieldOrFieldArg( - Type $oldType, - Type $newType - ) { - if ($oldType instanceof NamedType) { - // if they're both named types, see if their names are equivalent - return $newType instanceof NamedType && $oldType->name === $newType->name; - } elseif ($oldType instanceof ListOfType) { - // if they're both lists, make sure the underlying types are compatible - return $newType instanceof ListOfType && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType->getWrappedType()); - } elseif ($oldType instanceof NonNull) { - return ( - // if they're both non-null, make sure the underlying types are - // compatible - ($newType instanceof NonNull && - self::isChangeSafeForInputObjectFieldOrFieldArg( - $oldType->getWrappedType(), - $newType->getWrappedType() - )) || - // moving from non-null to nullable of the same underlying type is safe - (!($newType instanceof NonNull) && - self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType)) - ); - } - return false; - } - - /** - * Given two schemas, returns an Array containing descriptions of any breaking - * changes in the newSchema related to removing types from a union type. - * - * @return array - */ - public static function findTypesRemovedFromUnions( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $typesRemovedFromUnion = []; - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) { - continue; - } - $typeNamesInNewUnion = []; - foreach ($newType->getTypes() as $type) { - $typeNamesInNewUnion[$type->name] = true; - } - foreach ($oldType->getTypes() as $type) { - if (!isset($typeNamesInNewUnion[$type->name])) { - $typesRemovedFromUnion[] = [ - 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, - 'description' => "{$type->name} was removed from union type ${typeName}.", - ]; - } - } - } - return $typesRemovedFromUnion; - } - - /** - * Given two schemas, returns an Array containing descriptions of any dangerous - * changes in the newSchema related to adding types to a union type. - * - * @return array - */ - public static function findTypesAddedToUnions( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $typesAddedToUnion = []; - foreach ($newTypeMap as $typeName => $newType) { - $oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null; - if (!($oldType instanceof UnionType) || !($newType instanceof UnionType)) { - continue; - } - - $typeNamesInOldUnion = []; - foreach ($oldType->getTypes() as $type) { - $typeNamesInOldUnion[$type->name] = true; - } - foreach ($newType->getTypes() as $type) { - if (!isset($typeNamesInOldUnion[$type->name])) { - $typesAddedToUnion[] = [ - 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, - 'description' => "{$type->name} was added to union type ${typeName}.", - ]; - } - } - } - return $typesAddedToUnion; - } - - /** - * Given two schemas, returns an Array containing descriptions of any breaking - * changes in the newSchema related to removing values from an enum type. - * - * @return array - */ - public static function findValuesRemovedFromEnums( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - - $valuesRemovedFromEnums = []; - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) { - continue; - } - $valuesInNewEnum = []; - foreach ($newType->getValues() as $value) { - $valuesInNewEnum[$value->name] = true; - } - foreach ($oldType->getValues() as $value) { - if (!isset($valuesInNewEnum[$value->name])) { - $valuesRemovedFromEnums[] = [ - 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, - 'description' => "{$value->name} was removed from enum type ${typeName}.", - ]; - } - } - } - return $valuesRemovedFromEnums; - } - /** * Given two schemas, returns an Array containing descriptions of any dangerous * changes in the newSchema related to adding values to an enum type. * - * @return array + * @return string[][] */ public static function findValuesAddedToEnums( Schema $oldSchema, @@ -579,8 +807,8 @@ class FindBreakingChanges $valuesAddedToEnums = []; foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if (!($oldType instanceof EnumType) || !($newType instanceof EnumType)) { + $newType = $newTypeMap[$typeName] ?? null; + if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) { continue; } $valuesInOldEnum = []; @@ -588,218 +816,102 @@ class FindBreakingChanges $valuesInOldEnum[$value->name] = true; } foreach ($newType->getValues() as $value) { - if (!isset($valuesInOldEnum[$value->name])) { - $valuesAddedToEnums[] = [ - 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, - 'description' => "{$value->name} was added to enum type ${typeName}.", - ]; + if (isset($valuesInOldEnum[$value->name])) { + continue; } + + $valuesAddedToEnums[] = [ + 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, + 'description' => sprintf('%s was added to enum type %s.', $value->name, $typeName), + ]; } } + return $valuesAddedToEnums; } /** - * @param Schema $oldSchema - * @param Schema $newSchema * - * @return array - */ - public static function findInterfacesRemovedFromObjectTypes( - Schema $oldSchema, - Schema $newSchema - ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); - $breakingChanges = []; - - foreach ($oldTypeMap as $typeName => $oldType) { - $newType = isset($newTypeMap[$typeName]) ? $newTypeMap[$typeName] : null; - if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) { - continue; - } - - $oldInterfaces = $oldType->getInterfaces(); - $newInterfaces = $newType->getInterfaces(); - foreach ($oldInterfaces as $oldInterface) { - if (!Utils::find($newInterfaces, function (InterfaceType $interface) use ($oldInterface) { - return $interface->name === $oldInterface->name; - })) { - $breakingChanges[] = [ - 'type' => self::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, - 'description' => "${typeName} no longer implements interface {$oldInterface->name}." - ]; - } - } - } - return $breakingChanges; - } - - /** - * @param Schema $oldSchema - * @param Schema $newSchema - * - * @return array + * @return string[][] */ public static function findInterfacesAddedToObjectTypes( Schema $oldSchema, Schema $newSchema ) { - $oldTypeMap = $oldSchema->getTypeMap(); - $newTypeMap = $newSchema->getTypeMap(); + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); $interfacesAddedToObjectTypes = []; foreach ($newTypeMap as $typeName => $newType) { - $oldType = isset($oldTypeMap[$typeName]) ? $oldTypeMap[$typeName] : null; - if (!($oldType instanceof ObjectType) || !($newType instanceof ObjectType)) { + $oldType = $oldTypeMap[$typeName] ?? null; + if (! ($oldType instanceof ObjectType) || ! ($newType instanceof ObjectType)) { continue; } $oldInterfaces = $oldType->getInterfaces(); $newInterfaces = $newType->getInterfaces(); foreach ($newInterfaces as $newInterface) { - if (!Utils::find($oldInterfaces, function (InterfaceType $interface) use ($newInterface) { - return $interface->name === $newInterface->name; - })) { - $interfacesAddedToObjectTypes[] = [ - 'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, - 'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.", - ]; + if (Utils::find( + $oldInterfaces, + function (InterfaceType $interface) use ($newInterface) { + return $interface->name === $newInterface->name; + } + )) { + continue; } + + $interfacesAddedToObjectTypes[] = [ + 'type' => self::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'description' => sprintf( + '%s added to interfaces implemented by %s.', + $newInterface->name, + $typeName + ), + ]; } } + return $interfacesAddedToObjectTypes; } - public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema) - { - $removedDirectives = []; + /** + * Given two schemas, returns an Array containing descriptions of any dangerous + * changes in the newSchema related to adding types to a union type. + * + * @return string[][] + */ + public static function findTypesAddedToUnions( + Schema $oldSchema, + Schema $newSchema + ) { + $oldTypeMap = $oldSchema->getTypeMap(); + $newTypeMap = $newSchema->getTypeMap(); - $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema); - foreach($oldSchema->getDirectives() as $directive) { - if (!isset($newSchemaDirectiveMap[$directive->name])) { - $removedDirectives[] = [ - 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED, - 'description' => "{$directive->name} was removed", - ]; - } - } - - return $removedDirectives; - } - - public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective) - { - $removedArgs = []; - $newArgMap = self::getArgumentMapForDirective($newDirective); - foreach((array) $oldDirective->args as $arg) { - if (!isset($newArgMap[$arg->name])) { - $removedArgs[] = $arg; - } - } - - return $removedArgs; - } - - public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema) - { - $removedDirectiveArgs = []; - $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); - - foreach($newSchema->getDirectives() as $newDirective) { - if (!isset($oldSchemaDirectiveMap[$newDirective->name])) { + $typesAddedToUnion = []; + foreach ($newTypeMap as $typeName => $newType) { + $oldType = $oldTypeMap[$typeName] ?? null; + if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) { continue; } - foreach(self::findRemovedArgsForDirectives($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) { - $removedDirectiveArgs[] = [ - 'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, - 'description' => "{$arg->name} was removed from {$newDirective->name}", - ]; + $typeNamesInOldUnion = []; + foreach ($oldType->getTypes() as $type) { + $typeNamesInOldUnion[$type->name] = true; } - } - - return $removedDirectiveArgs; - } - - public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective) - { - $addedArgs = []; - $oldArgMap = self::getArgumentMapForDirective($oldDirective); - foreach((array) $newDirective->args as $arg) { - if (!isset($oldArgMap[$arg->name])) { - $addedArgs[] = $arg; - } - } - - return $addedArgs; - } - - public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema) - { - $addedNonNullableArgs = []; - $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); - - foreach($newSchema->getDirectives() as $newDirective) { - if (!isset($oldSchemaDirectiveMap[$newDirective->name])) { - continue; - } - - foreach(self::findAddedArgsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $arg) { - if (!$arg->getType() instanceof NonNull) { + foreach ($newType->getTypes() as $type) { + if (isset($typeNamesInOldUnion[$type->name])) { continue; } - $addedNonNullableArgs[] = [ - 'type' => self::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, - 'description' => "A non-null arg {$arg->name} on directive {$newDirective->name} was added", + + $typesAddedToUnion[] = [ + 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, + 'description' => sprintf('%s was added to union type %s.', $type->name, $typeName), ]; } } - return $addedNonNullableArgs; - } - - public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective) - { - $removedLocations = []; - $newLocationSet = array_flip($newDirective->locations); - foreach($oldDirective->locations as $oldLocation) { - if (!array_key_exists($oldLocation, $newLocationSet)) { - $removedLocations[] = $oldLocation; - } - } - - return $removedLocations; - } - - public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema) - { - $removedLocations = []; - $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema); - - foreach($newSchema->getDirectives() as $newDirective) { - if (!isset($oldSchemaDirectiveMap[$newDirective->name])) { - continue; - } - - foreach(self::findRemovedLocationsForDirective($oldSchemaDirectiveMap[$newDirective->name], $newDirective) as $location) { - $removedLocations[] = [ - 'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, - 'description' => "{$location} was removed from {$newDirective->name}", - ]; - } - } - - return $removedLocations; - } - - private static function getDirectiveMapForSchema(Schema $schema) - { - return Utils::keyMap($schema->getDirectives(), function ($dir) { return $dir->name; }); - } - - private static function getArgumentMapForDirective(Directive $directive) - { - return Utils::keyMap($directive->args ?: [], function ($arg) { return $arg->name; }); + return $typesAddedToUnion; } } + +class_alias(BreakingChangesFinder::class, 'GraphQL\Utils\FindBreakingChanges'); diff --git a/src/Utils/BuildSchema.php b/src/Utils/BuildSchema.php index 18e169d..94d4dcf 100644 --- a/src/Utils/BuildSchema.php +++ b/src/Utils/BuildSchema.php @@ -1,14 +1,21 @@ ast = $ast; + $this->typeConfigDecorator = $typeConfigDecorator; + $this->options = $options; + } + + /** + * A helper function to build a GraphQLSchema directly from a source + * document. + * + * @api + * @param DocumentNode|Source|string $source + * @param bool[] $options + * @return Schema + */ + public static function build($source, ?callable $typeConfigDecorator = null, array $options = []) + { + $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); + + return self::buildAST($doc, $typeConfigDecorator, $options); + } + /** * This takes the ast of a schema document produced by the parse function in * GraphQL\Language\Parser. @@ -33,35 +78,22 @@ class BuildSchema * * * @api - * @param DocumentNode $ast - * @param callable $typeConfigDecorator - * @param array $options + * @param bool[] $options * @return Schema * @throws Error */ - public static function buildAST(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) + public static function buildAST(DocumentNode $ast, ?callable $typeConfigDecorator = null, array $options = []) { $builder = new self($ast, $typeConfigDecorator, $options); + return $builder->buildSchema(); } - private $ast; - private $nodeMap; - private $typeConfigDecorator; - private $options; - - public function __construct(DocumentNode $ast, callable $typeConfigDecorator = null, array $options = []) - { - $this->ast = $ast; - $this->typeConfigDecorator = $typeConfigDecorator; - $this->options = $options; - } - public function buildSchema() { /** @var SchemaDefinitionNode $schemaDef */ - $schemaDef = null; - $typeDefs = []; + $schemaDef = null; + $typeDefs = []; $this->nodeMap = []; $directiveDefs = []; foreach ($this->ast->definitions as $d) { @@ -79,10 +111,10 @@ class BuildSchema case NodeKind::UNION_TYPE_DEFINITION: case NodeKind::INPUT_OBJECT_TYPE_DEFINITION: $typeName = $d->name->value; - if (!empty($this->nodeMap[$typeName])) { - throw new Error("Type \"$typeName\" was defined more than once."); + if (! empty($this->nodeMap[$typeName])) { + throw new Error(sprintf('Type "%s" was defined more than once.', $typeName)); } - $typeDefs[] = $d; + $typeDefs[] = $d; $this->nodeMap[$typeName] = $d; break; case NodeKind::DIRECTIVE_DEFINITION: @@ -94,41 +126,55 @@ class BuildSchema $operationTypes = $schemaDef ? $this->getOperationTypes($schemaDef) : [ - 'query' => isset($this->nodeMap['Query']) ? 'Query' : null, - 'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null, + 'query' => isset($this->nodeMap['Query']) ? 'Query' : null, + 'mutation' => isset($this->nodeMap['Mutation']) ? 'Mutation' : null, 'subscription' => isset($this->nodeMap['Subscription']) ? 'Subscription' : null, ]; $defintionBuilder = new ASTDefinitionBuilder( $this->nodeMap, $this->options, - function($typeName) { throw new Error('Type "'. $typeName . '" not found in document.'); }, + function ($typeName) { + throw new Error('Type "' . $typeName . '" not found in document.'); + }, $this->typeConfigDecorator ); - $directives = array_map(function($def) use ($defintionBuilder) { - return $defintionBuilder->buildDirective($def); - }, $directiveDefs); + $directives = array_map( + function ($def) use ($defintionBuilder) { + return $defintionBuilder->buildDirective($def); + }, + $directiveDefs + ); // If specified directives were not explicitly declared, add them. - $skip = array_reduce($directives, function ($hasSkip, $directive) { - return $hasSkip || $directive->name == 'skip'; - }); - if (!$skip) { + $skip = array_reduce( + $directives, + function ($hasSkip, $directive) { + return $hasSkip || $directive->name === 'skip'; + } + ); + if (! $skip) { $directives[] = Directive::skipDirective(); } - $include = array_reduce($directives, function ($hasInclude, $directive) { - return $hasInclude || $directive->name == 'include'; - }); - if (!$include) { + $include = array_reduce( + $directives, + function ($hasInclude, $directive) { + return $hasInclude || $directive->name === 'include'; + } + ); + if (! $include) { $directives[] = Directive::includeDirective(); } - $deprecated = array_reduce($directives, function ($hasDeprecated, $directive) { - return $hasDeprecated || $directive->name == 'deprecated'; - }); - if (!$deprecated) { + $deprecated = array_reduce( + $directives, + function ($hasDeprecated, $directive) { + return $hasDeprecated || $directive->name === 'deprecated'; + } + ); + if (! $deprecated) { $directives[] = Directive::deprecatedDirective(); } @@ -137,27 +183,28 @@ class BuildSchema // validation with validateSchema() will produce more actionable results. $schema = new Schema([ - 'query' => isset($operationTypes['query']) + 'query' => isset($operationTypes['query']) ? $defintionBuilder->buildType($operationTypes['query']) : null, - 'mutation' => isset($operationTypes['mutation']) + 'mutation' => isset($operationTypes['mutation']) ? $defintionBuilder->buildType($operationTypes['mutation']) : null, 'subscription' => isset($operationTypes['subscription']) ? $defintionBuilder->buildType($operationTypes['subscription']) : null, - 'typeLoader' => function ($name) use ($defintionBuilder) { + 'typeLoader' => function ($name) use ($defintionBuilder) { return $defintionBuilder->buildType($name); }, - 'directives' => $directives, - 'astNode' => $schemaDef, - 'types' => function () use ($defintionBuilder) { + 'directives' => $directives, + 'astNode' => $schemaDef, + 'types' => function () use ($defintionBuilder) { $types = []; foreach ($this->nodeMap as $name => $def) { $types[] = $defintionBuilder->buildType($def->name->value); } + return $types; - } + }, ]); return $schema; @@ -165,7 +212,7 @@ class BuildSchema /** * @param SchemaDefinitionNode $schemaDef - * @return array + * @return string[] * @throws Error */ private function getOperationTypes($schemaDef) @@ -173,15 +220,15 @@ class BuildSchema $opTypes = []; foreach ($schemaDef->operationTypes as $operationType) { - $typeName = $operationType->type->name->value; + $typeName = $operationType->type->name->value; $operation = $operationType->operation; if (isset($opTypes[$operation])) { - throw new Error("Must provide only one $operation type in schema."); + throw new Error(sprintf('Must provide only one %s type in schema.', $operation)); } - if (!isset($this->nodeMap[$typeName])) { - throw new Error("Specified $operation type \"$typeName\" not found in document."); + if (! isset($this->nodeMap[$typeName])) { + throw new Error(sprintf('Specified %s type "%s" not found in document.', $operation, $typeName)); } $opTypes[$operation] = $typeName; @@ -189,20 +236,4 @@ class BuildSchema return $opTypes; } - - /** - * A helper function to build a GraphQLSchema directly from a source - * document. - * - * @api - * @param DocumentNode|Source|string $source - * @param callable $typeConfigDecorator - * @param array $options - * @return Schema - */ - public static function build($source, callable $typeConfigDecorator = null, array $options = []) - { - $doc = $source instanceof DocumentNode ? $source : Parser::parse($source); - return self::buildAST($doc, $typeConfigDecorator, $options); - } } diff --git a/src/Utils/MixedStore.php b/src/Utils/MixedStore.php index 89f183f..a3201a3 100644 --- a/src/Utils/MixedStore.php +++ b/src/Utils/MixedStore.php @@ -1,6 +1,19 @@ standardStore = []; - $this->floatStore = []; - $this->objectStore = new \SplObjectStorage(); - $this->arrayKeys = []; - $this->arrayValues = []; - $this->nullValueIsSet = false; - $this->trueValueIsSet = false; + $this->standardStore = []; + $this->floatStore = []; + $this->objectStore = new \SplObjectStorage(); + $this->arrayKeys = []; + $this->arrayValues = []; + $this->nullValueIsSet = false; + $this->trueValueIsSet = false; $this->falseValueIsSet = false; } @@ -98,18 +81,17 @@ class MixedStore implements \ArrayAccess * @param mixed $offset

* An offset to check for. *

- * @return boolean true on success or false on failure. + * @return bool true on success or false on failure. *

*

* The return value will be casted to boolean if non-boolean was returned. - * @since 5.0.0 */ public function offsetExists($offset) { - if (false === $offset) { + if ($offset === false) { return $this->falseValueIsSet; } - if (true === $offset) { + if ($offset === true) { return $this->trueValueIsSet; } if (is_int($offset) || is_string($offset)) { @@ -124,15 +106,17 @@ class MixedStore implements \ArrayAccess if (is_array($offset)) { foreach ($this->arrayKeys as $index => $entry) { if ($entry === $offset) { - $this->lastArrayKey = $offset; + $this->lastArrayKey = $offset; $this->lastArrayValue = $this->arrayValues[$index]; + return true; } } } - if (null === $offset) { + if ($offset === null) { return $this->nullValueIsSet; } + return false; } @@ -143,21 +127,20 @@ class MixedStore implements \ArrayAccess * The offset to retrieve. *

* @return mixed Can return all value types. - * @since 5.0.0 */ public function offsetGet($offset) { - if (true === $offset) { + if ($offset === true) { return $this->trueValue; } - if (false === $offset) { + if ($offset === false) { return $this->falseValue; } if (is_int($offset) || is_string($offset)) { return $this->standardStore[$offset]; } if (is_float($offset)) { - return $this->floatStore[(string)$offset]; + return $this->floatStore[(string) $offset]; } if (is_object($offset)) { return $this->objectStore->offsetGet($offset); @@ -173,9 +156,10 @@ class MixedStore implements \ArrayAccess } } } - if (null === $offset) { + if ($offset === null) { return $this->nullValue; } + return null; } @@ -185,34 +169,33 @@ class MixedStore implements \ArrayAccess * @param mixed $offset

* The offset to assign the value to. *

- * @param mixed $value

- * The value to set. - *

+ * @param mixed $value

+ * The value to set. + *

* @return void - * @since 5.0.0 */ public function offsetSet($offset, $value) { - if (false === $offset) { - $this->falseValue = $value; + if ($offset === false) { + $this->falseValue = $value; $this->falseValueIsSet = true; - } else if (true === $offset) { - $this->trueValue = $value; + } elseif ($offset === true) { + $this->trueValue = $value; $this->trueValueIsSet = true; - } else if (is_int($offset) || is_string($offset)) { + } elseif (is_int($offset) || is_string($offset)) { $this->standardStore[$offset] = $value; - } else if (is_float($offset)) { - $this->floatStore[(string)$offset] = $value; - } else if (is_object($offset)) { + } elseif (is_float($offset)) { + $this->floatStore[(string) $offset] = $value; + } elseif (is_object($offset)) { $this->objectStore[$offset] = $value; - } else if (is_array($offset)) { - $this->arrayKeys[] = $offset; + } elseif (is_array($offset)) { + $this->arrayKeys[] = $offset; $this->arrayValues[] = $value; - } else if (null === $offset) { - $this->nullValue = $value; + } elseif ($offset === null) { + $this->nullValue = $value; $this->nullValueIsSet = true; } else { - throw new \InvalidArgumentException("Unexpected offset type: " . Utils::printSafe($offset)); + throw new \InvalidArgumentException('Unexpected offset type: ' . Utils::printSafe($offset)); } } @@ -223,31 +206,30 @@ class MixedStore implements \ArrayAccess * The offset to unset. *

* @return void - * @since 5.0.0 */ public function offsetUnset($offset) { - if (true === $offset) { - $this->trueValue = null; + if ($offset === true) { + $this->trueValue = null; $this->trueValueIsSet = false; - } else if (false === $offset) { - $this->falseValue = null; + } elseif ($offset === false) { + $this->falseValue = null; $this->falseValueIsSet = false; - } else if (is_int($offset) || is_string($offset)) { + } elseif (is_int($offset) || is_string($offset)) { unset($this->standardStore[$offset]); - } else if (is_float($offset)) { - unset($this->floatStore[(string)$offset]); - } else if (is_object($offset)) { + } elseif (is_float($offset)) { + unset($this->floatStore[(string) $offset]); + } elseif (is_object($offset)) { $this->objectStore->offsetUnset($offset); - } else if (is_array($offset)) { + } elseif (is_array($offset)) { $index = array_search($offset, $this->arrayKeys, true); - if (false !== $index) { + if ($index !== false) { array_splice($this->arrayKeys, $index, 1); array_splice($this->arrayValues, $index, 1); } - } else if (null === $offset) { - $this->nullValue = null; + } elseif ($offset === null) { + $this->nullValue = null; $this->nullValueIsSet = false; } } diff --git a/src/Utils/PairSet.php b/src/Utils/PairSet.php index d00161d..e910a4b 100644 --- a/src/Utils/PairSet.php +++ b/src/Utils/PairSet.php @@ -1,4 +1,7 @@ data = []; @@ -23,12 +21,12 @@ class PairSet /** * @param string $a * @param string $b - * @param bool $areMutuallyExclusive + * @param bool $areMutuallyExclusive * @return bool */ public function has($a, $b, $areMutuallyExclusive) { - $first = isset($this->data[$a]) ? $this->data[$a] : null; + $first = $this->data[$a] ?? null; $result = ($first && isset($first[$b])) ? $first[$b] : null; if ($result === null) { return false; @@ -39,13 +37,14 @@ class PairSet if ($areMutuallyExclusive === false) { return $result === false; } + return true; } /** * @param string $a * @param string $b - * @param bool $areMutuallyExclusive + * @param bool $areMutuallyExclusive */ public function add($a, $b, $areMutuallyExclusive) { @@ -56,11 +55,11 @@ class PairSet /** * @param string $a * @param string $b - * @param bool $areMutuallyExclusive + * @param bool $areMutuallyExclusive */ private function pairSetAdd($a, $b, $areMutuallyExclusive) { - $this->data[$a] = isset($this->data[$a]) ? $this->data[$a] : []; + $this->data[$a] = $this->data[$a] ?? []; $this->data[$a][$b] = $areMutuallyExclusive; } } diff --git a/src/Utils/SchemaPrinter.php b/src/Utils/SchemaPrinter.php index 1c56648..7b547dd 100644 --- a/src/Utils/SchemaPrinter.php +++ b/src/Utils/SchemaPrinter.php @@ -1,10 +1,12 @@ getDirectives(), function($directive) use ($directiveFilter) { - return $directiveFilter($directive); - }); + $directives = array_filter( + $schema->getDirectives(), + function ($directive) use ($directiveFilter) { + return $directiveFilter($directive); + } + ); + $types = $schema->getTypeMap(); ksort($types); $types = array_filter($types, $typeFilter); - return implode("\n\n", array_filter(array_merge( - [self::printSchemaDefinition($schema)], - array_map(function($directive) use ($options) { return self::printDirective($directive, $options); }, $directives), - array_map(function($type) use ($options) { return self::printType($type, $options); }, $types) - ))) . "\n"; + return sprintf( + "%s\n", + implode( + "\n\n", + array_filter( + array_merge( + [self::printSchemaDefinition($schema)], + array_map( + function ($directive) use ($options) { + return self::printDirective($directive, $options); + }, + $directives + ), + array_map( + function ($type) use ($options) { + return self::printType($type, $options); + }, + $types + ) + ) + ) + ) + ); } private static function printSchemaDefinition(Schema $schema) @@ -83,20 +111,20 @@ class SchemaPrinter $queryType = $schema->getQueryType(); if ($queryType) { - $operationTypes[] = " query: {$queryType->name}"; + $operationTypes[] = sprintf(' query: %s', $queryType->name); } $mutationType = $schema->getMutationType(); if ($mutationType) { - $operationTypes[] = " mutation: {$mutationType->name}"; + $operationTypes[] = sprintf(' mutation: %s', $mutationType->name); } $subscriptionType = $schema->getSubscriptionType(); if ($subscriptionType) { - $operationTypes[] = " subscription: {$subscriptionType->name}"; + $operationTypes[] = sprintf(' subscription: %s', $subscriptionType->name); } - return "schema {\n" . implode("\n", $operationTypes) . "\n}"; + return sprintf("schema {\n%s\n}", implode("\n", $operationTypes)); } /** @@ -131,120 +159,6 @@ class SchemaPrinter return true; } - public static function printType(Type $type, array $options = []) - { - if ($type instanceof ScalarType) { - return self::printScalar($type, $options); - } else if ($type instanceof ObjectType) { - return self::printObject($type, $options); - } else if ($type instanceof InterfaceType) { - return self::printInterface($type, $options); - } else if ($type instanceof UnionType) { - return self::printUnion($type, $options); - } else if ($type instanceof EnumType) { - return self::printEnum($type, $options); - } else if ($type instanceof InputObjectType) { - return self::printInputObject($type, $options); - } - - throw new Error('Unknown type: ' . Utils::printSafe($type) . '.'); - } - - private static function printScalar(ScalarType $type, array $options) - { - return self::printDescription($options, $type) . "scalar {$type->name}"; - } - - private static function printObject(ObjectType $type, array $options) - { - $interfaces = $type->getInterfaces(); - $implementedInterfaces = !empty($interfaces) ? - ' implements ' . implode(', ', array_map(function($i) { - return $i->name; - }, $interfaces)) : ''; - return self::printDescription($options, $type) . - "type {$type->name}$implementedInterfaces {\n" . - self::printFields($options, $type) . "\n" . - "}"; - } - - private static function printInterface(InterfaceType $type, array $options) - { - return self::printDescription($options, $type) . - "interface {$type->name} {\n" . - self::printFields($options, $type) . "\n" . - "}"; - } - - private static function printUnion(UnionType $type, array $options) - { - return self::printDescription($options, $type) . - "union {$type->name} = " . implode(" | ", $type->getTypes()); - } - - private static function printEnum(EnumType $type, array $options) - { - return self::printDescription($options, $type) . - "enum {$type->name} {\n" . - self::printEnumValues($type->getValues(), $options) . "\n" . - "}"; - } - - private static function printEnumValues($values, $options) - { - return implode("\n", array_map(function($value, $i) use ($options) { - return self::printDescription($options, $value, ' ', !$i) . ' ' . - $value->name . self::printDeprecated($value); - }, $values, array_keys($values))); - } - - private static function printInputObject(InputObjectType $type, array $options) - { - $fields = array_values($type->getFields()); - return self::printDescription($options, $type) . - "input {$type->name} {\n" . - implode("\n", array_map(function($f, $i) use ($options) { - return self::printDescription($options, $f, ' ', !$i) . ' ' . self::printInputValue($f); - }, $fields, array_keys($fields))) . "\n" . - "}"; - } - - private static function printFields($options, $type) - { - $fields = array_values($type->getFields()); - return implode("\n", array_map(function($f, $i) use ($options) { - return self::printDescription($options, $f, ' ', !$i) . ' ' . - $f->name . self::printArgs($options, $f->args, ' ') . ': ' . - (string) $f->getType() . self::printDeprecated($f); - }, $fields, array_keys($fields))); - } - - private static function printArgs($options, $args, $indentation = '') - { - if (!$args) { - return ''; - } - - // If every arg does not have a description, print them on one line. - if (Utils::every($args, function($arg) { return empty($arg->description); })) { - return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; - } - - return "(\n" . implode("\n", array_map(function($arg, $i) use ($indentation, $options) { - return self::printDescription($options, $arg, ' ' . $indentation, !$i) . ' ' . $indentation . - self::printInputValue($arg); - }, $args, array_keys($args))) . "\n" . $indentation . ')'; - } - - private static function printInputValue($arg) - { - $argDecl = $arg->name . ': ' . (string) $arg->getType(); - if ($arg->defaultValueExists()) { - $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType())); - } - return $argDecl; - } - private static function printDirective($directive, $options) { return self::printDescription($options, $directive) . @@ -252,22 +166,9 @@ class SchemaPrinter ' on ' . implode(' | ', $directive->locations); } - private static function printDeprecated($fieldOrEnumVal) - { - $reason = $fieldOrEnumVal->deprecationReason; - if (empty($reason)) { - return ''; - } - if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) { - return ' @deprecated'; - } - return ' @deprecated(reason: ' . - Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; - } - private static function printDescription($options, $def, $indentation = '', $firstInBlock = true) { - if (!$def->description) { + if (! $def->description) { return ''; } $lines = self::descriptionLines($def->description, 120 - strlen($indentation)); @@ -275,13 +176,12 @@ class SchemaPrinter return self::printDescriptionWithComments($lines, $indentation, $firstInBlock); } - $description = ($indentation && !$firstInBlock) + $description = ($indentation && ! $firstInBlock) ? "\n" . $indentation . '"""' : $indentation . '"""'; // In some circumstances, a single line can be used for the description. - if ( - count($lines) === 1 && + if (count($lines) === 1 && mb_strlen($lines[0]) < 70 && substr($lines[0], -1) !== '"' ) { @@ -294,13 +194,13 @@ class SchemaPrinter substr($lines[0], 0, 1) === ' ' || substr($lines[0], 0, 1) === '\t' ); - if (!$hasLeadingSpace) { + if (! $hasLeadingSpace) { $description .= "\n"; } $lineLength = count($lines); for ($i = 0; $i < $lineLength; $i++) { - if ($i !== 0 || !$hasLeadingSpace) { + if ($i !== 0 || ! $hasLeadingSpace) { $description .= $indentation; } $description .= self::escapeQuote($lines[$i]) . "\n"; @@ -310,29 +210,11 @@ class SchemaPrinter return $description; } - private static function escapeQuote($line) + private static function descriptionLines($description, $maxLen) { - return str_replace('"""', '\\"""', $line); - } - - private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) - { - $description = $indentation && !$firstInBlock ? "\n" : ''; - foreach ($lines as $line) { - if ($line === '') { - $description .= $indentation . "#\n"; - } else { - $description .= $indentation . '# ' . $line . "\n"; - } - } - - return $description; - } - - private static function descriptionLines($description, $maxLen) { - $lines = []; + $lines = []; $rawLines = explode("\n", $description); - foreach($rawLines as $line) { + foreach ($rawLines as $line) { if ($line === '') { $lines[] = $line; } else { @@ -344,6 +226,7 @@ class SchemaPrinter } } } + return $lines; } @@ -352,10 +235,251 @@ class SchemaPrinter if (strlen($line) < $maxLen + 5) { return [$line]; } - preg_match_all("/((?: |^).{15," . ($maxLen - 40) . "}(?= |$))/", $line, $parts); + preg_match_all('/((?: |^).{15,' . ($maxLen - 40) . '}(?= |$))/', $line, $parts); $parts = $parts[0]; - return array_map(function($part) { - return trim($part); - }, $parts); + + return array_map('trim', $parts); + } + + private static function printDescriptionWithComments($lines, $indentation, $firstInBlock) + { + $description = $indentation && ! $firstInBlock ? "\n" : ''; + foreach ($lines as $line) { + if ($line === '') { + $description .= $indentation . "#\n"; + } else { + $description .= $indentation . '# ' . $line . "\n"; + } + } + + return $description; + } + + private static function escapeQuote($line) + { + return str_replace('"""', '\\"""', $line); + } + + private static function printArgs($options, $args, $indentation = '') + { + if (! $args) { + return ''; + } + + // If every arg does not have a description, print them on one line. + if (Utils::every( + $args, + function ($arg) { + return empty($arg->description); + } + )) { + return '(' . implode(', ', array_map('self::printInputValue', $args)) . ')'; + } + + return sprintf( + "(\n%s\n%s)", + implode( + "\n", + array_map( + function ($arg, $i) use ($indentation, $options) { + return self::printDescription($options, $arg, ' ' . $indentation, ! $i) . ' ' . $indentation . + self::printInputValue($arg); + }, + $args, + array_keys($args) + ) + ), + $indentation + ); + } + + private static function printInputValue($arg) + { + $argDecl = $arg->name . ': ' . (string) $arg->getType(); + if ($arg->defaultValueExists()) { + $argDecl .= ' = ' . Printer::doPrint(AST::astFromValue($arg->defaultValue, $arg->getType())); + } + + return $argDecl; + } + + /** + * @param bool[] $options + */ + public static function printType(Type $type, array $options = []) + { + if ($type instanceof ScalarType) { + return self::printScalar($type, $options); + } + + if ($type instanceof ObjectType) { + return self::printObject($type, $options); + } + + if ($type instanceof InterfaceType) { + return self::printInterface($type, $options); + } + + if ($type instanceof UnionType) { + return self::printUnion($type, $options); + } + + if ($type instanceof EnumType) { + return self::printEnum($type, $options); + } + + if ($type instanceof InputObjectType) { + return self::printInputObject($type, $options); + } + + throw new Error(sprintf('Unknown type: %s.', Utils::printSafe($type))); + } + + /** + * @param bool[] $options + */ + private static function printScalar(ScalarType $type, array $options) + { + return sprintf('%sscalar %s', self::printDescription($options, $type), $type->name); + } + + /** + * @param bool[] $options + */ + private static function printObject(ObjectType $type, array $options) + { + $interfaces = $type->getInterfaces(); + $implementedInterfaces = ! empty($interfaces) ? + ' implements ' . implode( + ', ', + array_map( + function ($i) { + return $i->name; + }, + $interfaces + ) + ) : ''; + + return self::printDescription($options, $type) . + sprintf("type %s%s {\n%s\n}", $type->name, $implementedInterfaces, self::printFields($options, $type)); + } + + /** + * @param bool[] $options + */ + private static function printFields($options, $type) + { + $fields = array_values($type->getFields()); + + return implode( + "\n", + array_map( + function ($f, $i) use ($options) { + return self::printDescription($options, $f, ' ', ! $i) . ' ' . + $f->name . self::printArgs($options, $f->args, ' ') . ': ' . + (string) $f->getType() . self::printDeprecated($f); + }, + $fields, + array_keys($fields) + ) + ); + } + + private static function printDeprecated($fieldOrEnumVal) + { + $reason = $fieldOrEnumVal->deprecationReason; + if (empty($reason)) { + return ''; + } + if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) { + return ' @deprecated'; + } + + return ' @deprecated(reason: ' . + Printer::doPrint(AST::astFromValue($reason, Type::string())) . ')'; + } + + /** + * @param bool[] $options + */ + private static function printInterface(InterfaceType $type, array $options) + { + return self::printDescription($options, $type) . + sprintf("interface %s {\n%s\n}", $type->name, self::printFields($options, $type)); + } + + /** + * @param bool[] $options + */ + private static function printUnion(UnionType $type, array $options) + { + return self::printDescription($options, $type) . + sprintf('union %s = %s', $type->name, implode(' | ', $type->getTypes())); + } + + /** + * @param bool[] $options + */ + private static function printEnum(EnumType $type, array $options) + { + return self::printDescription($options, $type) . + sprintf("enum %s {\n%s\n}", $type->name, self::printEnumValues($type->getValues(), $options)); + } + + /** + * @param bool[] $options + */ + private static function printEnumValues($values, $options) + { + return implode( + "\n", + array_map( + function ($value, $i) use ($options) { + return self::printDescription($options, $value, ' ', ! $i) . ' ' . + $value->name . self::printDeprecated($value); + }, + $values, + array_keys($values) + ) + ); + } + + /** + * @param bool[] $options + */ + private static function printInputObject(InputObjectType $type, array $options) + { + $fields = array_values($type->getFields()); + + return self::printDescription($options, $type) . + sprintf( + "input %s {\n%s\n}", + $type->name, + implode( + "\n", + array_map( + function ($f, $i) use ($options) { + return self::printDescription($options, $f, ' ', ! $i) . ' ' . self::printInputValue($f); + }, + $fields, + array_keys($fields) + ) + ) + ); + } + + /** + * @api + * @param bool[] $options + * @return string + */ + public static function printIntrosepctionSchema(Schema $schema, array $options = []) + { + return self::printFilteredSchema( + $schema, + [Directive::class, 'isSpecifiedDirective'], + [Introspection::class, 'isIntrospectionType'], + $options + ); } } diff --git a/src/Utils/TypeComparators.php b/src/Utils/TypeComparators.php index 0ddcbaf..c66b266 100644 --- a/src/Utils/TypeComparators.php +++ b/src/Utils/TypeComparators.php @@ -1,21 +1,22 @@ getWrappedType(), $superType->getWrappedType()); } + return false; - } else if ($maybeSubType instanceof NonNull) { + } + + if ($maybeSubType instanceof NonNull) { // If superType is nullable, maybeSubType may be non-null. return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType); } @@ -71,15 +74,24 @@ class TypeComparators if ($maybeSubType instanceof ListOfType) { return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType()); } + return false; - } else if ($maybeSubType instanceof ListOfType) { + } + + if ($maybeSubType instanceof ListOfType) { // If superType is not a list, maybeSubType must also be not a list. return false; } // If superType type is an abstract type, maybeSubType type may be a currently // possible object type. - if (Type::isAbstractType($superType) && $maybeSubType instanceof ObjectType && $schema->isPossibleType($superType, $maybeSubType)) { + if (Type::isAbstractType($superType) && + $maybeSubType instanceof ObjectType && + $schema->isPossibleType( + $superType, + $maybeSubType + ) + ) { return true; } @@ -96,12 +108,9 @@ class TypeComparators * * This function is commutative. * - * @param Schema $schema - * @param CompositeType $typeA - * @param CompositeType $typeB * @return bool */ - static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) + public static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) { // Equivalent types overlap if ($typeA === $typeB) { @@ -117,6 +126,7 @@ class TypeComparators return true; } } + return false; } diff --git a/src/Utils/TypeInfo.php b/src/Utils/TypeInfo.php index 7d93ee2..616c754 100644 --- a/src/Utils/TypeInfo.php +++ b/src/Utils/TypeInfo.php @@ -1,4 +1,7 @@ */ + private $typeStack; + + /** @var \SplStack */ + private $parentTypeStack; + + /** @var \SplStack */ + private $inputTypeStack; + + /** @var \SplStack */ + private $fieldDefStack; + + /** @var Directive */ + private $directive; + + /** @var FieldArgument */ + private $argument; + + /** @var mixed */ + private $enumValue; + + /** + * + * @param Type|null $initialType + */ + public function __construct(Schema $schema, $initialType = null) + { + $this->schema = $schema; + $this->typeStack = []; + $this->parentTypeStack = []; + $this->inputTypeStack = []; + $this->fieldDefStack = []; + if (! $initialType) { + return; + } + + if (Type::isInputType($initialType)) { + $this->inputTypeStack[] = $initialType; + } + if (Type::isCompositeType($initialType)) { + $this->parentTypeStack[] = $initialType; + } + if (! Type::isOutputType($initialType)) { + return; + } + + $this->typeStack[] = $initialType; + } + /** * @deprecated moved to GraphQL\Utils\TypeComparators */ @@ -42,7 +101,7 @@ class TypeInfo /** * @deprecated moved to GraphQL\Utils\TypeComparators */ - static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) + public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType) { return TypeComparators::isTypeSubTypeOf($schema, $maybeSubType, $superType); } @@ -50,22 +109,11 @@ class TypeInfo /** * @deprecated moved to GraphQL\Utils\TypeComparators */ - static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) + public static function doTypesOverlap(Schema $schema, CompositeType $typeA, CompositeType $typeB) { return TypeComparators::doTypesOverlap($schema, $typeA, $typeB); } - /** - * @param Schema $schema - * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode - * @return Type|null - * @throws InvariantViolation - */ - public static function typeFromAST(Schema $schema, $inputTypeNode) - { - return AST::typeFromAST($schema, $inputTypeNode); - } - /** * Given root type scans through all fields to find nested types. Returns array where keys are for type name * and value contains corresponding type instance. @@ -77,37 +125,39 @@ class TypeInfo * ... * ] * - * @param Type $type - * @param array|null $typeMap - * @return array + * @param Type|null $type + * @param Type[]|null $typeMap + * @return Type[]|null */ - public static function extractTypes($type, array $typeMap = null) + public static function extractTypes($type, ?array $typeMap = null) { - if (!$typeMap) { + if (! $typeMap) { $typeMap = []; } - if (!$type) { + if (! $type) { return $typeMap; } if ($type instanceof WrappingType) { return self::extractTypes($type->getWrappedType(true), $typeMap); } - if (!$type instanceof Type) { + if (! $type instanceof Type) { Warning::warnOnce( - 'One of the schema types is not a valid type definition instance. '. + 'One of the schema types is not a valid type definition instance. ' . 'Try running $schema->assertValid() to find out the cause of this warning.', Warning::WARNING_NOT_A_TYPE ); + return $typeMap; } - if (!empty($typeMap[$type->name])) { + if (! empty($typeMap[$type->name])) { Utils::invariant( $typeMap[$type->name] === $type, - "Schema must contain unique named types but contains multiple types named \"$type\" ". - "(see http://webonyx.github.io/graphql-php/type-system/#type-registry)." + sprintf('Schema must contain unique named types but contains multiple types named "%s" ', $type) . + '(see http://webonyx.github.io/graphql-php/type-system/#type-registry).' ); + return $typeMap; } $typeMap[$type->name] = $type; @@ -122,8 +172,14 @@ class TypeInfo } if ($type instanceof ObjectType || $type instanceof InterfaceType) { foreach ((array) $type->getFields() as $fieldName => $field) { - if (!empty($field->args)) { - $fieldArgTypes = array_map(function(FieldArgument $arg) { return $arg->getType(); }, $field->args); + if (! empty($field->args)) { + $fieldArgTypes = array_map( + function (FieldArgument $arg) { + return $arg->getType(); + }, + $field->args + ); + $nestedTypes = array_merge($nestedTypes, $fieldArgTypes); } $nestedTypes[] = $field->getType(); @@ -137,9 +193,169 @@ class TypeInfo foreach ($nestedTypes as $type) { $typeMap = self::extractTypes($type, $typeMap); } + return $typeMap; } + /** + * @return InputType|null + */ + public function getParentInputType() + { + $inputTypeStackLength = count($this->inputTypeStack); + if ($inputTypeStackLength > 1) { + return $this->inputTypeStack[$inputTypeStackLength - 2]; + } + } + + /** + * @return FieldArgument|null + */ + public function getArgument() + { + return $this->argument; + } + + /** + * @return mixed + */ + public function getEnumValue() + { + return $this->enumValue; + } + + public function enter(Node $node) + { + $schema = $this->schema; + + // Note: many of the types below are explicitly typed as "mixed" to drop + // any assumptions of a valid schema to ensure runtime types are properly + // checked before continuing since TypeInfo is used as part of validation + // which occurs before guarantees of schema and document validity. + switch ($node->kind) { + case NodeKind::SELECTION_SET: + $namedType = Type::getNamedType($this->getType()); + $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null; + break; + + case NodeKind::FIELD: + $parentType = $this->getParentType(); + $fieldDef = null; + if ($parentType) { + $fieldDef = self::getFieldDefinition($schema, $parentType, $node); + } + $fieldType = null; + if ($fieldDef) { + $fieldType = $fieldDef->getType(); + } + $this->fieldDefStack[] = $fieldDef; + $this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null; + break; + + case NodeKind::DIRECTIVE: + $this->directive = $schema->getDirective($node->name->value); + break; + + case NodeKind::OPERATION_DEFINITION: + $type = null; + if ($node->operation === 'query') { + $type = $schema->getQueryType(); + } elseif ($node->operation === 'mutation') { + $type = $schema->getMutationType(); + } elseif ($node->operation === 'subscription') { + $type = $schema->getSubscriptionType(); + } + $this->typeStack[] = Type::isOutputType($type) ? $type : null; + break; + + case NodeKind::INLINE_FRAGMENT: + case NodeKind::FRAGMENT_DEFINITION: + $typeConditionNode = $node->typeCondition; + $outputType = $typeConditionNode ? self::typeFromAST( + $schema, + $typeConditionNode + ) : Type::getNamedType($this->getType()); + $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; + break; + + case NodeKind::VARIABLE_DEFINITION: + $inputType = self::typeFromAST($schema, $node->type); + $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push + break; + + case NodeKind::ARGUMENT: + $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef(); + $argDef = $argType = null; + if ($fieldOrDirective) { + $argDef = Utils::find( + $fieldOrDirective->args, + function ($arg) use ($node) { + return $arg->name === $node->name->value; + } + ); + if ($argDef) { + $argType = $argDef->getType(); + } + } + $this->argument = $argDef; + $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null; + break; + + case NodeKind::LST: + $listType = Type::getNullableType($this->getInputType()); + $itemType = $listType instanceof ListOfType + ? $listType->getWrappedType() + : $listType; + $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; + break; + + case NodeKind::OBJECT_FIELD: + $objectType = Type::getNamedType($this->getInputType()); + $fieldType = null; + $inputFieldType = null; + if ($objectType instanceof InputObjectType) { + $tmp = $objectType->getFields(); + $inputField = $tmp[$node->name->value] ?? null; + $inputFieldType = $inputField ? $inputField->getType() : null; + } + $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null; + break; + + case NodeKind::ENUM: + $enumType = Type::getNamedType($this->getInputType()); + $enumValue = null; + if ($enumType instanceof EnumType) { + $enumValue = $enumType->getValue($node->value); + } + $this->enumValue = $enumValue; + break; + } + } + + /** + * @return Type + */ + public function getType() + { + if (! empty($this->typeStack)) { + return $this->typeStack[count($this->typeStack) - 1]; + } + + return null; + } + + /** + * @return Type + */ + public function getParentType() + { + if (! empty($this->parentTypeStack)) { + return $this->parentTypeStack[count($this->parentTypeStack) - 1]; + } + + return null; + } + /** * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, @@ -147,9 +363,9 @@ class TypeInfo * * @return FieldDefinition */ - static private function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode) + private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode) { - $name = $fieldNode->name->value; + $name = $fieldNode->name->value; $schemaMeta = Introspection::schemaMetaFieldDef(); if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) { return $schemaMeta; @@ -166,263 +382,56 @@ class TypeInfo if ($parentType instanceof ObjectType || $parentType instanceof InterfaceType) { $fields = $parentType->getFields(); - return isset($fields[$name]) ? $fields[$name] : null; + + return $fields[$name] ?? null; } - return null; - } - - /** - * @var Schema - */ - private $schema; - - /** - * @var \SplStack - */ - private $typeStack; - - /** - * @var \SplStack - */ - private $parentTypeStack; - - /** - * @var \SplStack - */ - private $inputTypeStack; - - /** - * @var \SplStack - */ - private $fieldDefStack; - - /** - * @var Directive - */ - private $directive; - - /** - * @var FieldArgument - */ - private $argument; - - /** - * @var mixed - */ - private $enumValue; - - /** - * TypeInfo constructor. - * @param Schema $schema - * @param Type|null $initialType - */ - public function __construct(Schema $schema, $initialType = null) - { - $this->schema = $schema; - $this->typeStack = []; - $this->parentTypeStack = []; - $this->inputTypeStack = []; - $this->fieldDefStack = []; - if ($initialType) { - if (Type::isInputType($initialType)) { - $this->inputTypeStack[] = $initialType; - } - if (Type::isCompositeType($initialType)) { - $this->parentTypeStack[] = $initialType; - } - if (Type::isOutputType($initialType)) { - $this->typeStack[] = $initialType; - } - } - } - - /** - * @return Type - */ - function getType() - { - if (!empty($this->typeStack)) { - return $this->typeStack[count($this->typeStack) - 1]; - } return null; } /** - * @return Type + * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode + * @return Type|null + * @throws InvariantViolation */ - function getParentType() + public static function typeFromAST(Schema $schema, $inputTypeNode) { - if (!empty($this->parentTypeStack)) { - return $this->parentTypeStack[count($this->parentTypeStack) - 1]; + return AST::typeFromAST($schema, $inputTypeNode); + } + + /** + * @return Directive|null + */ + public function getDirective() + { + return $this->directive; + } + + /** + * @return FieldDefinition + */ + public function getFieldDef() + { + if (! empty($this->fieldDefStack)) { + return $this->fieldDefStack[count($this->fieldDefStack) - 1]; } + return null; } /** * @return InputType */ - function getInputType() + public function getInputType() { - if (!empty($this->inputTypeStack)) { + if (! empty($this->inputTypeStack)) { return $this->inputTypeStack[count($this->inputTypeStack) - 1]; } + return null; } - /** - * @return InputType|null - */ - public function getParentInputType() - { - $inputTypeStackLength = count($this->inputTypeStack); - if ($inputTypeStackLength > 1) { - return $this->inputTypeStack[$inputTypeStackLength - 2]; - } - } - - /** - * @return FieldDefinition - */ - function getFieldDef() - { - if (!empty($this->fieldDefStack)) { - return $this->fieldDefStack[count($this->fieldDefStack) - 1]; - } - return null; - } - - /** - * @return Directive|null - */ - function getDirective() - { - return $this->directive; - } - - /** - * @return FieldArgument|null - */ - function getArgument() - { - return $this->argument; - } - - /** - * @return mixed - */ - function getEnumValue() - { - return $this->enumValue; - } - - /** - * @param Node $node - */ - function enter(Node $node) - { - $schema = $this->schema; - - // Note: many of the types below are explicitly typed as "mixed" to drop - // any assumptions of a valid schema to ensure runtime types are properly - // checked before continuing since TypeInfo is used as part of validation - // which occurs before guarantees of schema and document validity. - switch ($node->kind) { - case NodeKind::SELECTION_SET: - $namedType = Type::getNamedType($this->getType()); - $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null; - break; - - case NodeKind::FIELD: - $parentType = $this->getParentType(); - $fieldDef = null; - if ($parentType) { - $fieldDef = self::getFieldDefinition($schema, $parentType, $node); - } - $fieldType = null; - if ($fieldDef) { - $fieldType = $fieldDef->getType(); - } - $this->fieldDefStack[] = $fieldDef; - $this->typeStack[] = Type::isOutputType($fieldType) ? $fieldType : null; - break; - - case NodeKind::DIRECTIVE: - $this->directive = $schema->getDirective($node->name->value); - break; - - case NodeKind::OPERATION_DEFINITION: - $type = null; - if ($node->operation === 'query') { - $type = $schema->getQueryType(); - } else if ($node->operation === 'mutation') { - $type = $schema->getMutationType(); - } else if ($node->operation === 'subscription') { - $type = $schema->getSubscriptionType(); - } - $this->typeStack[] = Type::isOutputType($type) ? $type : null; - break; - - case NodeKind::INLINE_FRAGMENT: - case NodeKind::FRAGMENT_DEFINITION: - $typeConditionNode = $node->typeCondition; - $outputType = $typeConditionNode ? self::typeFromAST($schema, $typeConditionNode) : Type::getNamedType($this->getType()); - $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null; - break; - - case NodeKind::VARIABLE_DEFINITION: - $inputType = self::typeFromAST($schema, $node->type); - $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push - break; - - case NodeKind::ARGUMENT: - $fieldOrDirective = $this->getDirective() ?: $this->getFieldDef(); - $argDef = $argType = null; - if ($fieldOrDirective) { - $argDef = Utils::find($fieldOrDirective->args, function($arg) use ($node) {return $arg->name === $node->name->value;}); - if ($argDef) { - $argType = $argDef->getType(); - } - } - $this->argument = $argDef; - $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null; - break; - - case NodeKind::LST: - $listType = Type::getNullableType($this->getInputType()); - $itemType = $listType instanceof ListOfType - ? $listType->getWrappedType() - : $listType; - $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null; - break; - - case NodeKind::OBJECT_FIELD: - $objectType = Type::getNamedType($this->getInputType()); - $fieldType = null; - $inputFieldType = null; - if ($objectType instanceof InputObjectType) { - $tmp = $objectType->getFields(); - $inputField = isset($tmp[$node->name->value]) ? $tmp[$node->name->value] : null; - $inputFieldType = $inputField ? $inputField->getType() : null; - } - $this->inputTypeStack[] = Type::isInputType($inputFieldType) ? $inputFieldType : null; - break; - - case NodeKind::ENUM: - $enumType = Type::getNamedType($this->getInputType()); - $enumValue = null; - if ($enumType instanceof EnumType) { - $enumValue = $enumType->getValue($node->value); - } - $this->enumValue = $enumValue; - break; - } - } - - /** - * @param Node $node - */ - function leave(Node $node) + public function leave(Node $node) { switch ($node->kind) { case NodeKind::SELECTION_SET: diff --git a/src/Utils/Utils.php b/src/Utils/Utils.php index 1d9c0f4..a4f5884 100644 --- a/src/Utils/Utils.php +++ b/src/Utils/Utils.php @@ -1,4 +1,7 @@ $value) { - if (!property_exists($obj, $key)) { + if (! property_exists($obj, $key)) { $cls = get_class($obj); Warning::warn( - "Trying to set non-existing property '$key' on class '$cls'", + sprintf("Trying to set non-existing property '%s' on class '%s'", $key, $cls), Warning::WARNING_ASSIGN ); } $obj->{$key} = $value; } + return $obj; } /** - * @param array|Traversable $traversable - * @param callable $predicate + * @param mixed|Traversable $traversable * @return mixed|null */ public static function find($traversable, callable $predicate) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); foreach ($traversable as $key => $value) { if ($predicate($value, $key)) { return $value; } } + return null; } /** - * @param $traversable - * @param callable $predicate - * @return array + * @param mixed|Traversable $traversable + * @return mixed[] * @throws \Exception */ public static function filter($traversable, callable $predicate) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); $result = []; - $assoc = false; + $assoc = false; foreach ($traversable as $key => $value) { - if (!$assoc && !is_int($key)) { + if (! $assoc && ! is_int($key)) { $assoc = true; } - if ($predicate($value, $key)) { - $result[$key] = $value; + if (! $predicate($value, $key)) { + continue; } + + $result[$key] = $value; } return $assoc ? $result : array_values($result); } /** - * @param array|\Traversable $traversable - * @param callable $fn function($value, $key) => $newValue - * @return array + * @param mixed|\Traversable $traversable + * @return int[][] * @throws \Exception */ public static function map($traversable, callable $fn) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); $map = []; foreach ($traversable as $key => $value) { $map[$key] = $fn($value, $key); } + return $map; } /** - * @param $traversable - * @param callable $fn - * @return array + * @param mixed|Traversable $traversable + * @return mixed[] * @throws \Exception */ public static function mapKeyValue($traversable, callable $fn) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); $map = []; foreach ($traversable as $key => $value) { list($newKey, $newValue) = $fn($value, $key); $map[$newKey] = $newValue; } + return $map; } /** - * @param $traversable - * @param callable $keyFn function($value, $key) => $newKey - * @return array + * @param mixed|Traversable $traversable + * @return mixed[] * @throws \Exception */ public static function keyMap($traversable, callable $keyFn) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); $map = []; foreach ($traversable as $key => $value) { $newKey = $keyFn($value, $key); - if (is_scalar($newKey)) { - $map[$newKey] = $value; + if (! is_scalar($newKey)) { + continue; } + + $map[$newKey] = $value; } + return $map; } - /** - * @param $traversable - * @param callable $fn - */ public static function each($traversable, callable $fn) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); foreach ($traversable as $key => $item) { $fn($item, $key); @@ -177,13 +236,15 @@ class Utils * * $keyFn is also allowed to return array of keys. Then value will be added to all arrays with given keys * - * @param $traversable - * @param callable $keyFn function($value, $key) => $newKey(s) - * @return array + * @param mixed[]|Traversable $traversable + * @return mixed[] */ public static function groupBy($traversable, callable $keyFn) { - self::invariant(is_array($traversable) || $traversable instanceof \Traversable, __METHOD__ . ' expects array or Traversable'); + self::invariant( + is_array($traversable) || $traversable instanceof \Traversable, + __METHOD__ . ' expects array or Traversable' + ); $grouped = []; foreach ($traversable as $key => $value) { @@ -197,10 +258,8 @@ class Utils } /** - * @param array|Traversable $traversable - * @param callable $keyFn - * @param callable $valFn - * @return array + * @param mixed[]|Traversable $traversable + * @return mixed[][] */ public static function keyValMap($traversable, callable $keyFn, callable $valFn) { @@ -208,38 +267,36 @@ class Utils foreach ($traversable as $item) { $map[$keyFn($item)] = $valFn($item); } + return $map; } /** - * @param $traversable - * @param callable $predicate + * @param mixed[] $traversable * @return bool */ public static function every($traversable, callable $predicate) { foreach ($traversable as $key => $value) { - if (!$predicate($value, $key)) { + if (! $predicate($value, $key)) { return false; } } + return true; } /** - * @param $test + * @param bool $test * @param string $message - * @param mixed $sprintfParam1 - * @param mixed $sprintfParam2 ... - * @throws Error */ public static function invariant($test, $message = '') { - if (!$test) { + if (! $test) { if (func_num_args() > 2) { $args = func_get_args(); array_shift($args); - $message = call_user_func_array('sprintf', $args); + $message = sprintf(...$args); } // TODO switch to Error here throw new InvariantViolation($message); @@ -247,7 +304,7 @@ class Utils } /** - * @param $var + * @param Type|mixed $var * @return string */ public static function getVariableType($var) @@ -257,8 +314,10 @@ class Utils if ($var instanceof WrappingType) { $var = $var->getWrappedType(true); } + return $var->name; } + return is_object($var) ? get_class($var) : gettype($var); } @@ -274,29 +333,30 @@ class Utils if (is_array($var)) { return json_encode($var); } - if ('' === $var) { + if ($var === '') { return '(empty string)'; } - if (null === $var) { + if ($var === null) { return 'null'; } - if (false === $var) { + if ($var === false) { return 'false'; } - if (true === $var) { + if ($var === true) { return 'true'; } if (is_string($var)) { - return "\"$var\""; + return sprintf('"%s"', $var); } if (is_scalar($var)) { return (string) $var; } + return gettype($var); } /** - * @param $var + * @param Type|mixed $var * @return string */ public static function printSafe($var) @@ -307,23 +367,23 @@ class Utils if (is_object($var)) { if (method_exists($var, '__toString')) { return (string) $var; - } else { - return 'instance of ' . get_class($var); } + + return 'instance of ' . get_class($var); } if (is_array($var)) { return json_encode($var); } - if ('' === $var) { + if ($var === '') { return '(empty string)'; } - if (null === $var) { + if ($var === null) { return 'null'; } - if (false === $var) { + if ($var === false) { return 'false'; } - if (true === $var) { + if ($var === true) { return 'true'; } if (is_string($var)) { @@ -332,6 +392,7 @@ class Utils if (is_scalar($var)) { return (string) $var; } + return gettype($var); } @@ -348,10 +409,10 @@ class Utils return chr($ord); } if ($encoding === 'UCS-4BE') { - return pack("N", $ord); - } else { - return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE'); + return pack('N', $ord); } + + return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE'); } /** @@ -363,44 +424,46 @@ class Utils */ public static function ord($char, $encoding = 'UTF-8') { - if (!$char && '0' !== $char) { + if (! $char && $char !== '0') { return 0; } - if (!isset($char[1])) { + if (! isset($char[1])) { return ord($char); } if ($encoding !== 'UCS-4BE') { $char = mb_convert_encoding($char, 'UCS-4BE', $encoding); } - list(, $ord) = unpack('N', $char); - return $ord; + + return unpack('N', $char)[1]; } /** * Returns UTF-8 char code at given $positing of the $string * - * @param $string - * @param $position + * @param string $string + * @param int $position * @return mixed */ public static function charCodeAt($string, $position) { $char = mb_substr($string, $position, 1, 'UTF-8'); + return self::ord($char); } /** - * @param $code + * @param int|null $code * @return string */ public static function printCharCode($code) { - if (null === $code) { + if ($code === null) { return ''; } + return $code < 0x007F // Trust JSON for ASCII. - ? json_encode(Utils::chr($code)) + ? json_encode(self::chr($code)) // Otherwise print the escaped form. : '"\\u' . dechex($code) . '"'; } @@ -408,7 +471,7 @@ class Utils /** * Upholds the spec rules about naming. * - * @param $name + * @param string $name * @throws Error */ public static function assertValidName($name) @@ -422,25 +485,25 @@ class Utils /** * Returns an Error if a name is invalid. * - * @param string $name + * @param string $name * @param Node|null $node * @return Error|null */ public static function isValidNameError($name, $node = null) { - Utils::invariant(is_string($name), 'Expected string'); + self::invariant(is_string($name), 'Expected string'); if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') { return new Error( - "Name \"{$name}\" must not begin with \"__\", which is reserved by " . - "GraphQL introspection.", + sprintf('Name "%s" must not begin with "__", which is reserved by ', $name) . + 'GraphQL introspection.', $node ); } - if (!preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) { + if (! preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name)) { return new Error( - "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.", + sprintf('Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but "%s" does not.', $name), $node ); } @@ -452,13 +515,12 @@ class Utils * Wraps original closure with PHP error handling (using set_error_handler). * Resulting closure will collect all PHP errors that occur during the call in $errors array. * - * @param callable $fn * @param \ErrorException[] $errors * @return \Closure */ public static function withErrorHandling(callable $fn, array &$errors) { - return function() use ($fn, &$errors) { + return function () use ($fn, &$errors) { // Catch custom errors (to report them in query results) set_error_handler(function ($severity, $message, $file, $line) use (&$errors) { $errors[] = new \ErrorException($message, 0, $severity, $file, $line); @@ -472,25 +534,34 @@ class Utils }; } - /** * @param string[] $items * @return string */ public static function quotedOrList(array $items) { - $items = array_map(function($item) { return "\"$item\""; }, $items); + $items = array_map( + function ($item) { + return sprintf('"%s"', $item); + }, + $items + ); + return self::orList($items); } + /** + * @param string[] $items + * @return string + */ public static function orList(array $items) { - if (!$items) { + if (count($items) === 0) { throw new \LogicException('items must not need to be empty.'); } - $selected = array_slice($items, 0, 5); + $selected = array_slice($items, 0, 5); $selectedLength = count($selected); - $firstSelected = $selected[0]; + $firstSelected = $selected[0]; if ($selectedLength === 1) { return $firstSelected; @@ -499,7 +570,7 @@ class Utils return array_reduce( range(1, $selectedLength - 1), function ($list, $index) use ($selected, $selectedLength) { - return $list. + return $list . ($selectedLength > 2 ? ', ' : ' ') . ($index === $selectedLength - 1 ? 'or ' : '') . $selected[$index]; @@ -515,24 +586,28 @@ class Utils * Includes a custom alteration from Damerau-Levenshtein to treat case changes * as a single edit which helps identify mis-cased values with an edit distance * of 1 - * @param string $input - * @param array $options + * @param string $input + * @param string[] $options * @return string[] */ public static function suggestionList($input, array $options) { $optionsByDistance = []; - $inputThreshold = mb_strlen($input) / 2; + $inputThreshold = mb_strlen($input) / 2; foreach ($options as $option) { - $distance = $input === $option - ? 0 - : (strtolower($input) === strtolower($option) + if ($input === $option) { + $distance = 0; + } else { + $distance = (strtolower($input) === strtolower($option) ? 1 : levenshtein($input, $option)); - $threshold = max($inputThreshold, mb_strlen($option) / 2, 1); - if ($distance <= $threshold) { - $optionsByDistance[$option] = $distance; } + $threshold = max($inputThreshold, mb_strlen($option) / 2, 1); + if ($distance > $threshold) { + continue; + } + + $optionsByDistance[$option] = $distance; } asort($optionsByDistance); diff --git a/src/Utils/Value.php b/src/Utils/Value.php index bf01018..9541291 100644 --- a/src/Utils/Value.php +++ b/src/Utils/Value.php @@ -1,4 +1,7 @@ getWrappedType(), $blameNode, $path); } - if (null === $value) { + if ($value === null) { // Explicitly return the value null. return self::ofValue(null); } @@ -50,7 +64,7 @@ class Value } catch (\Exception $error) { return self::ofErrors([ self::coercionError( - "Expected type {$type->name}", + sprintf('Expected type %s', $type->name), $blameNode, $path, $error->getMessage(), @@ -60,7 +74,7 @@ class Value } catch (\Throwable $error) { return self::ofErrors([ self::coercionError( - "Expected type {$type->name}", + sprintf('Expected type %s', $type->name), $blameNode, $path, $error->getMessage(), @@ -80,15 +94,21 @@ class Value $suggestions = Utils::suggestionList( Utils::printSafe($value), - array_map(function($enumValue) { return $enumValue->name; }, $type->getValues()) + array_map( + function ($enumValue) { + return $enumValue->name; + }, + $type->getValues() + ) ); + $didYouMean = $suggestions - ? "did you mean " . Utils::orList($suggestions) . "?" + ? 'did you mean ' . Utils::orList($suggestions) . '?' : null; return self::ofErrors([ self::coercionError( - "Expected type {$type->name}", + sprintf('Expected type %s', $type->name), $blameNode, $path, $didYouMean @@ -99,7 +119,7 @@ class Value if ($type instanceof ListOfType) { $itemType = $type->getWrappedType(); if (is_array($value) || $value instanceof \Traversable) { - $errors = []; + $errors = []; $coercedValue = []; foreach ($value as $index => $itemValue) { $coercedItem = self::coerceValue( @@ -114,44 +134,32 @@ class Value $coercedValue[] = $coercedItem['value']; } } + return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue); } // Lists accept a non-list value as a list of one. $coercedItem = self::coerceValue($value, $itemType, $blameNode); + return $coercedItem['errors'] ? $coercedItem : self::ofValue([$coercedItem['value']]); } if ($type instanceof InputObjectType) { - if (!is_object($value) && !is_array($value) && !$value instanceof \Traversable) { + if (! is_object($value) && ! is_array($value) && ! $value instanceof \Traversable) { return self::ofErrors([ self::coercionError( - "Expected type {$type->name} to be an object", + sprintf('Expected type %s to be an object', $type->name), $blameNode, $path ), ]); } - $errors = []; + $errors = []; $coercedValue = []; - $fields = $type->getFields(); + $fields = $type->getFields(); foreach ($fields as $fieldName => $field) { - if (!array_key_exists($fieldName, $value)) { - if ($field->defaultValueExists()) { - $coercedValue[$fieldName] = $field->defaultValue; - } else if ($field->getType() instanceof NonNull) { - $fieldPath = self::printPath(self::atPath($path, $fieldName)); - $errors = self::add( - $errors, - self::coercionError( - "Field {$fieldPath} of required " . - "type {$field->type} was not provided", - $blameNode - ) - ); - } - } else { - $fieldValue = $value[$fieldName]; + if (array_key_exists($fieldName, $value)) { + $fieldValue = $value[$fieldName]; $coercedField = self::coerceValue( $fieldValue, $field->getType(), @@ -163,63 +171,76 @@ class Value } else { $coercedValue[$fieldName] = $coercedField['value']; } - } - } - - // Ensure every provided field is defined. - foreach ($value as $fieldName => $field) { - if (!array_key_exists($fieldName, $fields)) { - $suggestions = Utils::suggestionList( - $fieldName, - array_keys($fields) - ); - $didYouMean = $suggestions - ? "did you mean " . Utils::orList($suggestions) . "?" - : null; - $errors = self::add( + } elseif ($field->defaultValueExists()) { + $coercedValue[$fieldName] = $field->defaultValue; + } elseif ($field->getType() instanceof NonNull) { + $fieldPath = self::printPath(self::atPath($path, $fieldName)); + $errors = self::add( $errors, self::coercionError( - "Field \"{$fieldName}\" is not defined by type {$type->name}", - $blameNode, - $path, - $didYouMean + sprintf( + 'Field %s of required type %s was not provided', + $fieldPath, + $field->type->toString() + ), + $blameNode ) ); } } + // Ensure every provided field is defined. + foreach ($value as $fieldName => $field) { + if (array_key_exists($fieldName, $fields)) { + continue; + } + + $suggestions = Utils::suggestionList( + $fieldName, + array_keys($fields) + ); + $didYouMean = $suggestions + ? 'did you mean ' . Utils::orList($suggestions) . '?' + : null; + $errors = self::add( + $errors, + self::coercionError( + sprintf('Field "%s" is not defined by type %s', $fieldName, $type->name), + $blameNode, + $path, + $didYouMean + ) + ); + } + return $errors ? self::ofErrors($errors) : self::ofValue($coercedValue); } - throw new Error("Unexpected type {$type}"); + throw new Error(sprintf('Unexpected type %s', $type->name)); } - private static function ofValue($value) { - return ['errors' => null, 'value' => $value]; - } - - private static function ofErrors($errors) { + private static function ofErrors($errors) + { return ['errors' => $errors, 'value' => Utils::undefined()]; } - private static function add($errors, $moreErrors) { - return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]); - } - - private static function atPath($prev, $key) { - return ['prev' => $prev, 'key' => $key]; - } - /** - * @param string $message - * @param Node $blameNode - * @param array|null $path - * @param string $subMessage + * @param string $message + * @param Node $blameNode + * @param mixed[]|null $path + * @param string $subMessage * @param \Exception|\Throwable|null $originalError * @return Error */ - private static function coercionError($message, $blameNode, array $path = null, $subMessage = null, $originalError = null) { + private static function coercionError( + $message, + $blameNode, + ?array $path = null, + $subMessage = null, + $originalError = null + ) { $pathStr = self::printPath($path); + // Return a GraphQLError instance return new Error( $message . @@ -236,19 +257,50 @@ class Value /** * Build a string describing the path into the value where the error was found * - * @param $path + * @param mixed[]|null $path * @return string */ - private static function printPath(array $path = null) { - $pathStr = ''; + private static function printPath(?array $path = null) + { + $pathStr = ''; $currentPath = $path; - while($currentPath) { - $pathStr = + while ($currentPath) { + $pathStr = (is_string($currentPath['key']) ? '.' . $currentPath['key'] : '[' . $currentPath['key'] . ']') . $pathStr; $currentPath = $currentPath['prev']; } + return $pathStr ? 'value' . $pathStr : ''; } + + /** + * @param mixed $value + * @return (mixed|null)[] + */ + private static function ofValue($value) + { + return ['errors' => null, 'value' => $value]; + } + + /** + * @param mixed|null $prev + * @param mixed|null $key + * @return (mixed|null)[] + */ + private static function atPath($prev, $key) + { + return ['prev' => $prev, 'key' => $key]; + } + + /** + * @param Error[] $errors + * @param Error|Error[] $moreErrors + * @return Error[] + */ + private static function add($errors, $moreErrors) + { + return array_merge($errors, is_array($moreErrors) ? $moreErrors : [$moreErrors]); + } } diff --git a/tests/Utils/FindBreakingChangesTest.php b/tests/Utils/BreakingChangesFinderTest.php similarity index 84% rename from tests/Utils/FindBreakingChangesTest.php rename to tests/Utils/BreakingChangesFinderTest.php index fb8ae57..978733a 100644 --- a/tests/Utils/FindBreakingChangesTest.php +++ b/tests/Utils/BreakingChangesFinderTest.php @@ -1,4 +1,5 @@ FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED, 'description' => 'Type1 was removed.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findRemovedTypes($oldSchema, $newSchema) + BreakingChangesFinder::findRemovedTypes($oldSchema, $newSchema) ); - $this->assertEquals([], FindBreakingChanges::findRemovedTypes($oldSchema, $oldSchema)); + $this->assertEquals([], BreakingChangesFinder::findRemovedTypes($oldSchema, $oldSchema)); } /** @@ -109,14 +110,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_CHANGED_KIND, 'description' => 'Type1 changed from an Interface type to a Union type.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findTypesThatChangedKind($oldSchema, $newSchema) + BreakingChangesFinder::findTypesThatChangedKind($oldSchema, $newSchema) ); } @@ -208,60 +209,60 @@ class FindBreakingChangesTest extends TestCase $expectedFieldChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED, 'description' => 'Type1.field2 was removed.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field3 changed type from String to Boolean.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field4 changed type from TypeA to TypeB.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field6 changed type from String to [String].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field7 changed type from [String] to String.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field9 changed type from Int! to Int.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field10 changed type from [Int]! to [Int].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field11 changed type from Int to [Int]!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field13 changed type from [Int!] to [Int].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field14 changed type from [Int] to [[Int]].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field15 changed type from [[Int]] to [Int].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field16 changed type from Int! to [Int]!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'Type1.field18 changed type from [[Int!]!] to [[Int!]].', ], ]; - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema)); + $this->assertEquals($expectedFieldChanges, BreakingChangesFinder::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema)); } /** @@ -380,52 +381,52 @@ class FindBreakingChangesTest extends TestCase $expectedFieldChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field1 changed type from String to Int.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED, 'description' => 'InputType1.field2 was removed.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field3 changed type from [String] to String.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field5 changed type from String to String!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field6 changed type from [Int] to [Int]!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field8 changed type from Int to [Int]!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field9 changed type from [Int] to [Int!].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field11 changed type from [Int] to [[Int]].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field12 changed type from [[Int]] to [Int].', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field13 changed type from Int! to [Int]!.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'InputType1.field15 changed type from [[Int]!] to [[Int!]!].', ], ]; - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges']); + $this->assertEquals($expectedFieldChanges, BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges']); } /** @@ -461,14 +462,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_INPUT_FIELD_ADDED, 'description' => 'A non-null field requiredField on input type InputType1 was added.' ], ]; $this->assertEquals( $expected, - FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'] + BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'] ); } @@ -524,14 +525,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => 'Type2 was removed from union type UnionType1.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findTypesRemovedFromUnions($oldSchema, $newSchema) + BreakingChangesFinder::findTypesRemovedFromUnions($oldSchema, $newSchema) ); } @@ -569,14 +570,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => 'VALUE1 was removed from enum type EnumType1.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findValuesRemovedFromEnums($oldSchema, $newSchema) + BreakingChangesFinder::findValuesRemovedFromEnums($oldSchema, $newSchema) ); } @@ -646,20 +647,20 @@ class FindBreakingChangesTest extends TestCase $expectedChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED, 'description' => 'Type1.field1 arg name was removed', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED, 'description' => 'Interface1.field1 arg arg1 was removed', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_REMOVED, 'description' => 'Interface1.field1 arg objectArg was removed', ] ]; - $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + $this->assertEquals($expectedChanges, BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']); } /** @@ -731,56 +732,56 @@ class FindBreakingChangesTest extends TestCase $expectedChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg1 has changed type from String to Int', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg2 has changed type from String to [String]' ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg3 has changed type from [String] to String', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg4 has changed type from String to String!', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg5 has changed type from String! to Int', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg6 has changed type from String! to Int!', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg8 has changed type from Int to [Int]!', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg9 has changed type from [Int] to [Int!]', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg11 has changed type from [Int] to [[Int]]', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg12 has changed type from [[Int]] to [Int]', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg13 has changed type from Int! to [Int]!', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'Type1.field1 arg arg15 has changed type from [[Int]!] to [[Int!]!]', ], ]; - $this->assertEquals($expectedChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + $this->assertEquals($expectedChanges, BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']); } /** @@ -821,14 +822,15 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_ARG_ADDED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_ARG_ADDED, 'description' => 'A non-null arg newRequiredArg on Type1.field1 was added' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges'] + ); } /** @@ -885,7 +887,7 @@ class FindBreakingChangesTest extends TestCase 'types' => [$newType], ]); - $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + $this->assertEquals([], BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']); } /** @@ -925,7 +927,7 @@ class FindBreakingChangesTest extends TestCase 'types' => [$newType], ]); - $this->assertEquals([], FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['breakingChanges']); + $this->assertEquals([], BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['breakingChanges']); } /** @@ -964,14 +966,15 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, 'description' => 'Type1 no longer implements interface Interface1.' ], ]; $this->assertEquals( $expected, - FindBreakingChanges::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema)); + BreakingChangesFinder::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema) + ); } /** @@ -1178,11 +1181,11 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED, 'description' => 'TypeThatGetsRemoved was removed.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED, 'description' => 'TypeInUnion2 was removed.', ], /* This is reported in the js version because builtin sclar types are added on demand @@ -1192,52 +1195,52 @@ class FindBreakingChangesTest extends TestCase 'description' => 'Int was removed.' ],*/ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_CHANGED_KIND, 'description' => 'TypeThatChangesType changed from an Object type to an Interface type.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_REMOVED, 'description' => 'TypeThatHasBreakingFieldChanges.field1 was removed.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_FIELD_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_FIELD_CHANGED_KIND, 'description' => 'TypeThatHasBreakingFieldChanges.field2 changed type from String to Boolean.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION, 'description' => 'TypeInUnion2 was removed from union type UnionTypeThatLosesAType.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM, 'description' => 'VALUE0 was removed from enum type EnumTypeThatLosesAValue.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_ARG_CHANGED_KIND, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_ARG_CHANGED_KIND, 'description' => 'ArgThatChanges.field1 arg id has changed type from Int to String', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_INTERFACE_REMOVED_FROM_OBJECT, 'description' => 'TypeThatLosesInterface1 no longer implements interface Interface1.', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED, 'description' => 'skip was removed', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, 'description' => 'arg1 was removed from DirectiveThatRemovesArg', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, 'description' => 'A non-null arg arg1 on directive NonNullDirectiveAdded was added', ], [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, 'description' => 'QUERY was removed from Directive Name', ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findBreakingChanges($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findBreakingChanges($oldSchema, $newSchema)); } /** @@ -1257,12 +1260,12 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED, 'description' => "{$includeDirective->name} was removed", ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectives($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findRemovedDirectives($oldSchema, $newSchema)); } /** @@ -1280,12 +1283,12 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_REMOVED, 'description' => "{$deprecatedDirective->name} was removed", ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectives($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findRemovedDirectives($oldSchema, $newSchema)); } /** @@ -1318,12 +1321,12 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED, 'description' => "arg1 was removed from DirectiveWithArg", ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectiveArgs($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findRemovedDirectiveArgs($oldSchema, $newSchema)); } /** @@ -1357,12 +1360,12 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_NON_NULL_DIRECTIVE_ARG_ADDED, 'description' => "A non-null arg arg1 on directive DirectiveName was added", ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findAddedNonNullDirectiveArgs($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findAddedNonNullDirectiveArgs($oldSchema, $newSchema)); } /** @@ -1380,7 +1383,7 @@ class FindBreakingChangesTest extends TestCase 'locations' => [DirectiveLocation::FIELD_DEFINITION], ]); - $this->assertEquals([DirectiveLocation::QUERY], FindBreakingChanges::findRemovedLocationsForDirective($d1, $d2)); + $this->assertEquals([DirectiveLocation::QUERY], BreakingChangesFinder::findRemovedLocationsForDirective($d1, $d2)); } /** @@ -1411,12 +1414,12 @@ class FindBreakingChangesTest extends TestCase $expectedBreakingChanges = [ [ - 'type' => FindBreakingChanges::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, + 'type' => BreakingChangesFinder::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED, 'description' => "QUERY was removed from Directive Name", ] ]; - $this->assertEquals($expectedBreakingChanges, FindBreakingChanges::findRemovedDirectiveLocations($oldSchema, $newSchema)); + $this->assertEquals($expectedBreakingChanges, BreakingChangesFinder::findRemovedDirectiveLocations($oldSchema, $newSchema)); } // DESCRIBE: findDangerousChanges @@ -1469,14 +1472,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, 'description' => 'Type1.field1 arg name has changed defaultValue' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges'] + BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['dangerousChanges'] ); } @@ -1513,14 +1516,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, 'description' => 'VALUE2 was added to enum type EnumType1.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findValuesAddedToEnums($oldSchema, $newSchema) + BreakingChangesFinder::findValuesAddedToEnums($oldSchema, $newSchema) ); } @@ -1562,14 +1565,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, 'description' => 'Interface1 added to interfaces implemented by Type1.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findInterfacesAddedToObjectTypes($oldSchema, $newSchema) + BreakingChangesFinder::findInterfacesAddedToObjectTypes($oldSchema, $newSchema) ); } @@ -1620,14 +1623,14 @@ class FindBreakingChangesTest extends TestCase $expected = [ [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => 'Type2 was added to union type UnionType1.' ] ]; $this->assertEquals( $expected, - FindBreakingChanges::findTypesAddedToUnions($oldSchema, $newSchema) + BreakingChangesFinder::findTypesAddedToUnions($oldSchema, $newSchema) ); } @@ -1673,11 +1676,11 @@ class FindBreakingChangesTest extends TestCase $expectedFieldChanges = [ [ 'description' => 'A nullable field field2 on input type InputType1 was added.', - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_INPUT_FIELD_ADDED, ], ]; - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']); + $this->assertEquals($expectedFieldChanges, BreakingChangesFinder::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']); } /** @@ -1797,23 +1800,23 @@ class FindBreakingChangesTest extends TestCase $expectedDangerousChanges = [ [ 'description' => 'Type1.field1 arg name has changed defaultValue', - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED, ], [ 'description' => 'VALUE2 was added to enum type EnumType1.', - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM, ], [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_INTERFACE_ADDED_TO_OBJECT, 'description' => 'Interface1 added to interfaces implemented by TypeThatGainsInterface1.', ], [ - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION, 'description' => 'TypeInUnion2 was added to union type UnionTypeThatGainsAType.', ] ]; - $this->assertEquals($expectedDangerousChanges, FindBreakingChanges::findDangerousChanges($oldSchema, $newSchema)); + $this->assertEquals($expectedDangerousChanges, BreakingChangesFinder::findDangerousChanges($oldSchema, $newSchema)); } /** @@ -1865,10 +1868,10 @@ class FindBreakingChangesTest extends TestCase $expectedFieldChanges = [ [ 'description' => 'A nullable arg arg2 on Type1.field1 was added', - 'type' => FindBreakingChanges::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED + 'type' => BreakingChangesFinder::DANGEROUS_CHANGE_NULLABLE_ARG_ADDED, ], ]; - $this->assertEquals($expectedFieldChanges, FindBreakingChanges::findArgChanges($oldSchema, $newSchema)['dangerousChanges']); + $this->assertEquals($expectedFieldChanges, BreakingChangesFinder::findArgChanges($oldSchema, $newSchema)['dangerousChanges']); } }