Failed Conditions
Push — master ( a4f39b...12ee90 )
by Vladimir
11:17
created

SchemaValidationContext::validateInputFields()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 39
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6

Importance

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

243
        if (! $error || Introspection::isIntrospectionType(/** @scrutinizer ignore-type */ $node)) {
Loading history...
244 95
            return;
245
        }
246
247 5
        $this->addError($error);
248 5
    }
249
250
    /**
251
     * @param string $argName
252
     *
253
     * @return InputValueDefinitionNode[]
254
     */
255
    private function getAllDirectiveArgNodes(Directive $directive, $argName)
256
    {
257
        $subNodes = $this->getAllSubNodes(
258
            $directive,
259
            static function ($directiveNode) {
260
                return $directiveNode->arguments;
261
            }
262
        );
263
264
        return Utils::filter(
265
            $subNodes,
266
            static function ($argNode) use ($argName) {
267
                return $argNode->name->value === $argName;
268
            }
269
        );
270
    }
271
272
    /**
273
     * @param string $argName
274
     *
275
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
276
     */
277
    private function getDirectiveArgTypeNode(Directive $directive, $argName) : ?TypeNode
278
    {
279
        $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0];
280
281
        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...
282
    }
283
284 95
    public function validateTypes() : void
285
    {
286 95
        $typeMap = $this->schema->getTypeMap();
287 95
        foreach ($typeMap as $typeName => $type) {
288
            // Ensure all provided types are in fact GraphQL type.
289 95
            if (! $type instanceof NamedType) {
290 4
                $this->reportError(
291 4
                    'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.',
292 4
                    $type instanceof Type ? $type->astNode : null
293
                );
294 4
                continue;
295
            }
296
297 95
            $this->validateName($type);
298
299 95
            if ($type instanceof ObjectType) {
300
                // Ensure fields are valid
301 95
                $this->validateFields($type);
302
303
                // Ensure objects implement the interfaces they claim to.
304 95
                $this->validateObjectInterfaces($type);
305
306
                // Ensure directives are valid
307 95
                $this->validateDirectivesAtLocation(
308 95
                    $this->getDirectives($type),
309 95
                    DirectiveLocation::OBJECT
310
                );
311 95
            } elseif ($type instanceof InterfaceType) {
312
                // Ensure fields are valid.
313 45
                $this->validateFields($type);
314
315
                // Ensure directives are valid
316 45
                $this->validateDirectivesAtLocation(
317 45
                    $this->getDirectives($type),
318 45
                    DirectiveLocation::IFACE
319
                );
320 95
            } elseif ($type instanceof UnionType) {
321
                // Ensure Unions include valid member types.
322 15
                $this->validateUnionMembers($type);
323
324
                // Ensure directives are valid
325 15
                $this->validateDirectivesAtLocation(
326 15
                    $this->getDirectives($type),
327 15
                    DirectiveLocation::UNION
328
                );
329 95
            } elseif ($type instanceof EnumType) {
330
                // Ensure Enums have valid values.
331 95
                $this->validateEnumValues($type);
332
333
                // Ensure directives are valid
334 95
                $this->validateDirectivesAtLocation(
335 95
                    $this->getDirectives($type),
336 95
                    DirectiveLocation::ENUM
337
                );
338 95
            } elseif ($type instanceof InputObjectType) {
339
                // Ensure Input Object fields are valid.
340 26
                $this->validateInputFields($type);
341
342
                // Ensure directives are valid
343 26
                $this->validateDirectivesAtLocation(
344 26
                    $this->getDirectives($type),
345 26
                    DirectiveLocation::INPUT_OBJECT
346
                );
347
348
                // Ensure Input Objects do not contain non-nullable circular references
349 26
                $this->inputObjectCircularRefs->validate($type);
350 95
            } elseif ($type instanceof ScalarType) {
351
                // Ensure directives are valid
352 95
                $this->validateDirectivesAtLocation(
353 95
                    $this->getDirectives($type),
354 95
                    DirectiveLocation::SCALAR
355
                );
356
            }
357
        }
358 95
    }
