Skip to content

Add validation rule UniqueEnumValueNames #1000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ You can find and compare releases at the [GitHub release page](https://github.co
- Expose structured enumeration of directive locations
- Add `AST::concatAST()` utility
- Allow lazy input object fields
- Add validation rule `UniqueEnumValueNames`

### Optimized

Expand Down
9 changes: 3 additions & 6 deletions src/Language/VisitorOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,9 @@

class VisitorOperation
{
/** @var bool */
public $doBreak;
public bool $doBreak = false;

/** @var bool */
public $doContinue;
public bool $doContinue = false;

/** @var bool */
public $removeNode;
public bool $removeNode = false;
}
33 changes: 0 additions & 33 deletions src/Type/SchemaValidationContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use GraphQL\Language\AST\DirectiveNode;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueDefinitionNode;
use GraphQL\Language\AST\FieldDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeDefinitionNode;
use GraphQL\Language\AST\InputObjectTypeExtensionNode;
Expand Down Expand Up @@ -958,15 +957,6 @@ private function validateEnumValues(EnumType $enumType): void
foreach ($enumValues as $enumValue) {
$valueName = $enumValue->name;

// Ensure no duplicates
$allNodes = $this->getEnumValueNodes($enumType, $valueName);
if (count($allNodes) > 1) {
$this->reportError(
sprintf('Enum type %s can include value %s only once.', $enumType->name, $valueName),
$allNodes
);
}

// Ensure valid name.
$this->validateName($enumValue);
if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
Expand All @@ -988,29 +978,6 @@ private function validateEnumValues(EnumType $enumType): void
}
}

/**
* @return array<int, EnumValueDefinitionNode>
*/
private function getEnumValueNodes(EnumType $enum, string $valueName): array
{
$allNodes = $enum->astNode !== null
? array_merge([$enum->astNode], $enum->extensionASTNodes)
: $enum->extensionASTNodes;

$values = [];
foreach ($allNodes as $node) {
foreach ($node->values as $value) {
if ($value->name->value !== $valueName) {
continue;
}

$values[] = $value;
}
}

return $values;
}

