Failed Conditions
Pull Request — master (#333)
by Jérémiah
04:09
created

Visitor::visit()   F

Complexity

Conditions 45
Paths > 20000

Size

Total Lines 149
Code Lines 103

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 94
CRAP Score 45.0023

Importance

Changes 0
Metric Value
eloc 103
dl 0
loc 149
ccs 94
cts 95
cp 0.9895
rs 0
c 0
b 0
f 0
cc 45
nc 96736
nop 3
crap 45.0023

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Language;
6
7
use ArrayObject;
8
use GraphQL\Language\AST\Node;
9
use GraphQL\Language\AST\NodeKind;
10
use GraphQL\Language\AST\NodeList;
11
use GraphQL\Utils\TypeInfo;
12
use stdClass;
13
use function array_pop;
14
use function array_splice;
15
use function call_user_func;
16
use function call_user_func_array;
17
use function count;
18
use function func_get_args;
19
use function is_array;
20
use function is_callable;
21
use function json_encode;
22
23
/**
24
 * Utility for efficient AST traversal and modification.
25
 *
26
 * `visit()` will walk through an AST using a depth first traversal, calling
27
 * the visitor's enter function at each node in the traversal, and calling the
28
 * leave function after visiting that node and all of it's child nodes.
29
 *
30
 * By returning different values from the enter and leave functions, the
31
 * behavior of the visitor can be altered, including skipping over a sub-tree of
32
 * the AST (by returning false), editing the AST by returning a value or null
33
 * to remove the value, or to stop the whole traversal by returning BREAK.
34
 *
35
 * When using `visit()` to edit an AST, the original AST will not be modified, and
36
 * a new version of the AST with the changes applied will be returned from the
37
 * visit function.
38
 *
39
 *     $editedAST = Visitor::visit($ast, [
40
 *       'enter' => function ($node, $key, $parent, $path, $ancestors) {
41
 *         // return
42
 *         //   null: no action
43
 *         //   Visitor::skipNode(): skip visiting this node
44
 *         //   Visitor::stop(): stop visiting altogether
45
 *         //   Visitor::removeNode(): delete this node
46
 *         //   any value: replace this node with the returned value
47
 *       },
48
 *       'leave' => function ($node, $key, $parent, $path, $ancestors) {
49
 *         // return
50
 *         //   null: no action
51
 *         //   Visitor::stop(): stop visiting altogether
52
 *         //   Visitor::removeNode(): delete this node
53
 *         //   any value: replace this node with the returned value
54
 *       }
55
 *     ]);
56
 *
57
 * Alternatively to providing enter() and leave() functions, a visitor can
58
 * instead provide functions named the same as the [kinds of AST nodes](reference.md#graphqllanguageastnodekind),
59
 * or enter/leave visitors at a named key, leading to four permutations of
60
 * visitor API:
61
 *
62
 * 1) Named visitors triggered when entering a node a specific kind.
63
 *
64
 *     Visitor::visit($ast, [
65
 *       'Kind' => function ($node) {
66
 *         // enter the "Kind" node
67
 *       }
68
 *     ]);
69
 *
70
 * 2) Named visitors that trigger upon entering and leaving a node of
71
 *    a specific kind.
72
 *
73
 *     Visitor::visit($ast, [
74
 *       'Kind' => [
75
 *         'enter' => function ($node) {
76
 *           // enter the "Kind" node
77
 *         }
78
 *         'leave' => function ($node) {
79
 *           // leave the "Kind" node
80
 *         }
81
 *       ]
82
 *     ]);
83
 *
84
 * 3) Generic visitors that trigger upon entering and leaving any node.
85
 *
86
 *     Visitor::visit($ast, [
87
 *       'enter' => function ($node) {
88
 *         // enter any node
89
 *       },
90
 *       'leave' => function ($node) {
91
 *         // leave any node
92
 *       }
93
 *     ]);
94
 *
95
 * 4) Parallel visitors for entering and leaving nodes of a specific kind.
96
 *
97
 *     Visitor::visit($ast, [
98
 *       'enter' => [
99
 *         'Kind' => function($node) {
100
 *           // enter the "Kind" node
101
 *         }
102
 *       },
103
 *       'leave' => [
104
 *         'Kind' => function ($node) {
105
 *           // leave the "Kind" node
106
 *         }
107
 *       ]
108
 *     ]);
109
 */
110
class Visitor
111
{
112
    /** @var string[][] */
113
    public static $visitorKeys = [
114
        NodeKind::NAME                 => [],
115
        NodeKind::DOCUMENT             => ['definitions'],
116
        NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
117
        NodeKind::VARIABLE_DEFINITION  => ['variable', 'type', 'defaultValue'],
118
        NodeKind::VARIABLE             => ['name'],
119
        NodeKind::SELECTION_SET        => ['selections'],
120
        NodeKind::FIELD                => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
121
        NodeKind::ARGUMENT             => ['name', 'value'],
122
        NodeKind::FRAGMENT_SPREAD      => ['name', 'directives'],
123
        NodeKind::INLINE_FRAGMENT      => ['typeCondition', 'directives', 'selectionSet'],
124
        NodeKind::FRAGMENT_DEFINITION  => [
125
            'name',
126
            // Note: fragment variable definitions are experimental and may be changed
127
            // or removed in the future.
128
            'variableDefinitions',
129
            'typeCondition',
130
            'directives',
131
            'selectionSet',
132
        ],
133
134
        NodeKind::INT           => [],
135
        NodeKind::FLOAT         => [],
136
        NodeKind::STRING        => [],
137
        NodeKind::BOOLEAN       => [],
138
        NodeKind::NULL          => [],
139
        NodeKind::ENUM          => [],
140
        NodeKind::LST           => ['values'],
141
        NodeKind::OBJECT        => ['fields'],
142
        NodeKind::OBJECT_FIELD  => ['name', 'value'],
143
        NodeKind::DIRECTIVE     => ['name', 'arguments'],
144
        NodeKind::NAMED_TYPE    => ['name'],
145
        NodeKind::LIST_TYPE     => ['type'],
146
        NodeKind::NON_NULL_TYPE => ['type'],
147
148
        NodeKind::SCHEMA_DEFINITION            => ['directives', 'operationTypes'],
149
        NodeKind::OPERATION_TYPE_DEFINITION    => ['type'],
150
        NodeKind::SCALAR_TYPE_DEFINITION       => ['description', 'name', 'directives'],
151
        NodeKind::OBJECT_TYPE_DEFINITION       => ['description', 'name', 'interfaces', 'directives', 'fields'],
152
        NodeKind::FIELD_DEFINITION             => ['description', 'name', 'arguments', 'type', 'directives'],
153
        NodeKind::INPUT_VALUE_DEFINITION       => ['description', 'name', 'type', 'defaultValue', 'directives'],
154
        NodeKind::INTERFACE_TYPE_DEFINITION    => ['description', 'name', 'directives', 'fields'],
155
        NodeKind::UNION_TYPE_DEFINITION        => ['description', 'name', 'directives', 'types'],
156
        NodeKind::ENUM_TYPE_DEFINITION         => ['description', 'name', 'directives', 'values'],
157
        NodeKind::ENUM_VALUE_DEFINITION        => ['description', 'name', 'directives'],
158
        NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
159
160
        NodeKind::SCALAR_TYPE_EXTENSION       => ['name', 'directives'],
161
        NodeKind::OBJECT_TYPE_EXTENSION       => ['name', 'interfaces', 'directives', 'fields'],
162
        NodeKind::INTERFACE_TYPE_EXTENSION    => ['name', 'directives', 'fields'],
163
        NodeKind::UNION_TYPE_EXTENSION        => ['name', 'directives', 'types'],
164
        NodeKind::ENUM_TYPE_EXTENSION         => ['name', 'directives', 'values'],
165
        NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
166
167
        NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
168
    ];
169
170
    /**
171
     * Visit the AST (see class description for details)
172
     *
173
     * @api
174
     * @param Node|ArrayObject|stdClass $root
175
     * @param callable[]                $visitor
176
     * @param mixed[]|null              $keyMap
177
     * @return Node|mixed
178
     * @throws \Exception
179
     */
180 569
    public static function visit($root, $visitor, $keyMap = null)
181
    {
182 569
        $visitorKeys = $keyMap ?: self::$visitorKeys;
183
184 569
        $stack     = null;
185 569
        $inArray   = $root instanceof NodeList || is_array($root);
0 ignored issues
show
introduced by
The condition is_array($root) is always false.
Loading history...
186 569
        $keys      = [$root];
187 569
        $index     = -1;
188 569
        $edits     = [];
189 569
        $parent    = null;
190 569
        $path      = [];
191 569
        $ancestors = [];
192 569
        $newRoot   = $root;
193
194 569
        $UNDEFINED = null;
195
196
        do {
197 569
            $index++;
198 569
            $isLeaving = $index === count($keys);
199 569
            $key       = null;
200 569
            $node      = null;
201 569
            $isEdited  = $isLeaving && count($edits) !== 0;
202
203 569
            if ($isLeaving) {
204 567
                $key    = ! $ancestors ? $UNDEFINED : $path[count($path) - 1];
205 567
                $node   = $parent;
206 567
                $parent = array_pop($ancestors);
207
208 567
                if ($isEdited) {
209 86
                    if ($inArray) {
210
                        // $node = $node; // arrays are value types in PHP
211 78
                        if ($node instanceof NodeList) {
212 78
                            $node = clone $node;
213
                        }
214
                    } else {
215 86
                        $node = clone $node;
216
                    }
217 86
                    $editOffset = 0;
218 86
                    for ($ii = 0; $ii < count($edits); $ii++) {
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...
219 86
                        $editKey   = $edits[$ii][0];
220 86
                        $editValue = $edits[$ii][1];
221
222 86
                        if ($inArray) {
223 78
                            $editKey -= $editOffset;
224
                        }
225 86
                        if ($inArray && $editValue === null) {
226 4
                            if ($node instanceof NodeList) {
227 4
                                $node->splice($editKey, 1);
228
                            } else {
229
                                array_splice($node, $editKey, 1);
0 ignored issues
show
Bug introduced by
$node of type object|null is incompatible with the type array expected by parameter $input of array_splice(). ( Ignorable by Annotation )

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

229
                                array_splice(/** @scrutinizer ignore-type */ $node, $editKey, 1);
Loading history...
230
                            }
231 4
                            $editOffset++;
232
                        } else {
233 86
                            if ($node instanceof NodeList || is_array($node)) {
234 78
                                $node[$editKey] = $editValue;
235
                            } else {
236 86
                                $node->{$editKey} = $editValue;
237
                            }
238
                        }
239
                    }
240
                }
241 567
                $index   = $stack['index'];
242 567
                $keys    = $stack['keys'];
243 567
                $edits   = $stack['edits'];
244 567
                $inArray = $stack['inArray'];
245 567
                $stack   = $stack['prev'];
246
            } else {
247 569
                $key  = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
248 569
                $node = $parent ? (($parent instanceof NodeList || is_array($parent)) ? $parent[$key] : $parent->{$key}) : $newRoot;
249 569
                if ($node === null || $node === $UNDEFINED) {
250 549
                    continue;
251
                }
252 569
                if ($parent) {
253 553
                    $path[] = $key;
254
                }
255
            }
256
257 569
            $result = null;
258 569
            if (! $node instanceof NodeList && ! is_array($node)) {
259 569
                if (! ($node instanceof Node)) {
260 2
                    throw new \Exception('Invalid AST Node: ' . json_encode($node));
261
                }
262
263 567
                $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
264
265 567
                if ($visitFn) {
266 567
                    $result    = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
267 567
                    $editValue = null;
268
269 567
                    if ($result !== null) {
270 152
                        if ($result instanceof VisitorOperation) {
271 7
                            if ($result->doBreak) {
272 2
                                break;
273
                            }
274 5
                            if (! $isLeaving && $result->doContinue) {
275 1
                                array_pop($path);
276 1
                                continue;
277
                            }
278 4
                            if ($result->removeNode) {
279 4
                                $editValue = null;
280
                            }
281
                        } else {
282 145
                            $editValue = $result;
283
                        }
284
285 149
                        $edits[] = [$key, $editValue];
286 149
                        if (! $isLeaving) {
287 65
                            if (! ($editValue instanceof Node)) {
288 61
                                array_pop($path);
289 61
                                continue;
290
                            }
291
292 4
                            $node = $editValue;
293
                        }
294
                    }
295
                }
296
            }
297
298 567
            if ($result === null && $isEdited) {
299 78
                $edits[] = [$key, $node];
300
            }
301
302 567
            if ($isLeaving) {
303 567
                array_pop($path);
304
            } else {
305
                $stack   = [
306 567
                    'inArray' => $inArray,
307 567
                    'index'   => $index,
308 567
                    'keys'    => $keys,
309 567
                    'edits'   => $edits,
310 567
                    'prev'    => $stack,
311
                ];
312 567
                $inArray = $node instanceof NodeList || is_array($node);
313
314 567
                $keys  = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
315 567
                $index = -1;
316 567
                $edits = [];
317 567
                if ($parent) {
318 553
                    $ancestors[] = $parent;
319
                }
320 567
                $parent = $node;
321
            }
322 567
        } while ($stack);
323
324 567
        if (count($edits) !== 0) {
325 149
            $newRoot = $edits[0][1];
326
        }
327
328 567
        return $newRoot;
329
    }
330
331
    /**
332
     * Returns marker for visitor break
333
     *
334
     * @api
335
     * @return VisitorOperation
336
     */
337 6
    public static function stop()
338
    {
339 6
        $r          = new VisitorOperation();
340 6
        $r->doBreak = true;
341
342 6
        return $r;
343
    }
344
345
    /**
346
     * Returns marker for skipping current node
347
     *
348
     * @api
349
     * @return VisitorOperation
350
     */
351 179
    public static function skipNode()
352
    {
353 179
        $r             = new VisitorOperation();
354 179
        $r->doContinue = true;
355
356 179
        return $r;
357
    }
358
359
    /**
360
     * Returns marker for removing a node
361
     *
362
     * @api
363
     * @return VisitorOperation
364
     */
365 4
    public static function removeNode()
366
    {
367 4
        $r             = new VisitorOperation();
368 4
        $r->removeNode = true;
369
370 4
        return $r;
371
    }
372
373
    /**
374
     * @param callable[][] $visitors
375
     * @return callable[][]
376
     */
377 523
    public static function visitInParallel($visitors)
378
    {
379 523
        $visitorsCount = count($visitors);
380 523
        $skipping      = new \SplFixedArray($visitorsCount);
381
382
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('enter' => ...ion(...) { /* ... */ }) returns the type array<string,callable> which is incompatible with the documented return type array<mixed,array<mixed,callable>>.
Loading history...
383
            'enter' => function (Node $node) use ($visitors, $skipping, $visitorsCount) {
384 523
                for ($i = 0; $i < $visitorsCount; $i++) {
385 523
                    if (! empty($skipping[$i])) {
386 181
                        continue;
387
                    }
388
389 523
                    $fn = self::getVisitFn(
390 523
                        $visitors[$i],
391 523
                        $node->kind, /* isLeaving */
392 523
                        false
393
                    );
394
395 523
                    if (! $fn) {
396 504
                        continue;
397
                    }
398
399 463
                    $result = call_user_func_array($fn, func_get_args());
400
401 463
                    if ($result instanceof VisitorOperation) {
402 180
                        if ($result->doContinue) {
403 177
                            $skipping[$i] = $node;
404 3
                        } elseif ($result->doBreak) {
405 2
                            $skipping[$i] = $result;
406 1
                        } elseif ($result->removeNode) {
407 180
                            return $result;
408
                        }
409 418
                    } elseif ($result !== null) {
410
                        return $result;
411
                    }
412
                }
413 523
            },
414
            'leave' => function (Node $node) use ($visitors, $skipping, $visitorsCount) {
415 523
                for ($i = 0; $i < $visitorsCount; $i++) {
416 523
                    if (empty($skipping[$i])) {
417 520
                        $fn = self::getVisitFn(
418 520
                            $visitors[$i],
419 520
                            $node->kind, /* isLeaving */
420 520
                            true
421
                        );
422
423 520
                        if ($fn) {
424 239
                            $result = call_user_func_array($fn, func_get_args());
425 239
                            if ($result instanceof VisitorOperation) {
426 13
                                if ($result->doBreak) {
427 2
                                    $skipping[$i] = $result;
428 11
                                } elseif ($result->removeNode) {
429 13
                                    return $result;
430
                                }
431 239
                            } elseif ($result !== null) {
432 520
                                return $result;
433
                            }
434
                        }
435 181
                    } elseif ($skipping[$i] === $node) {
436 177
                        $skipping[$i] = null;
437
                    }
438
                }
439 523
            },
440
        ];
441
    }
442
443
    /**
444
     * Creates a new visitor instance which maintains a provided TypeInfo instance
445
     * along with visiting visitor.
446
     */
447 519
    public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
448
    {
449
        return [
450
            'enter' => function (Node $node) use ($typeInfo, $visitor) {
451 519
                $typeInfo->enter($node);
452 519
                $fn = self::getVisitFn($visitor, $node->kind, false);
453
454 519
                if ($fn) {
455 519
                    $result = call_user_func_array($fn, func_get_args());
456 519
                    if ($result) {
457 1
                        $typeInfo->leave($node);
458 1
                        if ($result instanceof Node) {
459 1
                            $typeInfo->enter($result);
460
                        }
461
                    }
462
463 519
                    return $result;
464
                }
465
466 153
                return null;
467 519
            },
468
            'leave' => function (Node $node) use ($typeInfo, $visitor) {
469 519
                $fn     = self::getVisitFn($visitor, $node->kind, true);
470 519
                $result = $fn ? call_user_func_array($fn, func_get_args()) : null;
471 519
                $typeInfo->leave($node);
472
473 519
                return $result;
474 519
            },
475
        ];
476
    }
477
478
    /**
479
     * @param callable[]|null $visitor
480
     * @param string          $kind
481
     * @param bool            $isLeaving
482
     * @return callable|null
483
     */
484 567
    public static function getVisitFn($visitor, $kind, $isLeaving)
485
    {
486 567
        if ($visitor === null) {
487 1
            return null;
488
        }
489
490 567
        $kindVisitor = $visitor[$kind] ?? null;
491
492 567
        if (! $isLeaving && is_callable($kindVisitor)) {
493
            // { Kind() {} }
494 449
            return $kindVisitor;
495
        }
496
497 567
        if (is_array($kindVisitor)) {
498 234
            if ($isLeaving) {
499 234
                $kindSpecificVisitor = $kindVisitor['leave'] ?? null;
500
            } else {
501 234
                $kindSpecificVisitor = $kindVisitor['enter'] ?? null;
502
            }
503
504 234
            if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
505
                // { Kind: { enter() {}, leave() {} } }
506 234
                return $kindSpecificVisitor;
507
            }
508
509 175
            return null;
510
        }
511
512 566
        $visitor += ['leave' => null, 'enter' => null];
513
514 566
        $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
515
516 566
        if ($specificVisitor) {
517 564
            if (is_callable($specificVisitor)) {
518
                // { enter() {}, leave() {} }
519 536
                return $specificVisitor;
520
            }
521 83
            $specificKindVisitor = $specificVisitor[$kind] ?? null;
522
523 83
            if (is_callable($specificKindVisitor)) {
524
                // { enter: { Kind() {} }, leave: { Kind() {} } }
525 83
                return $specificKindVisitor;
526
            }
527
        }
528
529 545
        return null;
530
    }
531
}
532