359
360
    /**
361
     * @param NodeList<DirectiveNode> $directives
362
     */
363 95
    private function validateDirectivesAtLocation($directives, string $location)
364
    {
365 95
        $directivesNamed = [];
366 95
        $schema          = $this->schema;
367 95
        foreach ($directives as $directive) {
368 5
            $directiveName = $directive->name->value;
369
370
            // Ensure directive used is also defined
371 5
            $schemaDirective = $schema->getDirective($directiveName);
372 5
            if ($schemaDirective === null) {
373
                $this->reportError(
374
                    sprintf('No directive @%s defined.', $directiveName),
375
                    $directive
376
                );
377
                continue;
378
            }
379 5
            $includes = Utils::some(
380 5
                $schemaDirective->locations,
381
                static function ($schemaLocation) use ($location) {
382 5
                    return $schemaLocation === $location;
383 5
                }
384
            );
385 5
            if (! $includes) {
386
                $errorNodes = $schemaDirective->astNode
387
                    ? [$directive, $schemaDirective->astNode]
388
                    : [$directive];
389
                $this->reportError(
390
                    sprintf('Directive @%s not allowed at %s location.', $directiveName, $location),
391
                    $errorNodes
392
                );
393
            }
394
395 5
            $existingNodes                   = $directivesNamed[$directiveName] ?? [];
396 5
            $existingNodes[]                 = $directive;
397 5
            $directivesNamed[$directiveName] = $existingNodes;
398
        }
399 95
        foreach ($directivesNamed as $directiveName => $directiveList) {
400 5
            if (count($directiveList) <= 1) {
401 4
                continue;
402
            }
403
404 1
            $this->reportError(
405 1
                sprintf('Directive @%s used twice at the same location.', $directiveName),
406 1
                $directiveList
407
            );
408
        }
409 95
    }
410
411
    /**
412
     * @param ObjectType|InterfaceType $type
413
     */
414 95
    private function validateFields($type)
415
    {
416 95
        $fieldMap = $type->getFields();
417
418
        // Objects and Interfaces both must define one or more fields.
419 95
        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...
420 1
            $this->reportError(
421 1
                sprintf('Type %s must define one or more fields.', $type->name),
422 1
                $this->getAllNodes($type)
423
            );
424
        }
425
426 95
        foreach ($fieldMap as $fieldName => $field) {
427
            // Ensure they are named correctly.
428 95
            $this->validateName($field);
429
430
            // Ensure they were defined at most once.
431 95
            $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
432 95
            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...
433
                $this->reportError(
434
                    sprintf('Field %s.%s can only be defined once.', $type->name, $fieldName),
435
                    $fieldNodes
436
                );
437
                continue;
438
            }
439
440
            // Ensure the type is an output type
441 95
            if (! Type::isOutputType($field->getType())) {
442 8
                $this->reportError(
443 8
                    sprintf(
444 8
                        'The type of %s.%s must be Output Type but got: %s.',
445 8
                        $type->name,
446 8
                        $fieldName,
447 8
                        Utils::printSafe($field->getType())
448
                    ),
449 8
                    $this->getFieldTypeNode($type, $fieldName)
450
                );
451
            }
452
453
            // Ensure the arguments are valid
454 95
            $argNames = [];