private function validateInputFields(InputObjectType $inputObj): void
{
$fieldMap = $inputObj->getFields();
Expand Down
17 changes: 3 additions & 14 deletions src/Utils/SchemaExtender.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Type\Definition\Directive;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Type\Definition\EnumValueDefinition;
use GraphQL\Type\Definition\FieldArgument;
use GraphQL\Type\Definition\ImplementingType;
use GraphQL\Type\Definition\InputObjectType;
Expand Down Expand Up @@ -215,14 +214,9 @@ protected static function extendInputFieldMap(InputObjectType $type): array
protected static function extendEnumValueMap(EnumType $type): array
{
$newValueMap = [];
/** @var array<string, EnumValueDefinition> $oldValueMap */
$oldValueMap = [];
foreach ($type->getValues() as $value) {
$oldValueMap[$value->name] = $value;
}

foreach ($oldValueMap as $key => $value) {
$newValueMap[$key] = [
foreach ($type->getValues() as $value) {
$newValueMap[$value->name] = [
'name' => $value->name,
'description' => $value->description,
'value' => $value->value,
Expand All @@ -239,12 +233,7 @@ protected static function extendEnumValueMap(EnumType $type): array
*/
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
foreach ($extension->values as $value) {
$valueName = $value->name->value;
if (isset($oldValueMap[$valueName])) {
throw new Error('Enum value "' . $type->name . '.' . $valueName . '" already exists in the schema. It cannot also be defined in this type extension.', [$value]);
}

$newValueMap[$valueName] = static::$astBuilder->buildEnumValue($value);
$newValueMap[$value->name->value] = static::$astBuilder->buildEnumValue($value);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/Validator/DocumentValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use GraphQL\Validator\Rules\SingleFieldSubscription;
use GraphQL\Validator\Rules\UniqueArgumentNames;
use GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
use GraphQL\Validator\Rules\UniqueEnumValueNames;
use GraphQL\Validator\Rules\UniqueFragmentNames;
use GraphQL\Validator\Rules\UniqueInputFieldNames;
use GraphQL\Validator\Rules\UniqueOperationNames;
Expand Down Expand Up @@ -201,6 +202,7 @@ public static function sdlRules()
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
UniqueArgumentNames::class => new UniqueArgumentNames(),
UniqueEnumValueNames::class => new UniqueEnumValueNames(),
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
];
Expand Down
70 changes: 70 additions & 0 deletions src/Validator/Rules/UniqueEnumValueNames.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace GraphQL\Validator\Rules;

use GraphQL\Error\Error;
use GraphQL\Language\AST\EnumTypeDefinitionNode;
use GraphQL\Language\AST\EnumTypeExtensionNode;
use GraphQL\Language\AST\EnumValueNode;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Visitor;
use GraphQL\Language\VisitorOperation;
use GraphQL\Type\Definition\EnumType;
use GraphQL\Validator\SDLValidationContext;

class UniqueEnumValueNames extends ValidationRule
{
public function getSDLVisitor(SDLValidationContext $context): array
{
/** @var array<string, array<string, EnumValueNode>> $knownValueNames */
$knownValueNames = [];

/**
* @param EnumTypeDefinitionNode|EnumTypeExtensionNode $enum
*/
$checkValueUniqueness = static function ($enum) use ($context, &$knownValueNames): VisitorOperation {
$typeName = $enum->name->value;

$schema = $context->getSchema();
$existingType = $schema !== null
? $schema->getType($typeName)
: null;

$valueNodes = $enum->values;

if (! isset($knownValueNames[$typeName])) {
$knownValueNames[$typeName] = [];
}

$valueNames = &$knownValueNames[$typeName];

foreach ($valueNodes as $valueDef) {
$valueNameNode = $valueDef->name;
$valueName = $valueNameNode->value;

if ($existingType instanceof EnumType && $existingType->getValue($valueName) !== null) {
$context->reportError(new Error(
"Enum value \"${typeName}.${valueName}\" already exists in the schema. It cannot also be defined in this type extension.",
$valueNameNode
));
} elseif (isset($valueNames[$valueName])) {
$context->reportError(new Error(
"Enum value \"${typeName}.${valueName}\" can only be defined once.",
[$valueNames[$valueName], $valueNameNode]
));
} else {
$valueNames[$valueName] = $valueNameNode;
}
}

return Visitor::skipNode();
};

return [
NodeKind::ENUM_TYPE_DEFINITION => $checkValueUniqueness,
NodeKind::ENUM_TYPE_EXTENSION => $checkValueUniqueness,
];
}
}
26 changes: 0 additions & 26 deletions tests/Type/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1093,32 +1093,6 @@ enum SomeEnum
);
}

/**
* @see it('rejects an Enum type with duplicate values')
*/
public function testRejectsAnEnumTypeWithDuplicateValues(): void
{
$schema = BuildSchema::build('
type Query {
field: SomeEnum
}

enum SomeEnum {
SOME_VALUE
SOME_VALUE
}
');
$this->assertMatchesValidationMessage(
$schema->validate(),
[
[
'message' => 'Enum type SomeEnum can include value SOME_VALUE only once.',
'locations' => [['line' => 7, 'column' => 9], ['line' => 8, 'column' => 9]],
],
]
);
}

public function testDoesNotAllowIsDeprecatedWithoutDeprecationReasonOnEnum(): void
{
$enum = new EnumType([
Expand Down
19 changes: 0 additions & 19 deletions tests/Utils/SchemaExtenderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1392,25 +1392,6 @@ public function testDoesNotAllowReplacingADefaultDirective(): void
}
}

/**
* @see it('does not allow replacing an existing enum value')
*/
public function testDoesNotAllowReplacingAnExistingEnumValue(): void
{
$sdl = '
extend enum SomeEnum {
ONE
}
';

try {
$this->extendTestSchema($sdl);
self::fail();
} catch (Error $error) {
self::assertEquals('Enum value "SomeEnum.ONE" already exists in the schema. It cannot also be defined in this type extension.', $error->getMessage());
}
}

/**
* @see it('does not automatically include common root type names')
*/
Expand Down
Loading