Passed
Pull Request — master (#23)
by Christoffer
01:58
created

AcceptVisitorTrait::visitNodes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Digia\GraphQL\Language\AST\Visitor;
4
5
use Digia\GraphQL\Language\AST\Node\NodeInterface;
6
use Digia\GraphQL\Util\SerializationInterface;
7
8
trait AcceptVisitorTrait
9
{
10
11
    /**
12
     * @var VisitorInterface
13
     */
14
    protected $visitor;
15
16
    /**
17
     * @var array
18
     */
19
    protected $path;
20
21
    /**
22
     * @var boolean
23
     */
24
    protected $isEdited = false;
25
26
    /**
27
     * @param VisitorInterface $visitor
28
     * @param string|int||null $key
29
     * @param NodeInterface|null $parent
30
     * @param array $path
31
     * @return NodeInterface|AcceptVisitorTrait|SerializationInterface|null
32
     * @throws VisitorBreak
33
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment string|int||null at position 4 could not be parsed: Unknown type name '|' at position 4 in string|int||null.
Loading history...
34
    public function accept(
35
        VisitorInterface $visitor,
36
        $key = null,
37
        ?NodeInterface $parent = null,
38
        array $path = []
39
    ): ?NodeInterface {
40
        $this->visitor = $visitor;
41
        $this->path = $path;
42
43
        /** @var NodeInterface|AcceptVisitorTrait|null $newNode */
44
        $newNode = clone $this; // TODO: Benchmark cloning
45
46
        if (null === ($newNode = $visitor->enterNode($newNode, $key, $parent, $this->path))) {
0 ignored issues
show
Bug introduced by
It seems like $newNode can also be of type null and Digia\GraphQL\Language\A...itor\AcceptVisitorTrait; however, parameter $node of Digia\GraphQL\Language\A...rInterface::enterNode() does only seem to accept Digia\GraphQL\Language\AST\Node\NodeInterface, 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

46
        if (null === ($newNode = $visitor->enterNode(/** @scrutinizer ignore-type */ $newNode, $key, $parent, $this->path))) {
Loading history...
47
            return null;
48
        }
49
50
        // If the node was edited, we want to return early
51
        // to avoid visiting its sub-tree completely.
52
        if ($newNode->determineIsEdited($this)) {
0 ignored issues
show
Bug introduced by
The method determineIsEdited() does not exist on Digia\GraphQL\Language\AST\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...eExtensionNodeInterface or Digia\GraphQL\Language\AST\Node\ValueNodeInterface or Digia\GraphQL\Language\A...\SelectionNodeInterface or Digia\GraphQL\Language\AST\Node\TypeNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

52
        if ($newNode->/** @scrutinizer ignore-call */ determineIsEdited($this)) {
Loading history...
53
            return $newNode;
54
        }
55
56
        foreach (self::$kindToNodesToVisitMap[$this->kind] as $name) {
0 ignored issues
show
Bug Best Practice introduced by
The property kind does not exist on Digia\GraphQL\Language\A...itor\AcceptVisitorTrait. Did you maybe forget to declare it?
Loading history...
57
            $nodeOrNodes = $this->getNodeOrNodes($name);
58
59
            if (null === $nodeOrNodes || empty($nodeOrNodes)) {
60
                continue;
61
            }
62
63
            $newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $name);
64
65
            if (null === $newNodeOrNodes || empty($newNodeOrNodes)) {
66
                continue;
67
            }
68
69
            $setter = 'set' . ucfirst($name);
70
71
            if (method_exists($newNode, $setter)) {
72
                $newNode->{$setter}($newNodeOrNodes);
73
            }
74
        }
75
76
        return $visitor->leaveNode($newNode, $key, $parent, $this->path);
77
    }
78
79
    /**
80
     * @return bool
81
     */
82
    public function isEdited(): bool
83
    {
84
        return $this->isEdited;
85
    }
86
87
    /**
88
     * @param bool $isEdited
89
     * @return $this
90
     */
91
    public function setIsEdited(bool $isEdited)
92
    {
93
        $this->isEdited = $isEdited;
94
        return $this;
95
    }
96
97
    /**
98
     * @param string|int $key
99
     * @return array|NodeInterface|NodeInterface[]|null
100
     */
101
    protected function getNodeOrNodes($key)
102
    {
103
        return $this->{$key};
104
    }
105
106
    /**
107
     * @param $nodeOrNodes
108
     * @param string|int $key
109
     * @return array|NodeInterface|NodeInterface[]|null
110
     * @throws VisitorBreak
111
     */
112
    protected function visitNodeOrNodes($nodeOrNodes, $key)
113
    {
114
        return \is_array($nodeOrNodes)
115
            ? $this->visitNodes($nodeOrNodes, $key)
116
            : $this->visitNode($nodeOrNodes, $key, $this);
0 ignored issues
show
Bug introduced by
$this of type Digia\GraphQL\Language\A...itor\AcceptVisitorTrait is incompatible with the type null|Digia\GraphQL\Language\AST\Node\NodeInterface expected by parameter $parent of Digia\GraphQL\Language\A...sitorTrait::visitNode(). ( Ignorable by Annotation )

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

116
            : $this->visitNode($nodeOrNodes, $key, /** @scrutinizer ignore-type */ $this);
Loading history...
117
    }
118
119
    /**
120
     * @param NodeInterface[] $nodes
121
     * @param string|int $key
122
     * @return NodeInterface[]
123
     * @throws VisitorBreak
124
     */
125
    protected function visitNodes(array $nodes, $key): array
126
    {
127
        $this->addOneToPath($key);
128
129
        $index = 0;
130
        $newNodes = [];
131
132
        foreach ($nodes as $node) {
133
            $newNode = $this->visitNode($node, $index, null);
134
135
            if (null !== $newNode) {
136
                $newNodes[$index] = $newNode;
137
                $index++;
138
            }
139
        }
140
141
        $this->removeOneFromPath();
142
143
        return $newNodes;
144
    }
145
146
    /**
147
     * @param NodeInterface|AcceptVisitorTrait $node
148
     * @param string|int $key
149
     * @param NodeInterface|null $parent
150
     * @return NodeInterface|null
151
     * @throws VisitorBreak
152
     */
153
    protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?NodeInterface
154
    {
155
        $this->addOneToPath($key);
156
157
        $newNode = $node->accept($this->visitor, $key, $parent, $this->path);
0 ignored issues
show
Bug introduced by
The method accept() does not exist on Digia\GraphQL\Language\AST\Node\NodeInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...eExtensionNodeInterface or Digia\GraphQL\Language\AST\Node\ValueNodeInterface or Digia\GraphQL\Language\A...\SelectionNodeInterface or Digia\GraphQL\Language\AST\Node\TypeNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface or Digia\GraphQL\Language\A...DefinitionNodeInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

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

157
        /** @scrutinizer ignore-call */ 
158
        $newNode = $node->accept($this->visitor, $key, $parent, $this->path);
Loading history...
158
159
        // If the node was edited, we need to revisit it
160
        // to produce the expected result.
161
        if (null !== $newNode && $newNode->isEdited()) {
162
            $newNode = $newNode->accept($this->visitor, $key, $parent, $this->path);
163
        }
164
165
        $this->removeOneFromPath();
166
167
        return $newNode;
168
    }
169
170
    /**
171
     * @param NodeInterface|AcceptVisitorTrait $node
172
     * @return bool
173
     */
174
    protected function determineIsEdited(NodeInterface $node): bool
175
    {
176
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
177
        return $this->isEdited;
178
    }
179
180
    /**
181
     * @param NodeInterface $other
182
     * @return bool
183
     */
184
    protected function compareNode(NodeInterface $other)
185
    {
186
        // TODO: Figure out a better way to solve this.
187
        return $this->toJSON() === $other->toJSON();
0 ignored issues
show
Bug introduced by
It seems like toJSON() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

187
        return $this->/** @scrutinizer ignore-call */ toJSON() === $other->toJSON();
Loading history...
188
    }
189
190
    /**
191
     * Appends a key to the path.
192
     * @param string $key
193
     */
194
    protected function addOneToPath(string $key)
195
    {
196
        $this->path[] = $key;
197
    }
198
199
    /**
200
     * Removes the last item from the path.
201
     */
202
    protected function removeOneFromPath()
203
    {
204
        $this->path = \array_slice($this->path, 0, count($this->path) - 1);
205
    }
206
207
    /**
208
     * @var array
209
     */
210
    protected static $kindToNodesToVisitMap = [
211
        'Name' => [],
212
213
        'Document' => ['definitions'],
214
        'OperationDefinition' => [
215
            'name',
216
            'variableDefinitions',
217
            'directives',
218
            'selectionSet',
219
        ],
220
        'VariableDefinition' => ['variable', 'type', 'defaultValue'],
221
        'Variable' => ['name'],
222
        'SelectionSet' => ['selections'],
223
        'Field' => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
224
        'Argument' => ['name', 'value'],
225
226
        'FragmentSpread' => ['name', 'directives'],
227
        'InlineFragment' => ['typeCondition', 'directives', 'selectionSet'],
228
        'FragmentDefinition' => [
229
            'name',
230
            // Note: fragment variable definitions are experimental and may be changed or removed in the future.
231
            'variableDefinitions',
232
            'typeCondition',
233
            'directives',
234
            'selectionSet',
235
        ],
236
237
        'IntValue' => [],
238
        'FloatValue' => [],
239
        'StringValue' => [],
240
        'BooleanValue' => [],
241
        'NullValue' => [],
242
        'EnumValue' => [],
243
        'ListValue' => ['values'],
244
        'ObjectValue' => ['fields'],
245
        'ObjectField' => ['name', 'value'],
246
247
        'Directive' => ['name', 'arguments'],
248
249
        'NamedType' => ['name'],
250
        'ListType' => ['type'],
251
        'NonNullType' => ['type'],
252
253
        'SchemaDefinition' => ['directives', 'operationTypes'],
254
        'OperationTypeDefinition' => ['type'],
255
256
        'ScalarTypeDefinition' => ['description', 'name', 'directives'],
257
        'ObjectTypeDefinition' => [
258
            'description',
259
            'name',
260
            'interfaces',
261
            'directives',
262
            'fields',
263
        ],
264
        'FieldDefinition' => ['description', 'name', 'arguments', 'type', 'directives'],
265
        'InputValueDefinition' => [
266
            'description',
267
            'name',
268
            'type',
269
            'defaultValue',
270
            'directives',
271
        ],
272
        'InterfaceTypeDefinition' => ['description', 'name', 'directives', 'fields'],
273
        'UnionTypeDefinition' => ['description', 'name', 'directives', 'types'],
274
        'EnumTypeDefinition' => ['description', 'name', 'directives', 'values'],
275
        'EnumValueDefinition' => ['description', 'name', 'directives'],
276
        'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
277
278
        'ScalarTypeExtension' => ['name', 'directives'],
279
        'ObjectTypeExtension' => ['name', 'interfaces', 'directives', 'fields'],
280
        'InterfaceTypeExtension' => ['name', 'directives', 'fields'],
281
        'UnionTypeExtension' => ['name', 'directives', 'types'],
282
        'EnumTypeExtension' => ['name', 'directives', 'values'],
283
        'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
284
285
        'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
286
    ];
287
}
288