455 95
            foreach ($field->args as $arg) {
456 95
                $argName = $arg->name;
457
458
                // Ensure they are named correctly.
459 95
                $this->validateName($arg);
460
461 95
                if (isset($argNames[$argName])) {
462
                    $this->reportError(
463
                        sprintf(
464
                            'Field argument %s.%s(%s:) can only be defined once.',
465
                            $type->name,
466
                            $fieldName,
467
                            $argName
468
                        ),
469
                        $this->getAllFieldArgNodes($type, $fieldName, $argName)
470
                    );
471
                }
472 95
                $argNames[$argName] = true;
473
474
                // Ensure the type is an input type
475 95
                if (! Type::isInputType($arg->getType())) {
476 4
                    $this->reportError(
477 4
                        sprintf(
478 4
                            'The type of %s.%s(%s:) must be Input Type but got: %s.',
479 4
                            $type->name,
480 4
                            $fieldName,
481 4
                            $argName,
482 4
                            Utils::printSafe($arg->getType())
483
                        ),
484 4
                        $this->getFieldArgTypeNode($type, $fieldName, $argName)
485
                    );
486
                }
487
488
                // Ensure argument definition directives are valid
489 95
                if (! isset($arg->astNode, $arg->astNode->directives)) {
490 95
                    continue;
491
                }
492
493 20
                $this->validateDirectivesAtLocation(
494 20
                    $arg->astNode->directives,
0 ignored issues
show
Bug introduced by
$arg->astNode->directives of type GraphQL\Language\AST\DirectiveNode[] is incompatible with the type GraphQL\Language\AST\NodeList expected by parameter $directives of GraphQL\Type\SchemaValid...eDirectivesAtLocation(). ( Ignorable by Annotation )

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

494
                    /** @scrutinizer ignore-type */ $arg->astNode->directives,
Loading history...
495 20
                    DirectiveLocation::ARGUMENT_DEFINITION
496
                );
497
            }
498
499
            // Ensure any directives are valid
500 95
            if (! isset($field->astNode, $field->astNode->directives)) {
501 95
                continue;
502
            }
503
504 54
            $this->validateDirectivesAtLocation(
505 54
                $field->astNode->directives,
506 54
                DirectiveLocation::FIELD_DEFINITION
507
            );
508
        }
509 95
    }
510
511
    /**
512
     * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj
513
     *
514
     * @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
515
     */
516 95
    private function getAllNodes($obj)
517
    {
518 95
        if ($obj instanceof Schema) {
519 95
            $astNode        = $obj->getAstNode();
520 95
            $extensionNodes = $obj->extensionASTNodes;
521
        } else {
522 95
            $astNode        = $obj->astNode;
0 ignored issues
show
Documentation Bug introduced by
It seems like $obj->astNode can also be of type GraphQL\Language\AST\EnumTypeDefinitionNode or GraphQL\Language\AST\InputObjectTypeDefinitionNode or GraphQL\Language\AST\InterfaceTypeDefinitionNode or GraphQL\Language\AST\ObjectTypeDefinitionNode or GraphQL\Language\AST\UnionTypeDefinitionNode. However, the property $astNode is declared as type GraphQL\Language\AST\DirectiveDefinitionNode|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
523 95
            $extensionNodes = $obj->extensionASTNodes;
0 ignored issues
show
Bug introduced by
The property extensionASTNodes does not seem to exist on GraphQL\Type\Definition\Directive.
Loading history...
524
        }
525
526 95
        return $astNode
527 55
            ? ($extensionNodes
528 9
                ? array_merge([$astNode], $extensionNodes)
529 55
                : [$astNode])
530 95
            : ($extensionNodes ?: []);
531
    }
532
533
    /**
534
     * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|Directive $obj
535
     *
536
     * @return NodeList
537
     */
538 95
    private function getAllSubNodes($obj, callable $getter)
539
    {
540 95
        $result = new NodeList([]);
541 95
        foreach ($this->getAllNodes($obj) as $astNode) {
542 57
            if (! $astNode) {
543
                continue;
544
            }
545
546 57
            $subNodes = $getter($astNode);
547 57
            if (! $subNodes) {
548
                continue;
549
            }
550
551 57
            $result = $result->merge($subNodes);
552
        }
553
554 95
        return $result;
555
    }
556
557
    /**
558
     * @param ObjectType|InterfaceType $type
559
     * @param string                   $fieldName
560
     *
561
     * @return FieldDefinitionNode[]
562
     */
563 95
    private function getAllFieldNodes($type, $fieldName)
564
    {
565
        $subNodes = $this->getAllSubNodes($type, static function ($typeNode) {
566 54
            return $typeNode->fields;
567 95
        });
568
569
        return Utils::filter($subNodes, static function ($fieldNode) use ($fieldName) {
570 54
            return $fieldNode->name->value === $fieldName;
571 95
        });
572
    }
573
574
    /**
575
     * @param ObjectType|InterfaceType $type
576
     * @param string                   $fieldName
577
     *
578
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
579
     */
