Passed
Pull Request — master (#45)
by Christoffer
02:16
created

AcceptVisitorTrait   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 352
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 352
rs 9.2
c 0
b 0
f 0
wmc 34

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getNodeOrNodes() 0 3 1
A getParent() 0 3 1
A addOneToPath() 0 3 1
C accept() 0 56 11
A removeAncestor() 0 3 1
A visitNodeOrNodes() 0 5 2
A getKey() 0 3 1
A addAncestor() 0 3 1
A isEdited() 0 3 1
A visitNodes() 0 19 3
A determineIsEdited() 0 4 2
A getClosestAncestor() 0 3 2
A compareNode() 0 4 1
A visitNode() 0 15 3
A getPath() 0 3 1
A removeOneFromPath() 0 3 1
A setIsEdited() 0 4 1
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 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                 $path
46
     * @param array|NodeInterface[] $ancestors
47
     * @return NodeInterface|AcceptVisitorTrait|SerializationInterface|null
48
     */
49
    public function accept(
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|AcceptVisitorTrait|null $newNode */
63
        $newNode = clone $this; // TODO: Benchmark cloning
64
65
        if (null !== $parent) {
66
            $this->addAncestor($parent);
67
        }
68
69
        // If the result was null, it means that we should not traverse this branch.
70
        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...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

70
        if (null === ($newNode = $visitor->enterNode(/** @scrutinizer ignore-type */ $newNode))) {
Loading history...
71
            return null;
72
        }
73
74
        // If the node was edited, we want to return early
75
        // to avoid visiting its sub-tree completely.
76
        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

76
        if ($newNode->/** @scrutinizer ignore-call */ determineIsEdited($this)) {
Loading history...
77
            return $newNode;
78
        }
79
80
        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...
81
            $nodeOrNodes = $this->getNodeOrNodes($name);
82
83
            if (null === $nodeOrNodes || empty($nodeOrNodes)) {
84
                continue;
85
            }
86
87
            $newNodeOrNodes = $this->visitNodeOrNodes($nodeOrNodes, $name);
88
89
            if (null === $newNodeOrNodes || empty($newNodeOrNodes)) {
90
                continue;
91
            }
92
93
            $setter = 'set' . ucfirst($name);
94
95
            if (method_exists($newNode, $setter)) {
96
                $newNode->{$setter}($newNodeOrNodes);
97
            }
98
        }
99
100
        if (null !== $parent) {
101
            $this->removeAncestor();
102
        }
103
104
        return $visitor->leaveNode($newNode);
105
    }
106
107
    /**
108
     * @return bool
109
     */
110
    public function isEdited(): bool
111
    {
112
        return $this->isEdited;
113
    }
114
115
    /**
116
     * @param bool $isEdited
117
     * @return $this
118
     */
119
    public function setIsEdited(bool $isEdited)
120
    {
121
        $this->isEdited = $isEdited;
122
        return $this;
123
    }
124
125
    /**
126
     * @return NodeInterface|null
127
     */
128
    public function getClosestAncestor(): ?NodeInterface
129
    {
130
        return !empty($this->ancestors) ? $this->ancestors[\count($this->ancestors) - 1] : null;
131
    }
132
133
    /**
134
     * @return int|null|string
135
     */
136
    public function getKey()
137
    {
138
        return $this->key;
139
    }
140
141
    /**
142
     * @return NodeInterface|null
143
     */
144
    public function getParent(): ?NodeInterface
145
    {
146
        return $this->parent;
147
    }
148
149
    /**
150
     * @return array
151
     */
152
    public function getPath(): array
153
    {
154
        return $this->path;
155
    }
156
157
    /**
158
     * @param string|int $key
159
     * @return array|NodeInterface|NodeInterface[]|null
160
     */
161
    protected function getNodeOrNodes($key)
162
    {
163
        return $this->{$key};
164
    }
165
166
    /**
167
     * @param            $nodeOrNodes
168
     * @param string|int $key
169
     * @return array|NodeInterface|NodeInterface[]|null
170
     */
171
    protected function visitNodeOrNodes($nodeOrNodes, $key)
172
    {
173
        return \is_array($nodeOrNodes)
174
            ? $this->visitNodes($nodeOrNodes, $key)
175
            : $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

175
            : $this->visitNode($nodeOrNodes, $key, /** @scrutinizer ignore-type */ $this);
Loading history...
176
    }
177
178
    /**
179
     * @param NodeInterface[] $nodes
180
     * @param string|int      $key
181
     * @return NodeInterface[]
182
     */
183
    protected function visitNodes(array $nodes, $key): array
184
    {
185
        $this->addOneToPath($key);
186
187
        $index    = 0;
188
        $newNodes = [];
189
190
        foreach ($nodes as $node) {
191
            $newNode = $this->visitNode($node, $index, null);
192
193
            if (null !== $newNode) {
194
                $newNodes[$index] = $newNode;
195
                $index++;
196
            }
197
        }
198
199
        $this->removeOneFromPath();
200
201
        return $newNodes;
202
    }
203
204
    /**
205
     * @param NodeInterface|AcceptVisitorTrait $node
206
     * @param string|int                       $key
207
     * @param NodeInterface|null               $parent
208
     * @return NodeInterface|null
209
     */
210
    protected function visitNode(NodeInterface $node, $key, ?NodeInterface $parent): ?NodeInterface
211
    {
212
        $this->addOneToPath($key);
213
214
        $newNode = $node->accept($this->visitor, $key, $parent, $this->path, $this->ancestors);
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

214
        /** @scrutinizer ignore-call */ 
215
        $newNode = $node->accept($this->visitor, $key, $parent, $this->path, $this->ancestors);
Loading history...
215
216
        // If the node was edited, we need to revisit it
217
        // to produce the expected result.
218
        if (null !== $newNode && $newNode->isEdited()) {
219
            $newNode = $newNode->accept($this->visitor, $key, $parent, $this->path, $this->ancestors);
220
        }
221
222
        $this->removeOneFromPath();
223
224
        return $newNode;
225
    }
226
227
    /**
228
     * @param NodeInterface|AcceptVisitorTrait $node
229
     * @return bool
230
     */
231
    protected function determineIsEdited(NodeInterface $node): bool
232
    {
233
        $this->isEdited = $this->isEdited || !$this->compareNode($node);
234
        return $this->isEdited;
235
    }
236
237
    /**
238
     * @param NodeInterface $other
239
     * @return bool
240
     */
241
    protected function compareNode(NodeInterface $other)
242
    {
243
        // TODO: Figure out a better way to solve this.
244
        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

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