SchemaValidationContext   F
last analyzed

Complexity

Total Complexity 128

Size/Duplication

Total Lines 969
Duplicated Lines 0 %

Test Coverage

Coverage 91.91%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 128
eloc 440
c 2
b 0
f 0
dl 0
loc 969
rs 2
ccs 443
cts 482
cp 0.9191

32 Methods

Rating   Name   Duplication   Size   Complexity  
A getImplementsInterfaceNode() 0 5 1
A getAllDirectiveArgNodes() 0 13 1
B validateRootTypes() 0 31 7
A reportError() 0 4 3
A getFieldArgTypeNode() 0 5 2
A validateName() 0 9 3
A getOperationTypeNode() 0 17 6
A validateObjectInterfaces() 0 24 4
B validateInputFields() 0 39 6
A getUnionMemberTypeNodes() 0 8 1
B validateDirectiveDefinitions() 0 70 9
C validateFields() 0 93 11
A validateUnionMembers() 0 33 5
A getFieldTypeNode() 0 5 2
A getAllFieldNodes() 0 8 1
A addError() 0 3 1
A validateDirectives() 0 8 1
A getFieldArgNode() 0 5 1
A getFieldNode() 0 5 1
A getErrors() 0 3 1
F validateObjectImplementsInterface() 0 138 15
A getAllFieldArgNodes() 0 15 5
A getAllImplementsInterfaceNodes() 0 8 1
A getDirectiveArgTypeNode() 0 5 2
A getAllSubNodes() 0 17 4
B validateEnumValues() 0 40 9
A getDirectives() 0 4 1
A getAllNodes() 0 15 5
A __construct() 0 4 1
A getEnumValueNodes() 0 8 1
B validateTypes() 0 71 10
B validateDirectivesAtLocation() 0 44 7

How to fix   Complexity   

Complex Class

Complex classes like SchemaValidationContext often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaValidationContext, and based on these observations, apply Extract Interface, too.

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 89
    public function __construct(Schema $schema)
61
    {
62 89
        $this->schema                  = $schema;
63 89
        $this->inputObjectCircularRefs = new InputObjectCircularRefs($this);
64 89
    }
65
66
    /**
67
     * @return Error[]
68
     */
69 89
    public function getErrors()
70
    {
71 89
        return $this->errors;
72
    }
73
74 89
    public function validateRootTypes()
75
    {
76 89
        $queryType = $this->schema->getQueryType();
77 89
        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 87
        } 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 89
        $mutationType = $this->schema->getMutationType();
90 89
        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 89
        $subscriptionType = $this->schema->getSubscriptionType();
98 89
        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 87
            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 52
    public function reportError($message, $nodes = null)
113
    {
114 52
        $nodes = array_filter($nodes && is_array($nodes) ? $nodes : [$nodes]);
115 52
        $this->addError(new Error($message, $nodes));
116 52
    }
117
118
    /**
119
     * @param Error $error
120
     */
121 57
    private function addError($error)
122
    {
123 57
        $this->errors[] = $error;
124 57
    }
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 89
    public function validateDirectives()
152
    {
153 89
        $this->validateDirectiveDefinitions();
154
155
        // Validate directives that are used on the schema
156 89
        $this->validateDirectivesAtLocation(
157 89
            $this->getDirectives($this->schema),
158 89
            DirectiveLocation::SCHEMA
159
        );
160 89
    }
161
162 89
    public function validateDirectiveDefinitions()