580 15
    private function getFieldTypeNode($type, $fieldName) : ?TypeNode
581
    {
582 15
        $fieldNode = $this->getFieldNode($type, $fieldName);
583
584 15
        return $fieldNode ? $fieldNode->type : null;
585
    }
586
587
    /**
588
     * @param ObjectType|InterfaceType $type
589
     * @param string                   $fieldName
590
     *
591
     * @return FieldDefinitionNode|null
592
     */
593 26
    private function getFieldNode($type, $fieldName)
594
    {
595 26
        $nodes = $this->getAllFieldNodes($type, $fieldName);
596
597 26
        return $nodes[0] ?? null;
598
    }
599
600
    /**
601
     * @param ObjectType|InterfaceType $type
602
     * @param string                   $fieldName
603
     * @param string                   $argName
604
     *
605
     * @return InputValueDefinitionNode[]
606
     */
607 9
    private function getAllFieldArgNodes($type, $fieldName, $argName)
608
    {
609 9
        $argNodes  = [];
610 9
        $fieldNode = $this->getFieldNode($type, $fieldName);
611 9
        if ($fieldNode && $fieldNode->arguments) {
612 6
            foreach ($fieldNode->arguments as $node) {
613 6
                if ($node->name->value !== $argName) {
614 1
                    continue;
615
                }
616
617 6
                $argNodes[] = $node;
618
            }
619
        }
620
621 9
        return $argNodes;
622
    }
623
624
    /**
625
     * @param ObjectType|InterfaceType $type
626
     * @param string                   $fieldName
627
     * @param string                   $argName
628
     *
629
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
630
     */
631 7
    private function getFieldArgTypeNode($type, $fieldName, $argName) : ?TypeNode
632
    {
633 7
        $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
634
635 7
        return $fieldArgNode ? $fieldArgNode->type : null;
636
    }
637
638
    /**
639
     * @param ObjectType|InterfaceType $type
640
     * @param string                   $fieldName
641
     * @param string                   $argName
642
     *
643
     * @return InputValueDefinitionNode|null
644
     */
645 9
    private function getFieldArgNode($type, $fieldName, $argName)
646
    {
647 9
        $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
648
649 9
        return $nodes[0] ?? null;
650
    }
651
652 95
    private function validateObjectInterfaces(ObjectType $object)
653
    {
654 95
        $implementedTypeNames = [];
655 95
        foreach ($object->getInterfaces() as $iface) {
656 46
            if (! $iface instanceof InterfaceType) {
657 2
                $this->reportError(
658 2
                    sprintf(
659 2
                        'Type %s must only implement Interface types, it cannot implement %s.',
660 2
                        $object->name,
661 2
                        Utils::printSafe($iface)
662
                    ),
663 2
                    $this->getImplementsInterfaceNode($object, $iface)
664
                );
665 2
                continue;
666
            }
667 44
            if (isset($implementedTypeNames[$iface->name])) {
668 1
                $this->reportError(
669 1
                    sprintf('Type %s can only implement %s once.', $object->name, $iface->name),
670 1
                    $this->getAllImplementsInterfaceNodes($object, $iface)
671
                );
672 1
                continue;
673
            }
674 44
            $implementedTypeNames[$iface->name] = true;
675 44
            $this->validateObjectImplementsInterface($object, $iface);
676
        }
677 95
    }
678
679
    /**
680
     * @param Schema|Type $object
681
     *
682
     * @return NodeList<DirectiveNode>
683
     */
684 95
    private function getDirectives($object)
685
    {
686
        return $this->getAllSubNodes($object, static function ($node) {
687 57
            return $node->directives;
688 95
        });
689
    }
690
691
    /**
692
     * @param InterfaceType $iface
693
     *
694
     * @return NamedTypeNode|null
695
     */
696 2
    private function getImplementsInterfaceNode(ObjectType $type, $iface)
697
    {
698 2
        $nodes = $this->getAllImplementsInterfaceNodes($type, $iface);
699
700 2
        return $nodes[0] ?? null;
701
    }
