Passed
Push — master ( a27dd3...975c9f )
by Vladimir
11:24
created

NoFragmentCycles::getVisitor()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
dl 0
loc 20
ccs 8
cts 8
cp 1
rs 9.9666
c 1
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 131
    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 131
        $this->visitedFrags = [];
38
39
        // Array of AST nodes used to produce meaningful errors
40 131
        $this->spreadPath = [];
41
42
        // Position in the spread path
43 131
        $this->spreadPathIndexByName = [];
44
45
        return [
46
            NodeKind::OPERATION_DEFINITION => static function () {
47 115
                return Visitor::skipNode();
48 131
            },
49
            NodeKind::FRAGMENT_DEFINITION  => function (FragmentDefinitionNode $node) use ($context) {
50 27
                $this->detectCycleRecursive($node, $context);
51
52 27
                return Visitor::skipNode();
53 131
            },
54
        ];
55
    }
56
57 27
    private function detectCycleRecursive(FragmentDefinitionNode $fragment, ValidationContext $context)
58
    {
59 27
        if (! empty($this->visitedFrags[$fragment->name->value])) {
60 14
            return;
61
        }
62
63 27
        $fragmentName                      = $fragment->name->value;
64 27
        $this->visitedFrags[$fragmentName] = true;
65
66 27
        $spreadNodes = $context->getFragmentSpreads($fragment);
67
68 27
        if (empty($spreadNodes)) {
69 16
            return;
70
        }
71
72 18
        $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
73
74 18
        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...
75 18
            $spreadNode = $spreadNodes[$i];
76 18
            $spreadName = $spreadNode->name->value;
77 18
            $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
78
79 18
            $this->spreadPath[] = $spreadNode;
80 18
            if ($cycleIndex === null) {
81 15
                $spreadFragment = $context->getFragment($spreadName);
82 15
                if ($spreadFragment) {
83 15
                    $this->detectCycleRecursive($spreadFragment, $context);
84
                }
85
            } else {
86 10
                $cyclePath     = array_slice($this->spreadPath, $cycleIndex);
87
                $fragmentNames = Utils::map(array_slice($cyclePath, 0, -1), static function ($s) {
88 7
                    return $s->name->value;
89 10
                });
90
91 10
                $context->reportError(new Error(
92 10
                    self::cycleErrorMessage($spreadName, $fragmentNames),
93 10
                    $cyclePath
94
                ));
95
            }
96 18
            array_pop($this->spreadPath);
97
        }
98
99 18
        $this->spreadPathIndexByName[$fragmentName] = null;
100 18
    }
101
102
    /**
103
     * @param string[] $spreadNames
104
     */
105 10
    public static function cycleErrorMessage($fragName, array $spreadNames = [])
106
    {
107 10
        return sprintf(
108 10
            'Cannot spread fragment "%s" within itself%s.',
109 10
            $fragName,
110 10
            ! empty($spreadNames) ? ' via ' . implode(', ', $spreadNames) : ''
111
        );
112
    }
113
}
114