Skip to content

Commit 07ea968

Browse files
authored
Extract check for possible extensions into a separate rule (#1110)
1 parent d4620e1 commit 07ea968

File tree

5 files changed

+601
-156
lines changed

5 files changed

+601
-156
lines changed

src/Utils/SchemaExtender.php

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,7 @@ public static function extend(
9595
} elseif ($def instanceof TypeDefinitionNode) {
9696
$typeDefinitionMap[$def->name->value] = $def;
9797
} elseif ($def instanceof TypeExtensionNode) {
98-
$extendedTypeName = $def->name->value;
99-
$existingType = $schema->getType($extendedTypeName);
100-
if ($existingType === null) {
101-
throw new Error('Cannot extend type "' . $extendedTypeName . '" because it does not exist in the existing schema.', [$def]);
102-
}
103-
104-
static::assertTypeMatchesExtension($existingType, $def);
105-
static::$typeExtensionsMap[$extendedTypeName][] = $def;
98+
static::$typeExtensionsMap[$def->name->value][] = $def;
10699
} elseif ($def instanceof DirectiveDefinitionNode) {
107100
$directiveDefinitions[] = $def;
108101
}
@@ -191,62 +184,6 @@ protected static function extensionASTNodes(NamedType $type): ?array
191184
);
192185
}
193186