702
703
    /**
704
     * @param InterfaceType $iface
705
     *
706
     * @return NamedTypeNode[]
707
     */
708 3
    private function getAllImplementsInterfaceNodes(ObjectType $type, $iface)
709
    {
710
        $subNodes = $this->getAllSubNodes($type, static function ($typeNode) {
711 2
            return $typeNode->interfaces;
712 3
        });
713
714
        return Utils::filter($subNodes, static function ($ifaceNode) use ($iface) {
715 2
            return $ifaceNode->name->value === $iface->name;
716 3
        });
717
    }
718
719
    /**
720
     * @param InterfaceType $iface
721
     */
722 44
    private function validateObjectImplementsInterface(ObjectType $object, $iface)
723
    {
724 44
        $objectFieldMap = $object->getFields();
725 44
        $ifaceFieldMap  = $iface->getFields();
726
727
        // Assert each interface field is implemented.
728 44
        foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
729 44
            $objectField = array_key_exists($fieldName, $objectFieldMap)
730 43
                ? $objectFieldMap[$fieldName]
731 44
                : null;
732
733
            // Assert interface field exists on object.
734 44
            if (! $objectField) {
735 3
                $this->reportError(
736 3
                    sprintf(
737 3
                        'Interface field %s.%s expected but %s does not provide it.',
738 3
                        $iface->name,
739 3
                        $fieldName,
740 3
                        $object->name
741
                    ),
742 3
                    array_merge(
743 3
                        [$this->getFieldNode($iface, $fieldName)],
744 3
                        $this->getAllNodes($object)
745
                    )
746
                );
747 3
                continue;
748
            }
749
750
            // Assert interface field type is satisfied by object field type, by being
751
            // a valid subtype. (covariant)
752 43
            if (! TypeComparators::isTypeSubTypeOf(
753 43
                $this->schema,
754 43
                $objectField->getType(),
755 43
                $ifaceField->getType()
756
            )
757
            ) {
758 7
                $this->reportError(
759 7
                    sprintf(
760 7
                        'Interface field %s.%s expects type %s but %s.%s is type %s.',
761 7
                        $iface->name,
762 7
                        $fieldName,
763 7
                        $ifaceField->getType(),
764 7
                        $object->name,
765 7
                        $fieldName,
766 7
                        Utils::printSafe($objectField->getType())
767
                    ),
768
                    [
769 7
                        $this->getFieldTypeNode($iface, $fieldName),
770 7
                        $this->getFieldTypeNode($object, $fieldName),
771
                    ]
772
                );
773
            }
774
775
            // Assert each interface field arg is implemented.
776 43
            foreach ($ifaceField->args as $ifaceArg) {
777 9
                $argName   = $ifaceArg->name;
778 9
                $objectArg = null;
779
780 9
                foreach ($objectField->args as $arg) {
781 7
                    if ($arg->name === $argName) {
782 7
                        $objectArg = $arg;
783 7
                        break;
784
                    }
785
                }
786
787
                // Assert interface field arg exists on object field.
788 9
                if (! $objectArg) {
789 2
                    $this->reportError(
790 2
                        sprintf(
791 2
                            'Interface field argument %s.%s(%s:) expected but %s.%s does not provide it.',
792 2
                            $iface->name,
793 2
                            $fieldName,
794 2
                            $argName,
795 2
                            $object->name,
796 2
                            $fieldName
797
                        ),
798
                        [
799 2
                            $this->getFieldArgNode($iface, $fieldName, $argName),
800 2
                            $this->getFieldNode($object, $fieldName),
801
                        ]
802
                    );
803 2
                    continue;
804
                }
805
806
                // Assert interface field arg type matches object field arg type.
807
                // (invariant)
808
                // TODO: change to contravariant?
809 7
                if (! TypeComparators::isEqualType($ifaceArg->getType(), $objectArg->getType())) {
810 2
                    $this->reportError(
811 2
                        sprintf(
812 2
                            'Interface field argument %s.%s(%s:) expects type %s but %s.%s(%s:) is type %s.',
813 2
                            $iface->name,
814 2
                            $fieldName,
815 2
                            $argName,
816 2
                            Utils::printSafe($ifaceArg->getType()),
817 2
                            $object->name,
818 2
                            $fieldName,
819 2
                            $argName,
820 2
                            Utils::printSafe($objectArg->getType())
821
                        ),
822
                        [
823 2
                            $this->getFieldArgTypeNode($iface, $fieldName, $argName),
824 7
                            $this->getFieldArgTypeNode($object, $fieldName, $argName),
825
                        ]
826
                    );
827
                }
828
                // TODO: validate default values?
829
            }
830
831
            // Assert additional arguments must not be required.
832 43
            foreach ($objectField->args as $objectArg) {
833 8
                $argName  = $objectArg->name;
834 8
                $ifaceArg = null;
835
836 8
                foreach ($ifaceField->args as $arg) {
837 7
                    if ($arg->name === $argName) {
838 7
                        $ifaceArg = $arg;
839 7
                        break;
840
                    }
841
                }
842
843 8
                if ($ifaceArg || ! ($objectArg->getType() instanceof NonNull)) {
844 8
                    continue;
845
                }
846
847 1
                $this->reportError(
848 1
                    sprintf(
849 1
                        'Object field argument %s.%s(%s:) is of required type %s but is not also provided by the Interface field %s.%s.',
850 1
                        $object->name,
851 1
                        $fieldName,
852 1
                        $argName,
853 1
                        Utils::printSafe($objectArg->getType()),
854 1
                        $iface->name,
855 1
                        $fieldName
856
                    ),
857
                    [
858 1
                        $this->getFieldArgTypeNode($object, $fieldName, $argName),
859 43
                        $this->getFieldNode($iface, $fieldName),
860
                    ]
861
                );
862
            }
863
        }