163
    {
164 89
        $directiveDefinitions = [];
165
166 89
        $directives = $this->schema->getDirectives();
167 89
        foreach ($directives as $directive) {
168
            // Ensure all directives are in fact GraphQL directives.
169 89
            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 88
            $existingDefinitions                    = $directiveDefinitions[$directive->name] ?? [];
177 88
            $existingDefinitions[]                  = $directive;
178 88
            $directiveDefinitions[$directive->name] = $existingDefinitions;
179
180
            // Ensure they are named correctly.
181 88
            $this->validateName($directive);
182
183
            // TODO: Ensure proper locations.
184
185 88
            $argNames = [];
186 88
            foreach ($directive->args as $arg) {
187 88
                $argName = $arg->name;
188
189
                // Ensure they are named correctly.
190 88
                $this->validateName($directive);
191
192 88
                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 88
                $argNames[$argName] = true;
201
202
                // Ensure the type is an input type.
203 88
                if (Type::isInputType($arg->getType())) {
204 88
                    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 88
                    $this->getDirectiveArgTypeNode($directive, $argName)
215
                );
216
            }
217
        }
218 89
        foreach ($directiveDefinitions as $directiveName => $directiveList) {
219 88
            if (count($directiveList) <= 1) {
220 88
                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 89
    }
235
236
    /**
237
     * @param Type|Directive|FieldDefinition|EnumValueDefinition|InputObjectField $node
238
     */
239 89
    private function validateName($node)
240
    {
241
        // Ensure names are valid, however introspection types opt out.
242 89
        $error = Utils::isValidNameError($node->name, $node->astNode);
243 89
        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 89
            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 89
    public function validateTypes() : void
285
    {
286 89
        $typeMap = $this->schema->getTypeMap();
287 89
        foreach ($typeMap as $typeName => $type) {
288
            // Ensure all provided types are in fact GraphQL type.
289 89
            if (! $type instanceof NamedType) {
290
                $this->reportError(
291
                    'Expected GraphQL named type but got: ' . Utils::printSafe($type) . '.',
292
                    $type instanceof Type ? $type->astNode : null
293
                );
294
                continue;
295
            }
296
297 89
            $this->validateName($type);
298
299 89
            if ($type instanceof ObjectType) {
300
                // Ensure fields are valid
301 89
                $this->validateFields($type);
302
303
                // Ensure objects implement the interfaces they claim to.
304 89
                $this->validateObjectInterfaces($type);
305
306
                // Ensure directives are valid
307 89
                $this->validateDirectivesAtLocation(
308 89
                    $this->getDirectives($type),
309 89
                    DirectiveLocation::OBJECT
310
                );
311 89
            } elseif ($type instanceof InterfaceType) {
312
                // Ensure fields are valid.
313 39
                $this->validateFields($type);
314
315
                // Ensure directives are valid
316 39
                $this->validateDirectivesAtLocation(
317 39
                    $this->getDirectives($type),
318 39
                    DirectiveLocation::IFACE
319
                );
320 89
            } elseif ($type instanceof UnionType) {
321
                // Ensure Unions include valid member types.
322 17
                $this->validateUnionMembers($type);
323
324
                // Ensure directives are valid
325 17
                $this->validateDirectivesAtLocation(
326 17
                    $this->getDirectives($type),
327 17
                    DirectiveLocation::UNION
328
                );
329 89
            } elseif ($type instanceof EnumType) {
330
                // Ensure Enums have valid values.
331 89
                $this->validateEnumValues($type);
332
333
                // Ensure directives are valid
334 89
                $this->validateDirectivesAtLocation(
335 89
                    $this->getDirectives($type),
336 89
                    DirectiveLocation::ENUM
337
                );
338 89
            } 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 89
            } elseif ($type instanceof ScalarType) {
351
                // Ensure directives are valid
352 89
                $this->validateDirectivesAtLocation(
353 89
                    $this->getDirectives($type),
354 89
                    DirectiveLocation::SCALAR
355
                );
356
            }
357
        }
358 89
    }
359
360
    /**
361
     * @param NodeList<DirectiveNode> $directives
362
     */
363 89
    private function validateDirectivesAtLocation($directives, string $location)
364
    {
365 89
        $directivesNamed = [];
366 89
        $schema          = $this->schema;
367 89
        foreach ($directives as $directive) {
368 7
            $directiveName = $directive->name->value;
369
370
            // Ensure directive used is also defined
371 7
            $schemaDirective = $schema->getDirective($directiveName);
372 7
            if ($schemaDirective === null) {
373 1
                $this->reportError(
374 1
                    sprintf('No directive @%s defined.', $directiveName),
375 1
                    $directive
376
                );
377 1
                continue;
378
            }
379 7
            $includes = Utils::some(
380 7
                $schemaDirective->locations,
381
                static function ($schemaLocation) use ($location) {
382 7
                    return $schemaLocation === $location;
383 7
                }
384
            );
385 7
            if (! $includes) {
386 2
                $errorNodes = $schemaDirective->astNode
387 2
                    ? [$directive, $schemaDirective->astNode]
388 2
                    : [$directive];
389 2
                $this->reportError(
390 2
                    sprintf('Directive @%s not allowed at %s location.', $directiveName, $location),
391 2
                    $errorNodes
392
                );
393
            }
394
395 7
            $existingNodes                   = $directivesNamed[$directiveName] ?? [];
396 7
            $existingNodes[]                 = $directive;
397 7
            $directivesNamed[$directiveName] = $existingNodes;
398
        }
399 89
        foreach ($directivesNamed as $directiveName => $directiveList) {
400 7
            if (count($directiveList) <= 1) {
401 5
                continue;
402
            }
403
404 2
            $this->reportError(
405 2
                sprintf('Directive @%s used twice at the same location.', $directiveName),
406 2
                $directiveList
407
            );
408
        }
409 89
    }
410
411
    /**
412
     * @param ObjectType|InterfaceType $type
413
     */
414 89
    private function validateFields($type)
415
    {
416 89
        $fieldMap = $type->getFields();
417
418
        // Objects and Interfaces both must define one or more fields.
419 89
        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 89
        foreach ($fieldMap as $fieldName => $field) {
427
            // Ensure they are named correctly.
428 89
            $this->validateName($field);
429
430
            // Ensure they were defined at most once.
431 89
            $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
432 89
            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 89
            if (! Type::isOutputType($field->getType())) {
442 4
                $this->reportError(
443 4
                    sprintf(
444 4
                        'The type of %s.%s must be Output Type but got: %s.',
445 4
                        $type->name,
446 4
                        $fieldName,
447 4
                        Utils::printSafe($field->getType())
448
                    ),
449 4
                    $this->getFieldTypeNode($type, $fieldName)
450
                );
451
            }
452
453
            // Ensure the arguments are valid
454 89
            $argNames = [];
455 89
            foreach ($field->args as $arg) {
456 89
                $argName = $arg->name;
457
458
                // Ensure they are named correctly.
459 89
                $this->validateName($arg);
460
461 89
                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 89
                $argNames[$argName] = true;
473
474
                // Ensure the type is an input type
475 89
                if (! Type::isInputType($arg->getType())) {
476 2
                    $this->reportError(
477 2
                        sprintf(
478 2
                            'The type of %s.%s(%s:) must be Input Type but got: %s.',
479 2
                            $type->name,
480 2
                            $fieldName,
481 2
                            $argName,
482 2
                            Utils::printSafe($arg->getType())
483
                        ),
484 2
                        $this->getFieldArgTypeNode($type, $fieldName, $argName)
485
                    );
486
                }
487
488
                // Ensure argument definition directives are valid
489 89
                if (! isset($arg->astNode, $arg->astNode->directives)) {
490 89
                    continue;
491
                }
492
493 22
                $this->validateDirectivesAtLocation(
494 22
                    $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 22
                    DirectiveLocation::ARGUMENT_DEFINITION
496
                );
497
            }
498
499
            // Ensure any directives are valid
500 89
            if (! isset($field->astNode, $field->astNode->directives)) {
501 89
                continue;
502
            }
503
504 56
            $this->validateDirectivesAtLocation(
505 56
                $field->astNode->directives,
506 56
                DirectiveLocation::FIELD_DEFINITION
507
            );
508
        }
509 89
    }
510
511
    /**
512
     * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj
513
     *
514
     * @return ObjectTypeDefinitionNode[]|ObjectTypeExtensionNode[]|InterfaceTypeDefinitionNode[]|InterfaceTypeExtensionNode[]
515
     */
516 89
    private function getAllNodes($obj)
517
    {
518 89
        if ($obj instanceof Schema) {
519 89
            $astNode        = $obj->getAstNode();
520 89
            $extensionNodes = $obj->extensionASTNodes;
521
        } else {
522 89
            $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 89
            $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 89
        return $astNode
527 57
            ? ($extensionNodes
528 10
                ? array_merge([$astNode], $extensionNodes)
529 57
                : [$astNode])
530 89
            : ($extensionNodes ?: []);
531
    }
532
533
    /**
534
     * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|Directive $obj
535
     *
536
     * @return NodeList
537
     */
538 89
    private function getAllSubNodes($obj, callable $getter)
539
    {
540 89
        $result = new NodeList([]);
541 89
        foreach ($this->getAllNodes($obj) as $astNode) {
542 59
            if (! $astNode) {
543
                continue;
544
            }
545
546 59
            $subNodes = $getter($astNode);
547 59
            if (! $subNodes) {
548
                continue;
549
            }
550
551 59
            $result = $result->merge($subNodes);
552
        }
553
554 89
        return $result;
555
    }
556
557
    /**
558
     * @param ObjectType|InterfaceType $type
559
     * @param string                   $fieldName
560
     *
561
     * @return FieldDefinitionNode[]
562
     */
563 89
    private function getAllFieldNodes($type, $fieldName)
564
    {
565
        $subNodes = $this->getAllSubNodes($type, static function ($typeNode) {
566 56
            return $typeNode->fields;
567 89
        });
568
569
        return Utils::filter($subNodes, static function ($fieldNode) use ($fieldName) {
570 56
            return $fieldNode->name->value === $fieldName;
571 89
        });
572
    }
573
574
    /**
575
     * @param ObjectType|InterfaceType $type
576
     * @param string                   $fieldName
577
     *
578
     * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
579
     */
580 11
    private function getFieldTypeNode($type, $fieldName) : ?TypeNode
581
    {
582 11
        $fieldNode = $this->getFieldNode($type, $fieldName);
583
584 11
        return $fieldNode ? $fieldNode->type : null;
585
    }
586
587
    /**
588
     * @param ObjectType|InterfaceType $type
589
     * @param string                   $fieldName
590
     *
591
     * @return FieldDefinitionNode|null
592
     */
593 20
    private function getFieldNode($type, $fieldName)
594
    {
595 20
        $nodes = $this->getAllFieldNodes($type, $fieldName);
596
597 20
        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 7
    private function getAllFieldArgNodes($type, $fieldName, $argName)
608
    {
609 7
        $argNodes  = [];
610 7
        $fieldNode = $this->getFieldNode($type, $fieldName);
611 7
        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 7
        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 5
    private function getFieldArgTypeNode($type, $fieldName, $argName) : ?TypeNode
632
    {
633 5
        $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
634
635 5
        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 7
    private function getFieldArgNode($type, $fieldName, $argName)
646
    {
647 7
        $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
648
649 7
        return $nodes[0] ?? null;
650
    }
651
652 89
    private function validateObjectInterfaces(ObjectType $object)
653
    {
654 89
        $implementedTypeNames = [];
655 89
        foreach ($object->getInterfaces() as $iface) {
656 40
            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 38
            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 38
            $implementedTypeNames[$iface->name] = true;
675 38
            $this->validateObjectImplementsInterface($object, $iface);
676
        }
677 89
    }
678
679
    /**
680
     * @param Schema|Type $object
681
     *
682
     * @return NodeList<DirectiveNode>
683
     */
684 89
    private function getDirectives($object)
685
    {
686
        return $this->getAllSubNodes($object, static function ($node) {
687 59
            return $node->directives;
688 89
        });
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 38
    private function validateObjectImplementsInterface(ObjectType $object, $iface)
723
    {
724 38
        $objectFieldMap = $object->getFields();
725 38
        $ifaceFieldMap  = $iface->getFields();
726
727
        // Assert each interface field is implemented.
728 38
        foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
729 38
            $objectField = array_key_exists($fieldName, $objectFieldMap)
730 37
                ? $objectFieldMap[$fieldName]
731 38
                : null;
732
733
            // Assert interface field exists on object.
734 38
            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 37
            if (! TypeComparators::isTypeSubTypeOf(
753 37
                $this->schema,
754 37
                $objectField->getType(),
755 37
                $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 37
            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 37
            foreach ($objectField->args as $objectArg) {
833 10
                $argName  = $objectArg->name;
834 10
                $ifaceArg = null;
835
836 10
                foreach ($ifaceField->args as $arg) {
837 7
                    if ($arg->name === $argName) {
838 7
                        $ifaceArg = $arg;
839 7
                        break;
840
                    }
841
                }
842
843 10
                if ($ifaceArg || ! ($objectArg->getType() instanceof NonNull)) {
844 10
                    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 37
                        $this->getFieldNode($iface, $fieldName),
860
                    ]
861
                );
862
            }
863
        }
864 38
    }
865
866 17
    private function validateUnionMembers(UnionType $union)
867
    {
868 17
        $memberTypes = $union->getTypes();
869
870 17
        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 17
        $includedTypeNames = [];
878
879 17
        foreach ($memberTypes as $memberType) {
880 16
            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 16
            $includedTypeNames[$memberType->name] = true;
888 16
            if ($memberType instanceof ObjectType) {
889 15
                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 17
    }
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 89
    private function validateEnumValues(EnumType $enumType)
920
    {
921 89
        $enumValues = $enumType->getValues();
922
923 89
        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 89
        foreach ($enumValues as $enumValue) {
931 89
            $valueName = $enumValue->name;
932
933
            // Ensure no duplicates
934 89
            $allNodes = $this->getEnumValueNodes($enumType, $valueName);
935 89
            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 89
            $this->validateName($enumValue);
944 89
            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 89
            if (! isset($enumValue->astNode, $enumValue->astNode->directives)) {
953 89
                continue;
954
            }
955
956 4
            $this->validateDirectivesAtLocation(
957 4
                $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 4
                DirectiveLocation::ENUM_VALUE
959
            );
960
        }
961 89
    }
962
963
    /**
964
     * @param string $valueName
965
     *
966
     * @return EnumValueDefinitionNode[]
967
     */
968 89
    private function getEnumValueNodes(EnumType $enum, $valueName)
969
    {
970
        $subNodes = $this->getAllSubNodes($enum, static function ($enumNode) {
971 4
            return $enumNode->values;
972 89
        });
973
974
        return Utils::filter($subNodes, static function ($valueNode) use ($valueName) {
975 4
            return $valueNode->name->value === $valueName;
976 89
        });
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 3
                $this->reportError(
1000 3
                    sprintf(
1001 3
                        'The type of %s.%s must be Input Type but got: %s.',
1002 3
                        $inputObj->name,
1003 3
                        $fieldName,
1004 3
                        Utils::printSafe($field->getType())
1005
                    ),
1006 3
                    $field->astNode ? $field->astNode->type : null
1007
                );
1008
            }
1009
1010
            // Ensure valid directives
1011 25
            if (! isset($field->astNode, $field->astNode->directives)) {
1012 8
                continue;
1013
            }
1014
1015 17
            $this->validateDirectivesAtLocation(
1016 17
                $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 17
                DirectiveLocation::FIELD_DEFINITION
1018
            );
1019
        }
1020 26
    }
1021
}
1022