Completed
Push — master ( a01b08...b72ba3 )
by Vladimir
16s queued 14s
created

PossibleFragmentSpreads::getVisitor()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 33
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 33
rs 8.6346
ccs 21
cts 21
cp 1
cc 7
nc 1
nop 1
crap 7
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Validator\Rules;
6
7
use GraphQL\Error\Error;
8
use GraphQL\Language\AST\FragmentSpreadNode;
9
use GraphQL\Language\AST\InlineFragmentNode;
10
use GraphQL\Language\AST\NodeKind;
11
use GraphQL\Type\Definition\AbstractType;
12
use GraphQL\Type\Definition\CompositeType;
13
use GraphQL\Type\Definition\InterfaceType;
14
use GraphQL\Type\Definition\ObjectType;
15
use GraphQL\Type\Definition\UnionType;
16
use GraphQL\Type\Schema;
17
use GraphQL\Utils\TypeInfo;
18
use GraphQL\Validator\ValidationContext;
19
use function sprintf;
20
21
class PossibleFragmentSpreads extends ValidationRule
22
{
23 137
    public function getVisitor(ValidationContext $context)
24
    {
25
        return [
26
            NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context) {
27 24
                $fragType   = $context->getType();
28 24
                $parentType = $context->getParentType();
29
30 24
                if (! ($fragType instanceof CompositeType) ||
31 24
                    ! ($parentType instanceof CompositeType) ||
32 24
                    $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)) {
33 23
                    return;
34
                }
35
36 1
                $context->reportError(new Error(
37 1
                    self::typeIncompatibleAnonSpreadMessage($parentType, $fragType),
38 1
                    [$node]
39
                ));
40 137
            },
41
            NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context) {
42 29
                $fragName   = $node->name->value;
43 29
                $fragType   = $this->getFragmentType($context, $fragName);
44 29
                $parentType = $context->getParentType();
45
46 29
                if (! $fragType ||
47 28
                    ! $parentType ||
48 29
                    $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
0 ignored issues
show
Bug introduced by
$parentType of type GraphQL\Type\Definition\Type is incompatible with the type GraphQL\Type\Definition\CompositeType expected by parameter $parentType of GraphQL\Validator\Rules\...reads::doTypesOverlap(). ( Ignorable by Annotation )

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

48
                    $this->doTypesOverlap($context->getSchema(), $fragType, /** @scrutinizer ignore-type */ $parentType)
Loading history...
49
                ) {
50 21
                    return;
51
                }
52
53 8
                $context->reportError(new Error(
54 8
                    self::typeIncompatibleSpreadMessage($fragName, $parentType, $fragType),
55 8
                    [$node]
56
                ));
57 137
            },
58
        ];
59
    }
60
61 48
    private function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType)
62
    {
63
        // Checking in the order of the most frequently used scenarios:
64
        // Parent type === fragment type
65 48
        if ($parentType === $fragType) {
66 11
            return true;
67
        }
68
69
        // Parent type is interface or union, fragment type is object type
70 37
        if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
71 21
            return $schema->isPossibleType($parentType, $fragType);
72
        }
73
74
        // Parent type is object type, fragment type is interface (or rather rare - union)
75 16
        if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
76 4
            return $schema->isPossibleType($fragType, $parentType);
77
        }
78
79
        // Both are object types:
80 12
        if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) {
81 2
            return $parentType === $fragType;
82
        }
83
84
        // Both are interfaces
85
        // This case may be assumed valid only when implementations of two interfaces intersect
86
        // But we don't have information about all implementations at runtime
87
        // (getting this information via $schema->getPossibleTypes() requires scanning through whole schema
88
        // which is very costly to do at each request due to PHP "shared nothing" architecture)
89
        //
90
        // So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution
91
        // See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602
92 10
        if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) {
93 4
            return true;
94
95
            // Note that there is one case when we do have information about all implementations:
96
            // When schema descriptor is defined ($schema->hasDescriptor())
97
            // BUT we must avoid situation when some query that worked in development had suddenly stopped
98
            // working in production. So staying consistent and always validate.
99
        }
100
101
        // Interface within union
102 6
        if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) {
103 2
            foreach ($parentType->getTypes() as $type) {
104 2
                if ($type->implementsInterface($fragType)) {
105 2
                    return true;
106
                }
107
            }
108
        }
109
110 5
        if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) {
111 2
            foreach ($fragType->getTypes() as $type) {
112 2
                if ($type->implementsInterface($parentType)) {
113 2
                    return true;
114
                }
115
            }
116
        }
117
118 4
        if ($parentType instanceof UnionType && $fragType instanceof UnionType) {
119 2
            foreach ($fragType->getTypes() as $type) {
120 2
                if ($parentType->isPossibleType($type)) {
121 2
                    return true;
122
                }
123
            }
124
        }
125
126 3
        return false;
127
    }
128
129 1
    public static function typeIncompatibleAnonSpreadMessage($parentType, $fragType)
130
    {
131 1
        return sprintf(
132 1
            'Fragment cannot be spread here as objects of type "%s" can never be of type "%s".',
133 1
            $parentType,
134 1
            $fragType
135
        );
136
    }
137
138 29
    private function getFragmentType(ValidationContext $context, $name)
139
    {
140 29
        $frag = $context->getFragment($name);
141 29
        if ($frag) {
142 29
            $type = TypeInfo::typeFromAST($context->getSchema(), $frag->typeCondition);
143 29
            if ($type instanceof CompositeType) {
144 28
                return $type;
145
            }
146
        }
147
148 1
        return null;
149
    }
150
151 8
    public static function typeIncompatibleSpreadMessage($fragName, $parentType, $fragType)
152
    {
153 8
        return sprintf(
154 8
            'Fragment "%s" cannot be spread here as objects of type "%s" can never be of type "%s".',
155 8
            $fragName,
156 8
            $parentType,
157 8
            $fragType
158
        );
159
    }
160
}
161