864 44
    }
865
866 15
    private function validateUnionMembers(UnionType $union)
867
    {
868 15
        $memberTypes = $union->getTypes();
869
870 15
        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...
871 1
            $this->reportError(
872 1
                sprintf('Union type %s must define one or more member types.', $union->name),
873 1
                $this->getAllNodes($union)
874
            );
875
        }
876
877 15
        $includedTypeNames = [];
878
879 15
        foreach ($memberTypes as $memberType) {
880 14
            if (isset($includedTypeNames[$memberType->name])) {
881 1
                $this->reportError(
882 1
                    sprintf('Union type %s can only include type %s once.', $union->name, $memberType->name),
883 1
                    $this->getUnionMemberTypeNodes($union, $memberType->name)
884
                );
885 1
                continue;
886
            }
887 14
            $includedTypeNames[$memberType->name] = true;
888 14
            if ($memberType instanceof ObjectType) {
889 13
                continue;
890
            }
891
892 3
            $this->reportError(
893 3
                sprintf(
894 3
                    'Union type %s can only include Object types, it cannot include %s.',
895 3
                    $union->name,
896 3
                    Utils::printSafe($memberType)
897
                ),
898 3
                $this->getUnionMemberTypeNodes($union, Utils::printSafe($memberType))
899
            );
900
        }
901 15
    }
902
903
    /**
904
     * @param string $typeName
905
     *
906
     * @return NamedTypeNode[]
907
     */
908 4
    private function getUnionMemberTypeNodes(UnionType $union, $typeName)
909
    {
910
        $subNodes = $this->getAllSubNodes($union, static function ($unionNode) {
911 4
            return $unionNode->types;
912 4
        });
913
914
        return Utils::filter($subNodes, static function ($typeNode) use ($typeName) {
915 4
            return $typeNode->name->value === $typeName;
916 4
        });
917
    }
918
919 95
    private function validateEnumValues(EnumType $enumType)
