Completed
Pull Request — master (#45)
by Christoffer
02:03
created

NoFragmentCyclesRule   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 96
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 96
rs 10
c 0
b 0
f 0
wmc 10

2 Methods

Rating   Name   Duplication   Size   Complexity  
B detectFragmentCycle() 0 45 6
A enterNode() 0 15 4
1
<?php
2
3
namespace Digia\GraphQL\Validation\Rule;
4
5
use Digia\GraphQL\Error\ValidationException;
6
use Digia\GraphQL\Language\AST\Node\DocumentNode;
7
use Digia\GraphQL\Language\AST\Node\FragmentDefinitionNode;
8
use Digia\GraphQL\Language\AST\Node\FragmentSpreadNode;
9
use Digia\GraphQL\Language\AST\Node\NodeInterface;
10
use Digia\GraphQL\Language\AST\Node\OperationDefinitionNode;
11
12
class NoFragmentCyclesRule extends AbstractRule
13
{
14
    /**
15
     * Tracks already visited fragments to maintain O(N) and to ensure that cycles
16
     * are not redundantly reported.
17
     *
18
     * @var array
19
     */
20
    protected $visitedFragments = [];
21
22
    /**
23
     * Array of AST nodes used to produce meaningful errors.
24
     *
25
     * @var array
26
     */
27
    protected $spreadPath = [];
28
29
    /**
30
     * Position in the spread path.
31
     *
32
     * @var array
33
     */
34
    protected $spreadPathIndexByName = [];
35
36
    /**
37
     * @inheritdoc
38
     */
39
    public function enterNode(NodeInterface $node): ?NodeInterface
40
    {
41
        if ($node instanceof OperationDefinitionNode) {
42
            return null;
43
        }
44
45
        if ($node instanceof FragmentDefinitionNode) {
46
            if (!isset($this->visitedFragments[$node->getNameValue()])) {
47
                $this->detectFragmentCycle($node);
48
            }
49
50
            return null;
51
        }
52
53
        return $node;
54
    }
55
56
    /**
57
     * This does a straight-forward DFS to find cycles.
58
     * It does not terminate when a cycle was found but continues to explore
59
     * the graph to find all possible cycles.
60
     *
61
     * @param FragmentDefinitionNode $fragment
62
     */
63
    protected function detectFragmentCycle(FragmentDefinitionNode $fragment): void
64
    {
65
        $fragmentName = $fragment->getNameValue();
66
67
        $this->visitedFragments[$fragmentName] = true;
68
69
        $spreadNodes = $this->context->getFragmentSpreads($fragment->getSelectionSet());
70
71
        if (empty($spreadNodes)) {
72
            return;
73
        }
74
75
        $this->spreadPathIndexByName[$fragmentName] = \count($this->spreadPath);
76
77
        foreach ($spreadNodes as $spreadNode) {
78
            $spreadName = $spreadNode->getNameValue();
79
            $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
80
81
            if (null === $cycleIndex) {
82
                $this->spreadPath[] = $spreadNode;
83
84
                if (!isset($this->visitedFragments[$spreadName])) {
85
                    $spreadFragment = $this->context->getFragment($spreadName);
86
87
                    if (null !== $spreadFragment) {
88
                        $this->detectFragmentCycle($spreadFragment);
89
                    }
90
                }
91
92
                array_pop($this->spreadPath);
93
            } else {
94
                $cyclePath = \array_slice($this->spreadPath, $cycleIndex);
95
96
                $this->context->reportError(
97
                    new ValidationException(
98
                        fragmentCycleMessage($spreadName, array_map(function (FragmentSpreadNode $spread) {
99
                            return $spread->getNameValue();
100
                        }, $cyclePath)),
101
                        array_merge($cyclePath, [$spreadNode])
102
                    )
103
                );
104
            }
105
        }
106
107
        $this->spreadPathIndexByName[$fragmentName] = null;
108
    }
109
}
110