Passed
Push — master ( 21e0c8...7c1977 )
by Vladimir
06:02
created

SchemaValidationContext::validateInterfaces()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 14
rs 10
c 0
b 0
f 0
ccs 9
cts 9
cp 1
cc 2
nc 2
nop 1
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Type;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\EnumValueDefinitionNode;
9
use GraphQL\Language\AST\FieldDefinitionNode;
10
use GraphQL\Language\AST\InputValueDefinitionNode;
11
use GraphQL\Language\AST\InterfaceTypeDefinitionNode;
12
use GraphQL\Language\AST\InterfaceTypeExtensionNode;
13
use GraphQL\Language\AST\NamedTypeNode;
14
use GraphQL\Language\AST\Node;
15
use GraphQL\Language\AST\ObjectTypeDefinitionNode;
16
use GraphQL\Language\AST\ObjectTypeExtensionNode;
17
use GraphQL\Language\AST\SchemaDefinitionNode;
18
use GraphQL\Language\AST\TypeDefinitionNode;
19
use GraphQL\Language\AST\TypeNode;
20
use GraphQL\Type\Definition\Directive;
21
use GraphQL\Type\Definition\EnumType;
22
use GraphQL\Type\Definition\EnumValueDefinition;
23
use GraphQL\Type\Definition\FieldDefinition;
24
use GraphQL\Type\Definition\InputObjectField;
25
use GraphQL\Type\Definition\InputObjectType;
26
use GraphQL\Type\Definition\InterfaceType;
27
use GraphQL\Type\Definition\NamedType;
28
use GraphQL\Type\Definition\NonNull;
29
use GraphQL\Type\Definition\ObjectType;
30
use GraphQL\Type\Definition\Type;
31
use GraphQL\Type\Definition\UnionType;
32
use GraphQL\Utils\TypeComparators;
33
use GraphQL\Utils\Utils;
34
use function array_filter;
35
use function array_key_exists;
36
use function array_merge;
37
use function count;
38
use function is_array;
39
use function is_object;
40
use function iterator_to_array;
41
use function sprintf;
42
43
class SchemaValidationContext
44
{
45
    /** @var Error[] */
46
    private $errors = [];
47
48
    /** @var Schema */
49
    private $schema;
50
51 79
    public function __construct(Schema $schema)
52
    {
53 79
        $this->schema = $schema;
54 79
    }
55
56
    /**
57
     * @return Error[]
58
     */
59 79
    public function getErrors()
60
    {
61 79
        return $this->errors;
62
    }
63
64 79
    public function validateRootTypes()
65
    {
66 79
        $queryType = $this->schema->getQueryType();
67 79
        if (! $queryType) {
0 ignored issues
show
introduced by
$queryType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
68 2
            $this->reportError(
69 2
                'Query root type must be provided.',
70 2
                $this->schema->getAstNode()
71
            );
72 77
        } elseif (! $queryType instanceof ObjectType) {
0 ignored issues
show
introduced by
$queryType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
73 1
            $this->reportError(
74 1
                'Query root type must be Object type, it cannot be ' . Utils::printSafe($queryType) . '.',
75 1
                $this->getOperationTypeNode($queryType, 'query')
76
            );
77
        }
78
79 79
        $mutationType = $this->schema->getMutationType();
80 79
        if ($mutationType && ! $mutationType instanceof ObjectType) {
0 ignored issues
show
introduced by
$mutationType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
81 1
            $this->reportError(
82 1
                'Mutation root type must be Object type if provided, it cannot be ' . Utils::printSafe($mutationType) . '.',
83 1
                $this->getOperationTypeNode($mutationType, 'mutation')
84
            );
85
        }
86
87 79
        $subscriptionType = $this->schema->getSubscriptionType();
88 79
        if (! $subscriptionType || $subscriptionType instanceof ObjectType) {
0 ignored issues
show
introduced by
$subscriptionType is of type GraphQL\Type\Definition\ObjectType, thus it always evaluated to true.
Loading history...
introduced by
$subscriptionType is always a sub-type of GraphQL\Type\Definition\ObjectType.
Loading history...
89 78
            return;
90
        }
91
92 1
        $this->reportError(
93 1
            'Subscription root type must be Object type if provided, it cannot be ' . Utils::printSafe($subscriptionType) . '.',
94 1
            $this->getOperationTypeNode($subscriptionType, 'subscription')
95
        );
96 1
    }
97
98
    /**
99
     * @param string                                       $message
100
     * @param Node[]|Node|TypeNode|TypeDefinitionNode|null $nodes
101
     */
102 45
    private function reportError($message, $nodes = null)
103
    {
104 45
        $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
105 45
        $this->addError(new Error($message, $nodes));
106 45
    }
107
108
    /**
109
     * @param Error $error
110
     */
111 50
    private function addError($error)
112
    {
113 50
        $this->errors[] = $error;
114 50
    }
115
116
    /**
117
     * @param Type   $type
118
     * @param string $operation
119
     *
120
     * @return TypeNode|TypeDefinitionNode
121
     */
122 3
    private function getOperationTypeNode($type, $operation)
123
    {
124 3
        $astNode = $this->schema->getAstNode();
125
126 3
        $operationTypeNode = null;
127 3
        if ($astNode instanceof SchemaDefinitionNode) {
0 ignored issues
show
introduced by
$astNode is always a sub-type of GraphQL\Language\AST\SchemaDefinitionNode.
Loading history...
128 3
            $operationTypeNode = null;
129
130 3
            foreach ($astNode->operationTypes as $operationType) {
131 3
                if ($operationType->operation === $operation) {
132 3
                    $operationTypeNode = $operationType;
133 3
                    break;
134
                }
135
            }
136
        }
137
138 3
        return $operationTypeNode ? $operationTypeNode->type : ($type ? $type->astNode : null);
0 ignored issues
show
introduced by
$type is of type GraphQL\Type\Definition\Type, thus it always evaluated to true.
Loading history...
139
    }
140
141 79
    public function validateDirectives()
142
    {
143 79
        $directives = $this->schema->getDirectives();
144 79
        foreach ($directives as $directive) {
145
            // Ensure all directives are in fact GraphQL directives.
146 79
            if (! $directive instanceof Directive) {
147 1
                $this->reportError(
148 1
                    'Expected directive but got: ' . Utils::printSafe($directive) . '.',
149 1
                    is_object($directive) ? $directive->astNode : null
150
                );
151 1
                continue;
152
            }
153
154
            // Ensure they are named correctly.
155 78
            $this->validateName($directive);
156
157
            // TODO: Ensure proper locations.
158
159 78
            $argNames = [];
160 78
            foreach ($directive->args as $arg) {
161 78
                $argName = $arg->name;
162
163
                // Ensure they are named correctly.
164 78
                $this->validateName($directive);
165
166 78
                if (isset($argNames[$argName])) {
167
                    $this->reportError(
168
                        sprintf('Argument @%s(%s:) can only be defined once.', $directive->name, $argName),
169
                        $this->getAllDirectiveArgNodes($directive, $argName)
170
                    );
171
                    continue;
172
                }
173
174 78
                $argNames[$argName] = true;
175
176
                // Ensure the type is an input type.
177 78
                if (Type::isInputType($arg->getType())) {
178 78
                    continue;
179
                }
180
181
                $this->reportError(
182
                    sprintf(
183
                        'The type of @%s(%s:) must be Input Type but got: %s.',
184
                        $directive->name,
185
                        $argName,
186
                        Utils::printSafe($arg->getType())
187
                    ),
188 78
                    $this->getDirectiveArgTypeNode($directive, $argName)
189
                );
190
            }
191
        }
192 79
    }
193
194
    /**
195
     * @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
196
     */
197 79
    private function validateName($node)
198
    {
199
        // Ensure names are valid, however introspection types opt out.
200 79
        $error = Utils::isValidNameError($node->name, $node->astNode);
201 79
        if (! $error || Introspection::isIntrospectionType($node)) {
0 ignored issues
show
Bug introduced by
It seems like $node can also be of type GraphQL\Type\Definition\Directive and GraphQL\Type\Definition\EnumValueDefinition and GraphQL\Type\Definition\FieldDefinition and GraphQL\Type\Definition\InputObjectField; however, parameter $type of GraphQL\Type\Introspection::isIntrospectionType() does only seem to accept GraphQL\Type\Definition\Type, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

201
        if (! $error || Introspection::isIntrospectionType(/** @scrutinizer ignore-type */ $node)) {
Loading history...
202 79
            return;
203
        }
204
205 5
        $this->addError($error);
206 5
    }
207
208
    /**
209
     * @param string $argName
210
     *
211
     * @return InputValueDefinitionNode[]
212
     */
213
    private function getAllDirectiveArgNodes(Directive $directive, $argName)
214
    {
215
        $argNodes      = [];
216
        $directiveNode = $directive->astNode;
217
        if ($directiveNode && $directiveNode->arguments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $directiveNode->arguments of type GraphQL\Language\AST\ArgumentNode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
218
            foreach ($directiveNode->arguments as $node) {
219
                if ($node->name->value !== $argName) {
220
                    continue;
221
                }
222
223
                $argNodes[] = $node;
224
            }
225
        }
226
227
        return $argNodes;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $argNodes returns an array which contains values of type GraphQL\Language\AST\ArgumentNode which are incompatible with the documented value type GraphQL\Language\AST\InputValueDefinitionNode.
Loading history...
228
    }
229
230
    /**
231
     * @param string $argName
232
     *
233
     * @return TypeNode|null
234
     */
235
    private function getDirectiveArgTypeNode(Directive $directive, $argName)
236
    {
237
        $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
238
239
        return $argNode ? $argNode->type : null;
0 ignored issues
show
introduced by
$argNode is of type GraphQL\Language\AST\InputValueDefinitionNode, thus it always evaluated to true.
Loading history...
240
    }
241
242 79
    public function validateTypes()
243
    {
244 79
        $typeMap = $this->schema->getTypeMap();
245 79
        foreach ($typeMap as $typeName => $type) {
246
            // Ensure all provided types are in fact GraphQL type.
247 79
            if (! $type instanceof NamedType) {
248
                $this->reportError(
249
                    'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.',
250
                    is_object($type) ? $type->astNode : null
251
                );
252
                continue;
253
            }
254
255 79
            $this->validateName($type);
256
257 79
            if ($type instanceof ObjectType) {
258
                // Ensure fields are valid
259 79
                $this->validateFields($type);
260
261
                // Ensure objects implement the interfaces they claim to.
262 79
                $this->validateObjectInterfaces($type);
263 79
            } elseif ($type instanceof InterfaceType) {
264
                // Ensure fields are valid.
265 42
                $this->validateFields($type);
266
267
                // Ensure Interfaces include at least 1 Object type.
268 42
                $this->validateInterfaces($type);
269 79
            } elseif ($type instanceof UnionType) {
270
                // Ensure Unions include valid member types.
271 13
                $this->validateUnionMembers($type);
272 79
            } elseif ($type instanceof EnumType) {
273
                // Ensure Enums have valid values.
274 79
                $this->validateEnumValues($type);
275 79
            } elseif ($type instanceof InputObjectType) {
276
                // Ensure Input Object fields are valid.
277 79
                $this->validateInputFields($type);
278
            }
279
        }
280 79
    }
281
282
    /**
283
     * @param ObjectType|InterfaceType $type
284
     */
285 79
    private function validateFields($type)
286
    {
287 79
        $fieldMap = $type->getFields();
288
289
        // Objects and Interfaces both must define one or more fields.
290 79
        if (! $fieldMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldMap of type GraphQL\Type\Definition\FieldDefinition[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
291 1
            $this->reportError(
292 1
                sprintf('Type %s must define one or more fields.', $type->name),
293 1
                $this->getAllObjectOrInterfaceNodes($type)
294
            );
295
        }
296
297 79
        foreach ($fieldMap as $fieldName => $field) {
298
            // Ensure they are named correctly.
299 79
            $this->validateName($field);
300
301
            // Ensure they were defined at most once.
302 79
            $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
303 79
            if ($fieldNodes && count($fieldNodes) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldNodes of type GraphQL\Language\AST\FieldDefinitionNode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
304
                $this->reportError(
305
                    sprintf('Field %s.%s can only be defined once.', $type->name, $fieldName),
306
                    $fieldNodes
307
                );
308
                continue;
309
            }
310
311
            // Ensure the type is an output type
312 79
            if (! Type::isOutputType($field->getType())) {
313 6
                $this->reportError(
314 6
                    sprintf(
315 6
                        'The type of %s.%s must be Output Type but got: %s.',
316 6
                        $type->name,
317 6
                        $fieldName,
318 6
                        Utils::printSafe($field->getType())
319
                    ),
320 6
                    $this->getFieldTypeNode($type, $fieldName)
321
                );
322
            }
323
324
            // Ensure the arguments are valid
325 79
            $argNames = [];
326 79
            foreach ($field->args as $arg) {
327 79
                $argName = $arg->name;
328
329
                // Ensure they are named correctly.
330 79
                $this->validateName($arg);
331
332 79
                if (isset($argNames[$argName])) {
333
                    $this->reportError(
334
                        sprintf(
335
                            'Field argument %s.%s(%s:) can only be defined once.',
336
                            $type->name,
337
                            $fieldName,
338
                            $argName
339
                        ),
340
                        $this->getAllFieldArgNodes($type, $fieldName, $argName)
341
                    );
342
                }
343 79
                $argNames[$argName] = true;
344
345
                // Ensure the type is an input type
346 79
                if (Type::isInputType($arg->getType())) {
347 79
                    continue;
348
                }
349
350 3
                $this->reportError(
351 3
                    sprintf(
352 3
                        'The type of %s.%s(%s:) must be Input Type but got: %s.',
353 3
                        $type->name,
354 3
                        $fieldName,
355 3
                        $argName,
356 3
                        Utils::printSafe($arg->getType())
357
                    ),
358 79
                    $this->getFieldArgTypeNode($type, $fieldName, $argName)
359
                );
360
            }
361
        }
362 79
    }
363
364
    /**
365
     * @param ObjectType|InterfaceType $type
366
     *
367
     * @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
368
     */
369 79
    private function getAllObjectOrInterfaceNodes($type)
370
    {
371 79
        return $type->astNode
372 42
            ? ($type->extensionASTNodes
373
                ? array_merge([$type->astNode], $type->extensionASTNodes)
374 42
                : [$type->astNode])
375 79
            : ($type->extensionASTNodes ?: []);
376
    }
377
378
    /**
379
     * @param ObjectType|InterfaceType $type
380
     * @param string                   $fieldName
381
     *
382
     * @return FieldDefinitionNode[]
383
     */
384 79
    private function getAllFieldNodes($type, $fieldName)
385
    {
386 79
        $fieldNodes = [];
387 79
        $astNodes   = $this->getAllObjectOrInterfaceNodes($type);
388 79
        foreach ($astNodes as $astNode) {
389 43
            if (! $astNode || ! $astNode->fields) {
390
                continue;
391
            }
392
393 43
            foreach ($astNode->fields as $node) {
394 43
                if ($node->name->value !== $fieldName) {
395 2
                    continue;
396
                }
397
398 43
                $fieldNodes[] = $node;
399
            }
400
        }
401
402 79
        return $fieldNodes;
403
    }
404
405
    /**
406
     * @param ObjectType|InterfaceType $type
407
     * @param string                   $fieldName
408
     *
409
     * @return TypeNode|null
410
     */
411 12
    private function getFieldTypeNode($type, $fieldName)
412
    {
413 12
        $fieldNode = $this->getFieldNode($type, $fieldName);
414
415 12
        return $fieldNode ? $fieldNode->type : null;
416
    }
417
418
    /**
419
     * @param ObjectType|InterfaceType $type
420
     * @param string                   $fieldName
421
     *
422
     * @return FieldDefinitionNode|null
423
     */
424 20
    private function getFieldNode($type, $fieldName)
425
    {
426 20
        $nodes = $this->getAllFieldNodes($type, $fieldName);
427
428 20
        return $nodes[0] ?? null;
429
    }
430
431
    /**
432
     * @param ObjectType|InterfaceType $type
433
     * @param string                   $fieldName
434
     * @param string                   $argName
435
     *
436
     * @return InputValueDefinitionNode[]
437
     */
438 7
    private function getAllFieldArgNodes($type, $fieldName, $argName)
439
    {
440 7
        $argNodes  = [];
441 7
        $fieldNode = $this->getFieldNode($type, $fieldName);
442 7
        if ($fieldNode && $fieldNode->arguments) {
443 5
            foreach ($fieldNode->arguments as $node) {
444 5
                if ($node->name->value !== $argName) {
445 1
                    continue;
446
                }
447
448 5
                $argNodes[] = $node;
449
            }
450
        }
451
452 7
        return $argNodes;
453
    }
454
455
    /**
456
     * @param ObjectType|InterfaceType $type
457
     * @param string                   $fieldName
458
     * @param string                   $argName
459
     *
460
     * @return TypeNode|null
461
     */
462 6
    private function getFieldArgTypeNode($type, $fieldName, $argName)
463
    {
464 6
        $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
465
466 6
        return $fieldArgNode ? $fieldArgNode->type : null;
467
    }
468
469
    /**
470
     * @param ObjectType|InterfaceType $type
471
     * @param string                   $fieldName
472
     * @param string                   $argName
473
     *
474
     * @return InputValueDefinitionNode|null
475
     */
476 7
    private function getFieldArgNode($type, $fieldName, $argName)
477
    {
478 7
        $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
479
480 7
        return $nodes[0] ?? null;
481
    }
482
483 79
    private function validateObjectInterfaces(ObjectType $object)
484
    {
485 79
        $implementedTypeNames = [];
486 79
        foreach ($object->getInterfaces() as $iface) {
487 42
            if (! $iface instanceof InterfaceType) {
488 2
                $this->reportError(
489 2
                    sprintf(
490 2
                        'Type %s must only implement Interface types, it cannot implement %s.',
491 2
                        $object->name,
492 2
                        Utils::printSafe($iface)
493
                    ),
494 2
                    $this->getImplementsInterfaceNode($object, $iface)
495
                );
496 2
                continue;
497
            }
498 40
            if (isset($implementedTypeNames[$iface->name])) {
499 1
                $this->reportError(
500 1
                    sprintf('Type %s can only implement %s once.', $object->name, $iface->name),
501 1
                    $this->getAllImplementsInterfaceNodes($object, $iface)
502
                );
503 1
                continue;
504
            }
505 40
            $implementedTypeNames[$iface->name] = true;
506 40
            $this->validateObjectImplementsInterface($object, $iface);
507
        }
508 79
    }
509
510 42
    private function validateInterfaces(InterfaceType $iface)
511
    {
512 42
        $possibleTypes = $this->schema->getPossibleTypes($iface);
513
514 42
        if (count($possibleTypes) !== 0) {
515 40
            return;
516
        }
517
518 4
        $this->reportError(
519 4
            sprintf(
520 4
                'Interface %s must be implemented by at least one Object type.',
521 4
                $iface->name
522
            ),
523 4
            $iface->astNode
524
        );
525 4
    }
526
527
    /**
528
     * @param InterfaceType $iface
529
     *
530
     * @return NamedTypeNode|null
531
     */
532 2
    private function getImplementsInterfaceNode(ObjectType $type, $iface)
533
    {
534 2
        $nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
535
536 2
        return $nodes[0] ?? null;
537
    }
538
539
    /**
540
     * @param InterfaceType $iface
541
     *
542
     * @return NamedTypeNode[]
543
     */
544 3
    private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
545
    {
546 3
        $implementsNodes = [];
547 3
        $astNodes        = $this->getAllObjectOrInterfaceNodes($type);
548
549 3
        foreach ($astNodes as $astNode) {
550 2
            if (! $astNode || ! $astNode->interfaces) {
551
                continue;
552
            }
553
554 2
            foreach ($astNode->interfaces as $node) {
555 2
                if ($node->name->value !== $iface->name) {
556
                    continue;
557
                }
558
559 2
                $implementsNodes[] = $node;
560
            }
561
        }
562
563 3
        return $implementsNodes;
564
    }
565
566
    /**
567
     * @param InterfaceType $iface
568
     */
569 40
    private function validateObjectImplementsInterface(ObjectType $object, $iface)
570
    {
571 40
        $objectFieldMap = $object->getFields();
572 40
        $ifaceFieldMap  = $iface->getFields();
573
574
        // Assert each interface field is implemented.
575 40
        foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
576 40
            $objectField = array_key_exists($fieldName, $objectFieldMap)
577 39
                ? $objectFieldMap[$fieldName]
578 40
                : null;
579
580
            // Assert interface field exists on object.
581 40
            if (! $objectField) {
582 2
                $this->reportError(
583 2
                    sprintf(
584 2
                        'Interface field %s.%s expected but %s does not provide it.',
585 2
                        $iface->name,
586 2
                        $fieldName,
587 2
                        $object->name
588
                    ),
589 2
                    [$this->getFieldNode($iface, $fieldName), $object->astNode]
590
                );
591 2
                continue;
592
            }
593
594
            // Assert interface field type is satisfied by object field type, by being
595
            // a valid subtype. (covariant)
596 39
            if (! TypeComparators::isTypeSubTypeOf(
597 39
                $this->schema,
598 39
                $objectField->getType(),
0 ignored issues
show
Bug introduced by
$objectField->getType() of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\AbstractType expected by parameter $maybeSubType of GraphQL\Utils\TypeComparators::isTypeSubTypeOf(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

598
                /** @scrutinizer ignore-type */ $objectField->getType(),
Loading history...
599 39
                $ifaceField->getType()
0 ignored issues
show
Bug introduced by
$ifaceField->getType() of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\AbstractType expected by parameter $superType of GraphQL\Utils\TypeComparators::isTypeSubTypeOf(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

599
                /** @scrutinizer ignore-type */ $ifaceField->getType()
Loading history...
600
            )
601
            ) {
602 6
                $this->reportError(
603 6
                    sprintf(
604 6
                        'Interface field %s.%s expects type %s but %s.%s is type %s.',
605 6
                        $iface->name,
606 6
                        $fieldName,
607 6
                        $ifaceField->getType(),
608 6
                        $object->name,
609 6
                        $fieldName,
610 6
                        Utils::printSafe($objectField->getType())
611
                    ),
612
                    [
613 6
                        $this->getFieldTypeNode($iface, $fieldName),
614 6
                        $this->getFieldTypeNode($object, $fieldName),
615
                    ]
616
                );
617
            }
618
619
            // Assert each interface field arg is implemented.
620 39
            foreach ($ifaceField->args as $ifaceArg) {
621 8
                $argName   = $ifaceArg->name;
622 8
                $objectArg = null;
623
624 8
                foreach ($objectField->args as $arg) {
625 7
                    if ($arg->name === $argName) {
626 7
                        $objectArg = $arg;
627 7
                        break;
628
                    }
629
                }
630
631
                // Assert interface field arg exists on object field.
632 8
                if (! $objectArg) {
633 1
                    $this->reportError(
634 1
                        sprintf(
635 1
                            'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.',
636 1
                            $iface->name,
637 1
                            $fieldName,
638 1
                            $argName,
639 1
                            $object->name,
640 1
                            $fieldName
641
                        ),
642
                        [
643 1
                            $this->getFieldArgNode($iface, $fieldName, $argName),
644 1
                            $this->getFieldNode($object, $fieldName),
645
                        ]
646
                    );
647 1
                    continue;
648
                }
649
650
                // Assert interface field arg type matches object field arg type.
651
                // (invariant)
652
                // TODO: change to contravariant?
653 7
                if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
654 2
                    $this->reportError(
655 2
                        sprintf(
656 2
                            'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.',
657 2
                            $iface->name,
658 2
                            $fieldName,
659 2
                            $argName,
660 2
                            Utils::printSafe($ifaceArg->getType()),
661 2
                            $object->name,
662 2
                            $fieldName,
663 2
                            $argName,
664 2
                            Utils::printSafe($objectArg->getType())
665
                        ),
666
                        [
667 2
                            $this->getFieldArgTypeNode($iface, $fieldName, $argName),
668 7
                            $this->getFieldArgTypeNode($object, $fieldName, $argName),
669
                        ]
670
                    );
671
                }
672
                // TODO: validate default values?
673
            }
674
675
            // Assert additional arguments must not be required.
676 39
            foreach ($objectField->args as $objectArg) {
677 7
                $argName  = $objectArg->name;
678 7
                $ifaceArg = null;
679
680 7
                foreach ($ifaceField->args as $arg) {
681 7
                    if ($arg->name === $argName) {
682 7
                        $ifaceArg = $arg;
683 7
                        break;
684
                    }
685
                }
686
687 7
                if ($ifaceArg || ! ($objectArg->getType() instanceof NonNull)) {
688 7
                    continue;
689
                }
690
691 1
                $this->reportError(
692 1
                    sprintf(
693 1
                        'Object field argument %s.%s(%s:) is of required type %s but is not also provided by the Interface field %s.%s.',
694 1
                        $object->name,
695 1
                        $fieldName,
696 1
                        $argName,
697 1
                        Utils::printSafe($objectArg->getType()),
698 1
                        $iface->name,
699 1
                        $fieldName
700
                    ),
701
                    [
702 1
                        $this->getFieldArgTypeNode($object, $fieldName, $argName),
703 39
                        $this->getFieldNode($iface, $fieldName),
704
                    ]
705
                );
706
            }
707
        }
708 40
    }
709
710 13
    private function validateUnionMembers(UnionType $union)
711
    {
712 13
        $memberTypes = $union->getTypes();
713
714 13
        if (! $memberTypes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $memberTypes of type GraphQL\Type\Definition\ObjectType[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
715 1
            $this->reportError(
716 1
                sprintf('Union type %s must define one or more member types.', $union->name),
717 1
                $union->astNode
718
            );
719
        }
720
721 13
        $includedTypeNames = [];
722
723 13
        foreach ($memberTypes as $memberType) {
724 12
            if (isset($includedTypeNames[$memberType->name])) {
725 1
                $this->reportError(
726 1
                    sprintf('Union type %s can only include type %s once.', $union->name, $memberType->name),
727 1
                    $this->getUnionMemberTypeNodes($union, $memberType->name)
728
                );
729 1
                continue;
730
            }
731 12
            $includedTypeNames[$memberType->name] = true;
732 12
            if ($memberType instanceof ObjectType) {
733 12
                continue;
734
            }
735
736 2
            $this->reportError(
737 2
                sprintf(
738 2
                    'Union type %s can only include Object types, it cannot include %s.',
739 2
                    $union->name,
740 2
                    Utils::printSafe($memberType)
741
                ),
742 2
                $this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
743
            );
744
        }
745 13
    }
746
747
    /**
748
     * @param string $typeName
749
     *
750
     * @return NamedTypeNode[]
751
     */
752 3
    private function getUnionMemberTypeNodes(UnionType $union, $typeName)
753
    {
754 3
        if ($union->astNode && $union->astNode->types) {
755 2
            return array_filter(
756 2
                $union->astNode->types,
757
                static function (NamedTypeNode $value) use ($typeName) {
758 2
                    return $value->name->value === $typeName;
759 2
                }
760
            );
761
        }
762
763 2
        return $union->astNode ?
764 2
            $union->astNode->types : null;
765
    }
766
767 79
    private function validateEnumValues(EnumType $enumType)
768
    {
769 79
        $enumValues = $enumType->getValues();
770
771 79
        if (! $enumValues) {
0 ignored issues
show
introduced by
$enumValues is a non-empty array, thus ! $enumValues is always false.
Loading history...
Bug Best Practice introduced by
The expression $enumValues of type GraphQL\Type\Definition\EnumValueDefinition[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
772 1
            $this->reportError(
773 1
                sprintf('Enum type %s must define one or more values.', $enumType->name),
774 1
                $enumType->astNode
775
            );
776
        }
777
778 79
        foreach ($enumValues as $enumValue) {
779 79
            $valueName = $enumValue->name;
780
781
            // Ensure no duplicates
782 79
            $allNodes = $this->getEnumValueNodes($enumType, $valueName);
783 79
            if ($allNodes && count($allNodes) > 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $allNodes of type GraphQL\Language\AST\EnumValueDefinitionNode[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
784 1
                $this->reportError(
785 1
                    sprintf('Enum type %s can include value %s only once.', $enumType->name, $valueName),
786 1
                    $allNodes
787
                );
788
            }
789
790
            // Ensure valid name.
791 79
            $this->validateName($enumValue);
792 79
            if ($valueName !== 'true' && $valueName !== 'false' && $valueName !== 'null') {
793 79
                continue;
794
            }
795
796 3
            $this->reportError(
797 3
                sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName),
798 3
                $enumValue->astNode
799
            );
800
        }
801 79
    }
802
803
    /**
804
     * @param string $valueName
805
     *
806
     * @return EnumValueDefinitionNode[]
807
     */
808 79
    private function getEnumValueNodes(EnumType $enum, $valueName)
809
    {
810 79
        if ($enum->astNode && $enum->astNode->values) {
811 1
            return array_filter(
812 1
                iterator_to_array($enum->astNode->values),
0 ignored issues
show
Bug introduced by
It seems like $enum->astNode->values can also be of type GraphQL\Language\AST\EnumValueDefinitionNode[]; however, parameter $iterator of iterator_to_array() does only seem to accept Traversable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

812
                iterator_to_array(/** @scrutinizer ignore-type */ $enum->astNode->values),
Loading history...
813
                static function (EnumValueDefinitionNode $value) use ($valueName) {
814 1
                    return $value->name->value === $valueName;
815 1
                }
816
            );
817
        }
818
819 79
        return $enum->astNode ?
0 ignored issues
show
Bug Best Practice introduced by
The expression return $enum->astNode ? ...>astNode->values : null also could return the type GraphQL\Language\AST\NodeList which is incompatible with the documented return type GraphQL\Language\AST\EnumValueDefinitionNode[].
Loading history...
820 79
            $enum->astNode->values : null;
821
    }
822
823 19
    private function validateInputFields(InputObjectType $inputObj)
824
    {
825 19
        $fieldMap = $inputObj->getFields();
826
827 19
        if (! $fieldMap) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldMap of type GraphQL\Type\Definition\InputObjectField[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
828 1
            $this->reportError(
829 1
                sprintf('Input Object type %s must define one or more fields.', $inputObj->name),
830 1
                $inputObj->astNode
831
            );
832
        }
833
834
        // Ensure the arguments are valid
835 19
        foreach ($fieldMap as $fieldName => $field) {
836
            // Ensure they are named correctly.
837 18
            $this->validateName($field);
838
839
            // TODO: Ensure they are unique per field.
840
841
            // Ensure the type is an input type
842 18
            if (Type::isInputType($field->getType())) {
843 15
                continue;
844
            }
845
846 4
            $this->reportError(
847 4
                sprintf(
848 4
                    'The type of %s.%s must be Input Type but got: %s.',
849 4
                    $inputObj->name,
850 4
                    $fieldName,
851 4
                    Utils::printSafe($field->getType())
852
                ),
853 4
                $field->astNode ? $field->astNode->type : null
854
            );
855
        }
856 19
    }
857
}
858