920
    {
921 95
        $enumValues = $enumType->getValues();
922
923 95
        if (! $enumValues) {
0 ignored issues
show
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...
introduced by
$enumValues is a non-empty array, thus ! $enumValues is always false.
Loading history...
924 1
            $this->reportError(
925 1
                sprintf('Enum type %s must define one or more values.', $enumType->name),
926 1
                $this->getAllNodes($enumType)
927
            );
928
        }
929
930 95
        foreach ($enumValues as $enumValue) {
931 95
            $valueName = $enumValue->name;
932
933
            // Ensure no duplicates
934 95
            $allNodes = $this->getEnumValueNodes($enumType, $valueName);
935 95
            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...
936 1
                $this->reportError(
937 1
                    sprintf('Enum type %s can include value %s only once.', $enumType->name, $valueName),
938 1
                    $allNodes
939
                );
940
            }
941
942
            // Ensure valid name.
943 95
            $this->validateName($enumValue);
944 95
            if ($valueName === 'true' || $valueName === 'false' || $valueName === 'null') {
945 3
                $this->reportError(
946 3
                    sprintf('Enum type %s cannot include value: %s.', $enumType->name, $valueName),
947 3
                    $enumValue->astNode
948
                );
949
            }
950
951
            // Ensure valid directives
952 95
            if (! isset($enumValue->astNode, $enumValue->astNode->directives)) {
953 95
                continue;
954
            }
955
956 2
            $this->validateDirectivesAtLocation(
957 2
                $enumValue->astNode->directives,
0 ignored issues
show
Bug introduced by
$enumValue->astNode->directives of type GraphQL\Language\AST\DirectiveNode[] is incompatible with the type GraphQL\Language\AST\NodeList expected by parameter $directives of GraphQL\Type\SchemaValid...eDirectivesAtLocation(). ( Ignorable by Annotation )

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

957
                /** @scrutinizer ignore-type */ $enumValue->astNode->directives,
Loading history...
958 2
                DirectiveLocation::ENUM_VALUE
959
            );
960
        }
961 95
    }
962
963
    /**
964
     * @param string $valueName
965
     *
966
     * @return EnumValueDefinitionNode[]
967
     */
968 95
    private function getEnumValueNodes(EnumType $enum, $valueName)
969
    {
970
        $subNodes = $this->getAllSubNodes($enum, static function ($enumNode) {
971 2
            return $enumNode->values;
972 95
        });
973
974
        return Utils::filter($subNodes, static function ($valueNode) use ($valueName) {
975 2
            return $valueNode->name->value === $valueName;
976 95
        });
977
    }
978
979 26
    private function validateInputFields(InputObjectType $inputObj)
980
    {
981 26
        $fieldMap = $inputObj->getFields();
982
983 26
        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...
984 1
            $this->reportError(
985 1
                sprintf('Input Object type %s must define one or more fields.', $inputObj->name),
986 1
                $this->getAllNodes($inputObj)
987
            );
988
        }
989
990
        // Ensure the arguments are valid
991 26
        foreach ($fieldMap as $fieldName => $field) {
992
            // Ensure they are named correctly.
993 25
            $this->validateName($field);
994
995
            // TODO: Ensure they are unique per field.
996
997
            // Ensure the type is an input type
998 25
            if (! Type::isInputType($field->getType())) {
999 5
                $this->reportError(
1000 5
                    sprintf(
1001 5
                        'The type of %s.%s must be Input Type but got: %s.',
1002 5
                        $inputObj->name,
1003 5
                        $fieldName,
1004 5
                        Utils::printSafe($field->getType())
1005
                    ),
1006 5
                    $field->astNode ? $field->astNode->type : null
1007
                );
1008
            }
1009
1010
            // Ensure valid directives
1011 25
            if (! isset($field->astNode, $field->astNode->directives)) {
1012 10
                continue;
1013
            }
1014
1015 15
            $this->validateDirectivesAtLocation(
1016 15
                $field->astNode->directives,
0 ignored issues
show
Bug introduced by
$field->astNode->directives of type GraphQL\Language\AST\DirectiveNode[] is incompatible with the type GraphQL\Language\AST\NodeList expected by parameter $directives of GraphQL\Type\SchemaValid...eDirectivesAtLocation(). ( Ignorable by Annotation )

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

1016
                /** @scrutinizer ignore-type */ $field->astNode->directives,
Loading history...
1017 15
                DirectiveLocation::FIELD_DEFINITION
1018
            );
1019
        }
1020 26
    }
1021
}
1022