194-
/**
195-
* @param Type&NamedType $type
196-
*
197-
* @throws Error
198-
*/
199-
protected static function assertTypeMatchesExtension(NamedType $type, Node $node): void
200-
{
201-
switch (true) {
202-
case $node instanceof ObjectTypeExtensionNode:
203-
if (! ($type instanceof ObjectType)) {
204-
throw new Error(
205-
'Cannot extend non-object type "' . $type->name . '".',
206-
[$node]
207-
);
208-
}
209-
210-
break;
211-
case $node instanceof InterfaceTypeExtensionNode:
212-
if (! ($type instanceof InterfaceType)) {
213-
throw new Error(
214-
'Cannot extend non-interface type "' . $type->name . '".',
215-
[$node]
216-
);
217-
}
218-
219-
break;
220-
case $node instanceof EnumTypeExtensionNode:
221-
if (! ($type instanceof EnumType)) {
222-
throw new Error(
223-
'Cannot extend non-enum type "' . $type->name . '".',
224-
[$node]
225-
);
226-
}
227-
228-
break;
229-
case $node instanceof UnionTypeExtensionNode:
230-
if (! ($type instanceof UnionType)) {
231-
throw new Error(
232-
'Cannot extend non-union type "' . $type->name . '".',
233-
[$node]
234-
);
235-
}
236-
237-
break;
238-
case $node instanceof InputObjectTypeExtensionNode:
239-
if (! ($type instanceof InputObjectType)) {
240-
throw new Error(
241-
'Cannot extend non-input object type "' . $type->name . '".',
242-
[$node]
243-
);
244-
}
245-
246-
break;
247-
}
248-
}
249-
250187
protected static function extendScalarType(ScalarType $type): CustomScalarType
251188
{
252189
/** @var array<int, ScalarTypeExtensionNode> $extensionASTNodes */
@@ -334,7 +271,7 @@ protected static function extendInputFieldMap(InputObjectType $type): array
334271

335272
if (isset(static::$typeExtensionsMap[$type->name])) {
336273
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
337-
assert($extension instanceof InputObjectTypeExtensionNode, 'proven by assertTypeMatchesExtension()');
274+
assert($extension instanceof InputObjectTypeExtensionNode, 'proven by schema validation');
338275

339276
foreach ($extension->fields as $field) {
340277
$fieldName = $field->name->value;
@@ -369,7 +306,7 @@ protected static function extendEnumValueMap(EnumType $type): array
369306

370307
if (isset(static::$typeExtensionsMap[$type->name])) {
371308
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
372-
assert($extension instanceof EnumTypeExtensionNode, 'proven by assertTypeMatchesExtension()');
309+
assert($extension instanceof EnumTypeExtensionNode, 'proven by schema validation');
373310

374311
foreach ($extension->values as $value) {
375312
$newValueMap[$value->name->value] = static::$astBuilder->buildEnumValue($value);
@@ -392,7 +329,7 @@ protected static function extendUnionPossibleTypes(UnionType $type): array
392329

393330
if (isset(static::$typeExtensionsMap[$type->name])) {
394331
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
395-
assert($extension instanceof UnionTypeExtensionNode, 'proven by assertTypeMatchesExtension()');
332+
assert($extension instanceof UnionTypeExtensionNode, 'proven by schema validation');
396333

397334
foreach ($extension->types as $namedType) {
398335
$possibleTypes[] = static::$astBuilder->buildType($namedType);
@@ -420,7 +357,7 @@ protected static function extendImplementedInterfaces(ImplementingType $type): a
420357
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
421358
assert(
422359
$extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
423-
'proven by assertTypeMatchesExtension()'
360+
'proven by schema validation'
424361
);
425362

426363
foreach ($extension->interfaces as $namedType) {
@@ -517,7 +454,7 @@ protected static function extendFieldMap(Type $type): array
517454
foreach (static::$typeExtensionsMap[$type->name] as $extension) {
518455
assert(
519456
$extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
520-
'proven by assertTypeMatchesExtension()'
457+
'proven by schema validation'
521458
);
522459

523460
foreach ($extension->fields as $field) {

src/Validator/DocumentValidator.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use GraphQL\Validator\Rules\NoUnusedVariables;
2727
use GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
2828
use GraphQL\Validator\Rules\PossibleFragmentSpreads;
29+
use GraphQL\Validator\Rules\PossibleTypeExtensions;
2930
use GraphQL\Validator\Rules\ProvidedRequiredArguments;
3031
use GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
3132
use GraphQL\Validator\Rules\QueryComplexity;
@@ -210,6 +211,7 @@ public static function sdlRules(): array
210211
KnownDirectives::class => new KnownDirectives(),
211212
KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
212213
UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
214+
PossibleTypeExtensions::class => new PossibleTypeExtensions(),
213215
UniqueArgumentNames::class => new UniqueArgumentNames(),
214216
UniqueEnumValueNames::class => new UniqueEnumValueNames(),
215217
UniqueInputFieldNames::class => new UniqueInputFieldNames(),
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace GraphQL\Validator\Rules;
4+
5+
use GraphQL\Error\Error;
6+
use GraphQL\Error\InvariantViolation;
7+
use GraphQL\Language\AST\Node;
8+
use GraphQL\Language\AST\NodeKind;
9+
use GraphQL\Language\AST\TypeDefinitionNode;
10+
use GraphQL\Language\VisitorOperation;
11+
use GraphQL\Type\Definition\EnumType;
12+
use GraphQL\Type\Definition\InputObjectType;
13+
use GraphQL\Type\Definition\InterfaceType;
14+
use GraphQL\Type\Definition\NamedType;
15+
use GraphQL\Type\Definition\ObjectType;
16+
use GraphQL\Type\Definition\ScalarType;
17+
use GraphQL\Type\Definition\UnionType;
18+
use GraphQL\Utils\Utils;
19+
use GraphQL\Validator\SDLValidationContext;
20+
21+
/**
22+
* Possible type extension.
23+
*
24+
* A type extension is only valid if the type is defined and has the same kind.
25+
*/
26+
class PossibleTypeExtensions extends ValidationRule
27+
{
28+
public function getSDLVisitor(SDLValidationContext $context): array
29+
{
30+
$schema = $context->getSchema();
31+
32+
/** @var array<string, TypeDefinitionNode&Node> $definedTypes */
33+
$definedTypes = [];
34+
foreach ($context->getDocument()->definitions as $def) {
35+
if ($def instanceof TypeDefinitionNode) {
36+
$definedTypes[$def->name->value] = $def;
37+
}
38+
}
39+
40+
$checkTypeExtension = static function ($node) use ($context, $schema, &$definedTypes): ?VisitorOperation {
41+
$typeName = $node->name->value;
42+
$defNode = $definedTypes[$typeName] ?? null;
43+
$existingType = $schema !== null
44+
? $schema->getType($typeName)
45+
: null;
46+
47+
$expectedKind = null;
48+
if ($defNode !== null) {
49+
$expectedKind = self::defKindToExtKind($defNode->kind);
50+
} elseif ($existingType !== null) {
51+
$expectedKind = self::typeToExtKind($existingType);
52+
}
53+
54+
if ($expectedKind !== null) {
55+
if ($expectedKind !== $node->kind) {
56+
$kindStr = self::extensionKindToTypeName($node->kind);
57+
$context->reportError(
58+
new Error(
59+
'Cannot extend non-' . $kindStr . ' type "' . $typeName . '".',
60+
$defNode !== null
61+
? [$defNode, $node]
62+
: $node,
63+
),
64+
);
65+
}
66+
} else {
67+
$existingTypesMap = $schema !== null
68+
? $schema->getTypeMap()
69+
: [];
70+
$allTypeNames = [
71+
...array_keys($definedTypes),
72+
...array_keys($existingTypesMap),
73+
];
74+
$suggestedTypes = Utils::suggestionList($typeName, $allTypeNames);
75+
$didYouMean = \count($suggestedTypes) > 0
76+
? ' Did you mean ' . Utils::quotedOrList($suggestedTypes) . '?'
77+
: '';
78+
$context->reportError(
79+
new Error(
80+
'Cannot extend type "' . $typeName . '" because it is not defined.' . $didYouMean,
81+
$node->name,
82+
),
83+
);
84+
}
85+
86+
return null;
87+
};
88+
89+
return [
90+
NodeKind::SCALAR_TYPE_EXTENSION => $checkTypeExtension,
91+
NodeKind::OBJECT_TYPE_EXTENSION => $checkTypeExtension,
92+
NodeKind::INTERFACE_TYPE_EXTENSION => $checkTypeExtension,
93+
NodeKind::UNION_TYPE_EXTENSION => $checkTypeExtension,
94+
NodeKind::ENUM_TYPE_EXTENSION => $checkTypeExtension,
95+
NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkTypeExtension,
96+
];
97+
}
98+
99+
private static function defKindToExtKind(string $kind): string
100+
{
101+
switch ($kind) {
102+
case NodeKind::SCALAR_TYPE_DEFINITION:
103+
return NodeKind::SCALAR_TYPE_EXTENSION;
104+
case NodeKind::OBJECT_TYPE_DEFINITION:
105+
return NodeKind::OBJECT_TYPE_EXTENSION;
106+
case NodeKind::INTERFACE_TYPE_DEFINITION:
107+
return NodeKind::INTERFACE_TYPE_EXTENSION;
108+
case NodeKind::UNION_TYPE_DEFINITION:
109+
return NodeKind::UNION_TYPE_EXTENSION;
110+
case NodeKind::ENUM_TYPE_DEFINITION:
111+
return NodeKind::ENUM_TYPE_EXTENSION;
112+
case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
113+
return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
114+
default:
115+
throw new InvariantViolation("Unexpected definition kind: {$kind}");
116+
}
117+
}
118+
119+
private static function typeToExtKind(NamedType $type): string
120+
{
121+
switch (true) {
122+
case $type instanceof ScalarType:
123+
return NodeKind::SCALAR_TYPE_EXTENSION;
124+
case $type instanceof ObjectType:
125+
return NodeKind::OBJECT_TYPE_EXTENSION;
126+
case $type instanceof InterfaceType:
127+
return NodeKind::INTERFACE_TYPE_EXTENSION;
128+
case $type instanceof UnionType:
129+
return NodeKind::UNION_TYPE_EXTENSION;
130+
case $type instanceof EnumType:
131+
return NodeKind::ENUM_TYPE_EXTENSION;
132+
case $type instanceof InputObjectType:
133+
return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
134+
default:
135+
throw new InvariantViolation('Unexpected type: ' . Utils::printSafe($type));
136+
}
137+
}
138+
139+
private static function extensionKindToTypeName(string $kind): string
140+
{
141+
switch ($kind) {
142+
case NodeKind::SCALAR_TYPE_EXTENSION:
143+
return 'scalar';
144+
case NodeKind::OBJECT_TYPE_EXTENSION:
145+
return 'object';
146+
case NodeKind::INTERFACE_TYPE_EXTENSION:
147+
return 'interface';
148+
case NodeKind::UNION_TYPE_EXTENSION:
149+
return 'union';
150+
case NodeKind::ENUM_TYPE_EXTENSION:
151+
return 'enum';
152+
case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
153+
return 'input object';
154+
default:
155+
throw new InvariantViolation("Unexpected extension kind: {$kind}");
156+
}
157+
}
158+
}

tests/Utils/SchemaExtenderLegacyTest.php

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -234,91 +234,4 @@ public function testDoesNotAllowReplacingAnExistingField(): void
234234
self::assertEquals($existingFieldError('SomeInput', 'fooArg'), $error->getMessage());
235235
}
236236
}
237-
238-
// Extract check for possible extensions into a separate rule (#1643)
239-
240-
/**
241-
* @see it('does not allow extending an unknown type')
242-
*/
243-
public function testDoesNotAllowExtendingAnUnknownType(): void
244-
{
245-
$sdls = [
246-
'extend scalar UnknownType @foo',
247-
'extend type UnknownType @foo',
248-
'extend interface UnknownType @foo',
249-
'extend enum UnknownType @foo',
250-
'extend union UnknownType @foo',
251-
'extend input UnknownType @foo',
252-
];
253-
254-
foreach ($sdls as $sdl) {
255-
try {
256-
$this->extendTestSchema($sdl);
257-
self::fail();
258-
} catch (Error $error) {
259-
self::assertEquals('Cannot extend type "UnknownType" because it does not exist in the existing schema.', $error->getMessage());
260-
}
261-
}
262-
}
263-
264-
/**
265-
* @see it('does not allow extending a mismatch type')
266-
*/
267-
public function testDoesNotAllowExtendingAMismatchType(): void
268-
{
269-
$typeSDL = '
270-
extend type SomeInterface @foo
271-
';
272-
273-
try {
274-
$this->extendTestSchema($typeSDL);
275-
self::fail();
276-
} catch (Error $error) {
277-
self::assertEquals('Cannot extend non-object type "SomeInterface".', $error->getMessage());
278-
}
279-
280-
$interfaceSDL = '
281-
extend interface Foo @foo
282-
';
283-
284-
try {
285-
$this->extendTestSchema($interfaceSDL);
286-
self::fail();
287-
} catch (Error $error) {
288-
self::assertEquals('Cannot extend non-interface type "Foo".', $error->getMessage());
289-
}
290-
291-
$enumSDL = '
292-
extend enum Foo @foo
293-
';
294-
295-
try {
296-
$this->extendTestSchema($enumSDL);
297-
self::fail();
298-
} catch (Error $error) {
299-
self::assertEquals('Cannot extend non-enum type "Foo".', $error->getMessage());
300-
}
301-
302-
$unionSDL = '
303-
extend union Foo @foo
304-
';
305-
306-
try {
307-
$this->extendTestSchema($unionSDL);
308-
self::fail();
309-
} catch (Error $error) {
310-
self::assertEquals('Cannot extend non-union type "Foo".', $error->getMessage());
311-
}
312-
313-
$inputSDL = '
314-
extend input Foo @foo
315-
';
316-
317-
try {
318-
$this->extendTestSchema($inputSDL);
319-
self::fail();
320-
} catch (Error $error) {
321-
self::assertEquals('Cannot extend non-input object type "Foo".', $error->getMessage());
322-
}
323-
}
324237
}

0 commit comments

Comments
 (0)