Skip to content

Commit 4f7db61

Browse files
authored
Extract check for unique type names into separate rule (#998)
1 parent e8dfe9b commit 4f7db61

File tree

8 files changed

+281
-119
lines changed

8 files changed

+281
-119
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ You can find and compare releases at the [GitHub release page](https://github.co
5454
- Add ability to remove custom validation rules after adding them via `DocumentValidator::removeRule()`
5555
- Allow lazy enum values
5656
- Make `Node` implement `JsonSerializable`
57+
- Add SDL validation rule `UniqueTypeNames` (#998)
5758

5859
### Optimized
5960

src/Utils/BuildSchema.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,7 @@ public function buildSchema(): Schema
140140
$schemaDef = $definition;
141141
break;
142142
case $definition instanceof TypeDefinitionNode:
143-
$typeName = $definition->name->value;
144-
if (isset($this->nodeMap[$typeName])) {
145-
throw new Error('Type "' . $typeName . '" was defined more than once.');
146-
}
147-
148-
$this->nodeMap[$typeName] = $definition;
143+
$this->nodeMap[$definition->name->value] = $definition;
149144
break;
150145
case $definition instanceof DirectiveDefinitionNode:
151146
$directiveDefs[] = $definition;

src/Utils/SchemaExtender.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -579,15 +579,7 @@ public static function extend(
579579
} elseif ($def instanceof SchemaTypeExtensionNode) {
580580
$schemaExtensions[] = $def;
581581
} elseif ($def instanceof TypeDefinitionNode) {
582-
$typeName = $def->name->value;
583-
584-
$type = $schema->getType($typeName);
585-
586-
if ($type !== null) {
587-
throw new Error("Type \"{$typeName}\" already exists in the schema. It cannot also be defined in this type definition.", [$def]);
588-
}
589-
590-
$typeDefinitionMap[$typeName] = $def;
582+
$typeDefinitionMap[$def->name->value] = $def;
591583
} elseif ($def instanceof TypeExtensionNode) {
592584
$extendedTypeName = $def->name->value;
593585
$existingType = $schema->getType($extendedTypeName);

src/Validator/DocumentValidator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use GraphQL\Validator\Rules\UniqueInputFieldNames;
4141
use GraphQL\Validator\Rules\UniqueOperationNames;
4242
use GraphQL\Validator\Rules\UniqueOperationTypes;
43+
use GraphQL\Validator\Rules\UniqueTypeNames;
4344
use GraphQL\Validator\Rules\UniqueVariableNames;
4445
use GraphQL\Validator\Rules\ValidationRule;
4546
use GraphQL\Validator\Rules\ValuesOfCorrectType;
@@ -201,6 +202,7 @@ public static function sdlRules(): array
201202
return self::$sdlRules ??= [
202203
LoneSchemaDefinition::class => new LoneSchemaDefinition(),
203204
UniqueOperationTypes::class => new UniqueOperationTypes(),
205+
UniqueTypeNames::class => new UniqueTypeNames(),
204206
KnownDirectives::class => new KnownDirectives(),
205207
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
206208
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace GraphQL\Validator\Rules;
6+
7+
use function array_key_exists;
8+
use GraphQL\Error\Error;
9+
use GraphQL\Language\AST\NameNode;
10+
use GraphQL\Language\AST\NodeKind;
11+
use GraphQL\Language\Visitor;
12+
use GraphQL\Language\VisitorOperation;
13+
use GraphQL\Validator\SDLValidationContext;
14+
15+
/**
16+
* Unique type names.
17+
*
18+
* A GraphQL document is only valid if all defined types have unique names.
19+
*/
20+
class UniqueTypeNames extends ValidationRule
21+
{
22+
public function getSDLVisitor(SDLValidationContext $context): array
23+
{
24+
$schema = $context->getSchema();
25+
/** @var array<string, NameNode> $knownTypeNames */
26+
$knownTypeNames = [];
27+
$checkTypeName = static function ($node) use ($context, $schema, &$knownTypeNames): ?VisitorOperation {
28+
$typeName = $node->name->value;
29+
30+
if ($schema !== null && $schema->getType($typeName) !== null) {
31+
$context->reportError(
32+
new Error(
33+
'Type "' . $typeName . '" already exists in the schema. It cannot also be defined in this type definition.',
34+
$node->name,
35+
),
36+
);
37+
38+
return null;
39+
}
40+
41+
if (array_key_exists($typeName, $knownTypeNames)) {
42+
$context->reportError(
43+
new Error(
44+
'There can be only one type named "' . $typeName . '".',
45+
[
46+
$knownTypeNames[$typeName],
47+
$node->name,
48+
]
49+
),
50+
);
51+
} else {
52+
$knownTypeNames[$typeName] = $node->name;
53+
}
54+
55+
return Visitor::skipNode();
56+
};
57+
58+
return [
59+
NodeKind::SCALAR_TYPE_DEFINITION => $checkTypeName,
60+
NodeKind::OBJECT_TYPE_DEFINITION => $checkTypeName,
61+
NodeKind::INTERFACE_TYPE_DEFINITION => $checkTypeName,
62+
NodeKind::UNION_TYPE_DEFINITION => $checkTypeName,
63+
NodeKind::ENUM_TYPE_DEFINITION => $checkTypeName,
64+
NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkTypeName,
65+
];
66+
}
67+
}

tests/Utils/BuildSchemaLegacyTest.php

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* Their counterparts have been removed from `buildASTSchema-test.js` and moved elsewhere,
1515
* but these changes to `graphql-js` haven't been reflected in `graphql-php` yet.
1616
* TODO align with:
17-
* - https://github.com/graphql/graphql-js/commit/257797a0ebdddd3da6e75b7c237fdc12a1a7c75a
1817
* - https://github.com/graphql/graphql-js/commit/3b9ea61f2348215dee755f779caef83df749d2bb
1918
* - https://github.com/graphql/graphql-js/commit/64a5c3448a201737f9218856786c51d66f2deabd.
2019
*/
@@ -319,28 +318,4 @@ public function testDoesNotConsiderFragmentNames(): void
319318
$doc = Parser::parse($sdl);
320319
BuildSchema::buildAST($doc);
321320
}
322-
323-
/**
324-
* @see it('Forbids duplicate type definitions')
325-
*/
326-
public function testForbidsDuplicateTypeDefinitions(): void
327-
{
328-
$sdl = '
329-
schema {
330-
query: Repeated
331-
}
332-
333-
type Repeated {
334-
id: Int
335-
}
336-
337-
type Repeated {
338-
id: String
339-
}
340-
';
341-
$doc = Parser::parse($sdl);
342-
$this->expectException(Error::class);
343-
$this->expectExceptionMessage('Type "Repeated" was defined more than once.');
344-
BuildSchema::buildAST($doc);
345-
}
346321
}

tests/Utils/SchemaExtenderLegacyTest.php

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
* but these changes to `graphql-js` haven't been reflected in `graphql-php` yet.
3030
* TODO align with:
3131
* - https://github.com/graphql/graphql-js/commit/c1745228b2ae5ec89b8de36ea766d544607e21ea
32-
* - https://github.com/graphql/graphql-js/commit/257797a0ebdddd3da6e75b7c237fdc12a1a7c75a
3332
* - https://github.com/graphql/graphql-js/commit/3b9ea61f2348215dee755f779caef83df749d2bb
3433
* - https://github.com/graphql/graphql-js/commit/e6a3f08cc92594f68a6e61d3d4b46a6d279f845e.
3534
*/
@@ -264,84 +263,6 @@ public function testDoesNotAllowReplacingAnExistingField(): void
264263
}
265264
}
266265

267-
// Extract check for unique type names into separate rule
268-
269-
/**
270-
* @see it('does not allow replacing an existing type')
271-
*/
272-
public function testDoesNotAllowReplacingAnExistingType(): void
273-
{
274-
$existingTypeError = static function ($type): string {
275-
return 'Type "' . $type . '" already exists in the schema. It cannot also be defined in this type definition.';
276-
};
277-
278-
$typeSDL = '
279-
type Bar
280-
';
281-
282-
try {
283-
$this->extendTestSchema($typeSDL);
284-
self::fail();
285-
} catch (Error $error) {
286-
self::assertEquals($existingTypeError('Bar'), $error->getMessage());
287-
}
288-
289-
$scalarSDL = '
290-
scalar SomeScalar
291-
';
292-
293-
try {
294-
$this->extendTestSchema($scalarSDL);
295-
self::fail();
296-
} catch (Error $error) {
297-
self::assertEquals($existingTypeError('SomeScalar'), $error->getMessage());
298-
}
299-
300-
$interfaceSDL = '
301-
interface SomeInterface
302-
';
303-
304-
try {
305-
$this->extendTestSchema($interfaceSDL);
306-
self::fail();
307-
} catch (Error $error) {
308-
self::assertEquals($existingTypeError('SomeInterface'), $error->getMessage());
309-
}
310-
311-
$enumSDL = '
312-
enum SomeEnum
313-
';
314-
315-
try {
316-
$this->extendTestSchema($enumSDL);
317-
self::fail();
318-
} catch (Error $error) {
319-
self::assertEquals($existingTypeError('SomeEnum'), $error->getMessage());
320-
}
321-
322-
$unionSDL = '
323-
union SomeUnion
324-
';
325-
326-
try {
327-
$this->extendTestSchema($unionSDL);
328-
self::fail();
329-
} catch (Error $error) {
330-
self::assertEquals($existingTypeError('SomeUnion'), $error->getMessage());
331-
}
332-
333-
$inputSDL = '
334-
input SomeInput
335-
';
336-
337-
try {
338-
$this->extendTestSchema($inputSDL);
339-
self::fail();
340-
} catch (Error $error) {
341-
self::assertEquals($existingTypeError('SomeInput'), $error->getMessage());
342-
}
343-
}
344-
345266
// Validation: add support of SDL to KnownTypeNames
346267

347268
/**

0 commit comments

Comments
 (0)