Failed Conditions
Push — master ( e31947...cc39b3 )
by Šimon
11s
created

NoFragmentCycles::cycleErrorMessage()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 1
nop 2
crap 2
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\FragmentDefinitionNode;
9
use GraphQL\Language\AST\FragmentSpreadNode;
10
use GraphQL\Language\AST\NodeKind;
11
use GraphQL\Language\Visitor;
12
use GraphQL\Utils\Utils;
13
use GraphQL\Validator\ValidationContext;
14
use function array_merge;
15
use function array_pop;
16
use function array_slice;
17
use function count;
18
use function implode;
19
use function is_array;
20
use function sprintf;
21
22
class NoFragmentCycles extends ValidationRule
23
{
24
    /** @var bool[] */
25
    public $visitedFrags;
26
27
    /** @var FragmentSpreadNode[] */
28
    public $spreadPath;
29
30
    /** @var (int|null)[] */
31
    public $spreadPathIndexByName;
32
33 117
    public function getVisitor(ValidationContext $context)
34
    {
35
        // Tracks already visited fragments to maintain O(N) and to ensure that cycles
36
        // are not redundantly reported.
37 117
        $this->visitedFrags = [];
38
39
        // Array of AST nodes used to produce meaningful errors
40 117
        $this->spreadPath = [];
41
42
        // Position in the spread path
43 117
        $this->spreadPathIndexByName = [];
44
45
        return [
46
            NodeKind::OPERATION_DEFINITION => function () {
47 102
                return Visitor::skipNode();
48 117
            },
49
            NodeKind::FRAGMENT_DEFINITION  => function (FragmentDefinitionNode $node) use ($context) {
50 23
                if (! isset($this->visitedFrags[$node->name->value])) {
51 23
                    $this->detectCycleRecursive($node, $context);
52
                }
53
54 23
                return Visitor::skipNode();
55 117
            },
56
        ];
57
    }
58
59 23
    private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context)
60
    {
61 23
        $fragmentName                      = $fragment->name->value;
62 23
        $this->visitedFrags[$fragmentName] = true;
63
64 23
        $spreadNodes = $context->getFragmentSpreads($fragment);
65
66 23
        if (empty($spreadNodes)) {
67 12
            return;
68
        }
69
70 17
        $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
71
72 17
        for ($i = 0; $i < count($spreadNodes); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
73 17
            $spreadNode = $spreadNodes[$i];
74 17
            $spreadName = $spreadNode->name->value;
75 17
            $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
76
77 17
            if ($cycleIndex === null) {
78 14
                $this->spreadPath[] = $spreadNode;
79 14
                if (empty($this->visitedFrags[$spreadName])) {
80 13
                    $spreadFragment = $context->getFragment($spreadName);
81 13
                    if ($spreadFragment) {
82 12
                        $this->detectCycleRecursive($spreadFragment, $context);
83
                    }
84
                }
85 14
                array_pop($this->spreadPath);
86
            } else {
87 10
                $cyclePath = array_slice($this->spreadPath, $cycleIndex);
88 10
                $nodes     = $cyclePath;
89
90 10
                if (is_array($spreadNode)) {
91
                    $nodes = array_merge($nodes, $spreadNode);
92
                } else {
93 10
                    $nodes[] = $spreadNode;
94
                }
95
96 10
                $context->reportError(new Error(
97 10
                    self::cycleErrorMessage(
98 10
                        $spreadName,
99 10
                        Utils::map(
100 10
                            $cyclePath,
101
                            function ($s) {
102 7
                                return $s->name->value;
103 10
                            }
104
                        )
105
                    ),
106 10
                    $nodes
107
                ));
108
            }
109
        }
110
111 17
        $this->spreadPathIndexByName[$fragmentName] = null;
112 17
    }
113
114
    /**
115
     * @param string[] $spreadNames
116
     */
117 10
    public static function cycleErrorMessage($fragName, array $spreadNames = [])
118
    {
119 10
        return sprintf(
120 10
            'Cannot spread fragment "%s" within itself%s.',
121 10
            $fragName,
122 10
            ! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : ''
123
        );
124
    }
125
}
126