Passed
Pull Request — master (#52)
by Christoffer
02:05
created

AcceptsVisitorsTrait::getAncestor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
rs 10
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 AcceptsVisitorsTrait
9
{
10
11
    /**
12
     * @var VisitorInterface
13
     */
14
    protected $visitor;
15
16
    /**
17
     * @var string|int|null
18
     */
19
    protected $key;
20
21
    /**
22
     * @var NodeInterface|null
23
     */
24
    protected $parent;
25
26
    /**
27
     * @var array
28
     */
29
    protected $path;
30
31
    /**
32
     * @var array
33
     */
34
    protected $ancestors = [];
35
36
    /**
37
     * @var boolean
38
     */
39
    protected $isEdited = false;
40
41
    /**
42
     * @param VisitorInterface      $visitor
43
     * @param string|int|null       $key
44
     * @param NodeInterface|null    $parent
45
     * @param array|string[]        $path
46
     * @param array|NodeInterface[] $ancestors
47
     * @return NodeInterface|AcceptsVisitorsTrait|SerializationInterface|null
48
     */
49
    public function acceptVisitor(
50
        VisitorInterface $visitor,
51
        $key = null,
52
        ?NodeInterface $parent = null,
53
        array $path = [],
54
        array $ancestors = []
55
    ): ?NodeInterface {
56
        $this->visitor   = $visitor;
57
        $this->key       = $key;
58
        $this->parent    = $parent;
59
        $this->path      = $path;
60
        $this->ancestors = $ancestors;
61
62
        /** @var NodeInterface|AcceptsVisitorsTrait|null $newNode */
63
        $newNode = clone $this; // TODO: Benchmark cloning
64
65
        // If the result was null, it means that we should not traverse this branch.
66
        if (null === ($newNode = $visitor->enterNode($newNode))) {
0 ignored issues
show
Bug introduced by
It seems like $newNode can also be of type null and Digia\GraphQL\Language\A...or\AcceptsVisitorsTrait; 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

66
        if (null === ($newNode = $visitor->enterNode(/** @scrutinizer ignore-type */ $newNode))) {
Loading history...
67
            return null;
68
        }
69
70
        // If the node was edited, we want to return early
71
        // to avoid visiting its sub-tree completely.
72
        if ($newNode->determineIsEdited($this)) {
0 ignored issues
show
Bug introduced by
The method determineIsEdited() does not exist on Digia\GraphQL\Language\AST\Node\NodeInterface. ( Ignorable by Annotation )

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

72
        if ($newNode->/** @scrutinizer ignore-call */ determineIsEdited($this)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
73
            return $newNode;
74
        }
75
76
        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...or\AcceptsVisitorsTrait. Did you maybe forget to declare it?
Loading history...
77
            $nodeOrNodes = $this->getNodeOrNodes($name);
78
79
            if (null === $nodeOrNodes || empty($nodeOrNodes)) {
80
                continue;
81
            }
82
83
            $newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $name);
84
85
            if (null === $newNodeOrNodes || empty($newNodeOrNodes)) {
86
                continue;
87
            }
88
89
            $setter = 'set' . ucfirst($name);
90
91
            if (method_exists($newNode, $setter)) {
92
                $newNode->{$setter}($newNodeOrNodes);
93
            }
94
        }
95
96
        if (null !== $parent) {
97
            $this->removeAncestor();
98
        }
99
100
        return $visitor->leaveNode($newNode);
101
    }
102
103
    /**
104
     * @return bool
105
     */
106
    public function isEdited(): bool
107
    {
108
        return $this->isEdited;
109
    }
110
111
    /**
112
     * @param bool $isEdited
113
     * @return $this
114
     */
115
    public function setIsEdited(bool $isEdited)
116
    {
117
        $this->isEdited = $isEdited;
118
        return $this;
119
    }
120
121
    /**
122
     * @param int $depth
123
     * @return NodeInterface|null
124
     */
125
    public function getAncestor(int $depth = 1): ?NodeInterface
126
    {
127
        return !empty($this->ancestors) ? $this->ancestors[\count($this->ancestors) - $depth] : null;
128
    }
129
130
    /**
131
     * @return NodeInterface[]
132
     */
133
    public function getAncestors(): array
134
    {
135
        return $this->ancestors;
136
    }
137
138
    /**
139
     * @return int|null|string
140
     */
141
    public function getKey()
142
    {
143
        return $this->key;
144
    }
145
146
    /**
147
     * @return NodeInterface|null
148
     */
149
    public function getParent(): ?NodeInterface
150
    {
151
        return $this->parent;
152
    }
153
154
    /**
155
     * @return array
156
     */
157
    public function getPath(): array
158
    {
159
        return $this->path;
160
    }
161
162
    /**
163
     * @param string|int $key
164
     * @return array|NodeInterface|NodeInterface[]|null
165
     */
166
    protected function getNodeOrNodes($key)
167
    {
168
        return $this->{$key};
169
    }
170
171
    /**
172
     * @param            $nodeOrNodes
173
     * @param string|int $key
174
     * @return array|NodeInterface|NodeInterface[]|null
175
     */
176
    protected function visitNodeOrNodes($nodeOrNodes, $key)
177
    {
178
        $this->addAncestor($this);
0 ignored issues
show
Bug introduced by
$this of type Digia\GraphQL\Language\A...or\AcceptsVisitorsTrait is incompatible with the type Digia\GraphQL\Language\AST\Node\NodeInterface expected by parameter $node of Digia\GraphQL\Language\A...orsTrait::addAncestor(). ( Ignorable by Annotation )

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

178
        $this->addAncestor(/** @scrutinizer ignore-type */ $this);
Loading history...
179
180
        $newNodeOrNodes = \is_array($nodeOrNodes)
181
            ? $this->visitNodes($nodeOrNodes, $key)
182
            : $this->visitNode($nodeOrNodes, $key, $this);
0 ignored issues
show
Bug introduced by
$this of type Digia\GraphQL\Language\A...or\AcceptsVisitorsTrait is incompatible with the type null|Digia\GraphQL\Language\AST\Node\NodeInterface expected by parameter $parent of Digia\GraphQL\Language\A...itorsTrait::visitNode(). ( Ignorable by Annotation )

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

182
            : $this->visitNode($nodeOrNodes, $key, /** @scrutinizer ignore-type */ $this);
Loading history...
183
184
        $this->removeAncestor();
185
186
        return $newNodeOrNodes;
187
    }
188
189
    /**
190
     * @param NodeInterface[]    $nodes
191
     * @param string|int         $key
192
     * @param NodeInterface|null $parent
193
     * @return NodeInterface[]
194
     */
195
    protected function visitNodes(array $nodes, $key): array
196
    {
197
        $this->addOneToPath($key);
198
199
        $index    = 0;
200
        $newNodes = [];
201
202
        foreach ($nodes as $node) {
203
            $newNode = $this->visitNode($node, $index, null);
204
205
            if (null !== $newNode) {
206
                $newNodes[$index] = $newNode;
207
                $index++;
208
            }
209
        }
210
211
        $this->removeOneFromPath();
212
213
        return $newNodes;
214
    }
215
216
    /**
217
     * @param NodeInterface|AcceptsVisitorsTrait $node
218
     * @param string|int                         $key
219
     * @param NodeInterface|null                 $parent
220
     * @return NodeInterface|null
221
     */
222
    protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?NodeInterface
223
    {
224
        $this->addOneToPath($key);
225
226
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
0 ignored issues
show
Bug introduced by
The method acceptVisitor() does not exist on Digia\GraphQL\Language\AST\Node\NodeInterface. ( Ignorable by Annotation )

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

226
        /** @scrutinizer ignore-call */ 
227
        $newNode = $node->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
227
228
        // If the node was edited, we need to revisit it
229
        // to produce the expected result.
230
        if (null !== $newNode && $newNode->isEdited()) {
231
            $newNode = $newNode->acceptVisitor($this->visitor, $key, $parent, $this->path, $this->ancestors);
232
        }
233
234
        $this->removeOneFromPath();
235
236
        return $newNode;
237
    }
238
239
    /**
240
     * @param NodeInterface|AcceptsVisitorsTrait $node
241
     * @return bool
242
     */
243
    protected function determineIsEdited(NodeInterface $node): bool
244
    {
245
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
246
        return $this->isEdited;
247
    }
248
249
    /**
250
     * @param NodeInterface $other
251
     * @return bool
252
     */
253
    protected function compareNode(NodeInterface $other)
254
    {
255
        // TODO: Figure out a better way to solve this.
256
        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

256
        return $this->/** @scrutinizer ignore-call */ toJSON() === $other->toJSON();
Loading history...
257
    }
258
259
    /**
260
     * Appends a key to the path.
261
     * @param string $key
262
     */
263
    protected function addOneToPath(string $key)
264
    {
265
        $this->path[] = $key;
266
    }
267
268
    /**
269
     * Removes the last item from the path.
270
     */
271
    protected function removeOneFromPath()
272
    {
273
        $this->path = \array_slice($this->path, 0, -1);
274
    }
275
276
    /**
277
     * Adds an ancestor.
278
     * @param NodeInterface $node
279
     */
280
    protected function addAncestor(NodeInterface $node)
281
    {
282
        $this->ancestors[] = $node;
283
    }
284
285
    /**
286
     *  Removes the last ancestor.
287
     */
288
    protected function removeAncestor()
289
    {
290
        $this->ancestors = \array_slice($this->ancestors, 0, -1);
291
    }
292
293
    /**
294
     * @var array
295
     */
296
    protected static $kindToNodesToVisitMap = [
297
        'Name' => [],
298
299
        'Document'            => ['definitions'],
300
        'OperationDefinition' => [
301
            'name',
302
            'variableDefinitions',
303
            'directives',
304
            'selectionSet',
305
        ],
306
        'VariableDefinition'  => ['variable', 'type', 'defaultValue'],
307
        'Variable'            => ['name'],
308
        'SelectionSet'        => ['selections'],
309
        'Field'               => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
310
        'Argument'            => ['name', 'value'],
311
312
        'FragmentSpread'     => ['name', 'directives'],
313
        'InlineFragment'     => ['typeCondition', 'directives', 'selectionSet'],
314
        'FragmentDefinition' => [
315
            'name',
316
            // Note: fragment variable definitions are experimental and may be changed or removed in the future.
317
            'variableDefinitions',
318
            'typeCondition',
319
            'directives',
320
            'selectionSet',
321
        ],
322
323
        'IntValue'     => [],
324
        'FloatValue'   => [],
325
        'StringValue'  => [],
326
        'BooleanValue' => [],
327
        'NullValue'    => [],
328
        'EnumValue'    => [],
329
        'ListValue'    => ['values'],
330
        'ObjectValue'  => ['fields'],
331
        'ObjectField'  => ['name', 'value'],
332
333
        'Directive' => ['name', 'arguments'],
334
335
        'NamedType'   => ['name'],
336
        'ListType'    => ['type'],
337
        'NonNullType' => ['type'],
338
339
        'SchemaDefinition'        => ['directives', 'operationTypes'],
340
        'OperationTypeDefinition' => ['type'],
341
342
        'ScalarTypeDefinition'      => ['description', 'name', 'directives'],
343
        'ObjectTypeDefinition'      => [
344
            'description',
345
            'name',
346
            'interfaces',
347
            'directives',
348
            'fields',
349
        ],
350
        'FieldDefinition'           => ['description', 'name', 'arguments', 'type', 'directives'],
351
        'InputValueDefinition'      => [
352
            'description',
353
            'name',
354
            'type',
355
            'defaultValue',
356
            'directives',
357
        ],
358
        'InterfaceTypeDefinition'   => ['description', 'name', 'directives', 'fields'],
359
        'UnionTypeDefinition'       => ['description', 'name', 'directives', 'types'],
360
        'EnumTypeDefinition'        => ['description', 'name', 'directives', 'values'],
361
        'EnumValueDefinition'       => ['description', 'name', 'directives'],
362
        'InputObjectTypeDefinition' => ['description', 'name', 'directives', 'fields'],
363
364
        'ScalarTypeExtension'      => ['name', 'directives'],
365
        'ObjectTypeExtension'      => ['name', 'interfaces', 'directives', 'fields'],
366
        'InterfaceTypeExtension'   => ['name', 'directives', 'fields'],
367
        'UnionTypeExtension'       => ['name', 'directives', 'types'],
368
        'EnumTypeExtension'        => ['name', 'directives', 'values'],
369
        'InputObjectTypeExtension' => ['name', 'directives', 'fields'],
370
371
        'DirectiveDefinition' => ['description', 'name', 'arguments', 'locations'],
372
    ];
373
}
374