Completed
Push — master ( 93ccd7...3b27ab )
by Vladimir
27s queued 13s
created

Visitor   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Test Coverage

Coverage 98.97%

Importance

Changes 0
Metric Value
wmc 82
eloc 249
dl 0
loc 440
ccs 193
cts 195
cp 0.9897
rs 2
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
C visitInParallel() 0 60 17
A visitWithTypeInfo() 0 27 5
A skipNode() 0 6 1
F visit() 0 159 45
A removeNode() 0 6 1
A stop() 0 6 1
C getVisitFn() 0 46 12

How to fix   Complexity   

Complex Class

Complex classes like Visitor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Visitor, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace GraphQL\Language;
6
7
use ArrayObject;
8
use Exception;
9
use GraphQL\Language\AST\Node;
10
use GraphQL\Language\AST\NodeKind;
11
use GraphQL\Language\AST\NodeList;
12
use GraphQL\Utils\TypeInfo;
13
use SplFixedArray;
14
use stdClass;
15
use function array_pop;
16
use function array_splice;
17
use function call_user_func;
18
use function call_user_func_array;
19
use function count;
20
use function func_get_args;
21
use function is_array;
22
use function is_callable;
23
use function json_encode;
24
25
/**
26
 * Utility for efficient AST traversal and modification.
27
 *
28
 * `visit()` will walk through an AST using a depth first traversal, calling
29
 * the visitor's enter function at each node in the traversal, and calling the
30
 * leave function after visiting that node and all of it's child nodes.
31
 *
32
 * By returning different values from the enter and leave functions, the
33
 * behavior of the visitor can be altered, including skipping over a sub-tree of
34
 * the AST (by returning false), editing the AST by returning a value or null
35
 * to remove the value, or to stop the whole traversal by returning BREAK.
36
 *
37
 * When using `visit()` to edit an AST, the original AST will not be modified, and
38
 * a new version of the AST with the changes applied will be returned from the
39
 * visit function.
40
 *
41
 *     $editedAST = Visitor::visit($ast, [
42
 *       'enter' => function ($node, $key, $parent, $path, $ancestors) {
43
 *         // return
44
 *         //   null: no action
45
 *         //   Visitor::skipNode(): skip visiting this node
46
 *         //   Visitor::stop(): stop visiting altogether
47
 *         //   Visitor::removeNode(): delete this node
48
 *         //   any value: replace this node with the returned value
49
 *       },
50
 *       'leave' => function ($node, $key, $parent, $path, $ancestors) {
51
 *         // return
52
 *         //   null: no action
53
 *         //   Visitor::stop(): stop visiting altogether
54
 *         //   Visitor::removeNode(): delete this node
55
 *         //   any value: replace this node with the returned value
56
 *       }
57
 *     ]);
58
 *
59
 * Alternatively to providing enter() and leave() functions, a visitor can
60
 * instead provide functions named the same as the [kinds of AST nodes](reference.md#graphqllanguageastnodekind),
61
 * or enter/leave visitors at a named key, leading to four permutations of
62
 * visitor API:
63
 *
64
 * 1) Named visitors triggered when entering a node a specific kind.
65
 *
66
 *     Visitor::visit($ast, [
67
 *       'Kind' => function ($node) {
68
 *         // enter the "Kind" node
69
 *       }
70
 *     ]);
71
 *
72
 * 2) Named visitors that trigger upon entering and leaving a node of
73
 *    a specific kind.
74
 *
75
 *     Visitor::visit($ast, [
76
 *       'Kind' => [
77
 *         'enter' => function ($node) {
78
 *           // enter the "Kind" node
79
 *         }
80
 *         'leave' => function ($node) {
81
 *           // leave the "Kind" node
82
 *         }
83
 *       ]
84
 *     ]);
85
 *
86
 * 3) Generic visitors that trigger upon entering and leaving any node.
87
 *
88
 *     Visitor::visit($ast, [
89
 *       'enter' => function ($node) {
90
 *         // enter any node
91
 *       },
92
 *       'leave' => function ($node) {
93
 *         // leave any node
94
 *       }
95
 *     ]);
96
 *
97
 * 4) Parallel visitors for entering and leaving nodes of a specific kind.
98
 *
99
 *     Visitor::visit($ast, [
100
 *       'enter' => [
101
 *         'Kind' => function($node) {
102
 *           // enter the "Kind" node
103
 *         }
104
 *       },
105
 *       'leave' => [
106
 *         'Kind' => function ($node) {
107
 *           // leave the "Kind" node
108
 *         }
109
 *       ]
110
 *     ]);
111
 */
112
class Visitor
113
{
114
    /** @var string[][] */
115
    public static $visitorKeys = [
116
        NodeKind::NAME                 => [],
117
        NodeKind::DOCUMENT             => ['definitions'],
118
        NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
119
        NodeKind::VARIABLE_DEFINITION  => ['variable', 'type', 'defaultValue'],
120
        NodeKind::VARIABLE             => ['name'],
121
        NodeKind::SELECTION_SET        => ['selections'],
122
        NodeKind::FIELD                => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
123
        NodeKind::ARGUMENT             => ['name', 'value'],
124
        NodeKind::FRAGMENT_SPREAD      => ['name', 'directives'],
125
        NodeKind::INLINE_FRAGMENT      => ['typeCondition', 'directives', 'selectionSet'],
126
        NodeKind::FRAGMENT_DEFINITION  => [
127
            'name',
128
            // Note: fragment variable definitions are experimental and may be changed
129
            // or removed in the future.
130
            'variableDefinitions',
131
            'typeCondition',
132
            'directives',
133
            'selectionSet',
134
        ],
135
136
        NodeKind::INT           => [],
137
        NodeKind::FLOAT         => [],
138
        NodeKind::STRING        => [],
139
        NodeKind::BOOLEAN       => [],
140
        NodeKind::NULL          => [],
141
        NodeKind::ENUM          => [],
142
        NodeKind::LST           => ['values'],
143
        NodeKind::OBJECT        => ['fields'],
144
        NodeKind::OBJECT_FIELD  => ['name', 'value'],
145
        NodeKind::DIRECTIVE     => ['name', 'arguments'],
146
        NodeKind::NAMED_TYPE    => ['name'],
147
        NodeKind::LIST_TYPE     => ['type'],
148
        NodeKind::NON_NULL_TYPE => ['type'],
149
150
        NodeKind::SCHEMA_DEFINITION            => ['directives', 'operationTypes'],
151
        NodeKind::OPERATION_TYPE_DEFINITION    => ['type'],
152
        NodeKind::SCALAR_TYPE_DEFINITION       => ['description', 'name', 'directives'],
153
        NodeKind::OBJECT_TYPE_DEFINITION       => ['description', 'name', 'interfaces', 'directives', 'fields'],
154
        NodeKind::FIELD_DEFINITION             => ['description', 'name', 'arguments', 'type', 'directives'],
155
        NodeKind::INPUT_VALUE_DEFINITION       => ['description', 'name', 'type', 'defaultValue', 'directives'],
156
        NodeKind::INTERFACE_TYPE_DEFINITION    => ['description', 'name', 'directives', 'fields'],
157
        NodeKind::UNION_TYPE_DEFINITION        => ['description', 'name', 'directives', 'types'],
158
        NodeKind::ENUM_TYPE_DEFINITION         => ['description', 'name', 'directives', 'values'],
159
        NodeKind::ENUM_VALUE_DEFINITION        => ['description', 'name', 'directives'],
160
        NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
161
162
        NodeKind::SCALAR_TYPE_EXTENSION       => ['name', 'directives'],
163
        NodeKind::OBJECT_TYPE_EXTENSION       => ['name', 'interfaces', 'directives', 'fields'],
164
        NodeKind::INTERFACE_TYPE_EXTENSION    => ['name', 'directives', 'fields'],
165
        NodeKind::UNION_TYPE_EXTENSION        => ['name', 'directives', 'types'],
166
        NodeKind::ENUM_TYPE_EXTENSION         => ['name', 'directives', 'values'],
167
        NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
168
169
        NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
170
171
        NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'],
172
    ];
173
174
    /**
175
     * Visit the AST (see class description for details)
176
     *
177
     * @param Node|ArrayObject|stdClass $root
178
     * @param callable[]                $visitor
179
     * @param mixed[]|null              $keyMap
180
     *
181
     * @return Node|mixed
182
     *
183
     * @throws Exception
184
     *
185
     * @api
186
     */
187 632
    public static function visit($root, $visitor, $keyMap = null)
188
    {
189 632
        $visitorKeys = $keyMap ?: self::$visitorKeys;
190
191 632
        $stack     = null;
192 632
        $inArray   = $root instanceof NodeList || is_array($root);
0 ignored issues
show
introduced by
The condition is_array($root) is always false.
Loading history...
193 632
        $keys      = [$root];
194 632
        $index     = -1;
195 632
        $edits     = [];
196 632
        $parent    = null;
197 632
        $path      = [];
198 632
        $ancestors = [];
199 632
        $newRoot   = $root;
200
201 632
        $UNDEFINED = null;
202
203
        do {
204 632
            $index++;
205 632
            $isLeaving = $index === count($keys);
206 632
            $key       = null;
207 632
            $node      = null;
208 632
            $isEdited  = $isLeaving && count($edits) !== 0;
209
210 632
            if ($isLeaving) {
211 630
                $key    = ! $ancestors ? $UNDEFINED : $path[count($path) - 1];
212 630
                $node   = $parent;
213 630
                $parent = array_pop($ancestors);
214
215 630
                if ($isEdited) {
216 142
                    if ($inArray) {
217
                        // $node = $node; // arrays are value types in PHP
218 134
                        if ($node instanceof NodeList) {
219 134
                            $node = clone $node;
220
                        }
221
                    } else {
222 142
                        $node = clone $node;
223
                    }
224 142
                    $editOffset = 0;
225 142
                    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...
226 142
                        $editKey   = $edits[$ii][0];
227 142
                        $editValue = $edits[$ii][1];
228
229 142
                        if ($inArray) {
230 134
                            $editKey -= $editOffset;
231
                        }
232 142
                        if ($inArray && $editValue === null) {
233 4
                            if ($node instanceof NodeList) {
234 4
                                $node->splice($editKey, 1);
235
                            } else {
236
                                array_splice($node, $editKey, 1);
237
                            }
238 4
                            $editOffset++;
239
                        } else {
240 142
                            if ($node instanceof NodeList || is_array($node)) {
241 134
                                $node[$editKey] = $editValue;
242
                            } else {
243 142
                                $node->{$editKey} = $editValue;
244
                            }
245
                        }
246
                    }
247
                }
248 630
                $index   = $stack['index'];
249 630
                $keys    = $stack['keys'];
250 630
                $edits   = $stack['edits'];
251 630
                $inArray = $stack['inArray'];
252 630
                $stack   = $stack['prev'];
253
            } else {
254 632
                $key  = $parent !== null
255 616
                    ? ($inArray
256 611
                        ? $index
257 616
                        : $keys[$index]
258
                    )
259 632
                    : $UNDEFINED;
260 632
                $node = $parent !== null
261 616
                    ? ($parent instanceof NodeList || is_array($parent)
262 611
                        ? $parent[$key]
263 616
                        : $parent->{$key}
264
                    )
265 632
                    : $newRoot;
266 632
                if ($node === null || $node === $UNDEFINED) {
267 612
                    continue;
268
                }
269 632
                if ($parent !== null) {
270 616
                    $path[] = $key;
271
                }
272
            }
273
274 632
            $result = null;
275 632
            if (! $node instanceof NodeList && ! is_array($node)) {
276 632
                if (! ($node instanceof Node)) {
277 2
                    throw new Exception('Invalid AST Node: ' . json_encode($node));
278
                }
279
280 630
                $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
281
282 630
                if ($visitFn) {
283 630
                    $result    = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
284 630
                    $editValue = null;
285
286 630
                    if ($result !== null) {
287 208
                        if ($result instanceof VisitorOperation) {
288 7
                            if ($result->doBreak) {
289 2
                                break;
290
                            }
291 5
                            if (! $isLeaving && $result->doContinue) {
292 1
                                array_pop($path);
293 1
                                continue;
294
                            }
295 4
                            if ($result->removeNode) {
296 4
                                $editValue = null;
297
                            }
298
                        } else {
299 201
                            $editValue = $result;
300
                        }
301
302 205
                        $edits[] = [$key, $editValue];
303 205
                        if (! $isLeaving) {
304 66
                            if (! ($editValue instanceof Node)) {
305 62
                                array_pop($path);
306 62
                                continue;
307
                            }
308
309 4
                            $node = $editValue;
310
                        }
311
                    }
312
                }
313
            }
314
315 630
            if ($result === null && $isEdited) {
316 134
                $edits[] = [$key, $node];
317
            }
318
319 630
            if ($isLeaving) {
320 630
                array_pop($path);
321
            } else {
322
                $stack   = [
323 630
                    'inArray' => $inArray,
324 630
                    'index'   => $index,
325 630
                    'keys'    => $keys,
326 630
                    'edits'   => $edits,
327 630
                    'prev'    => $stack,
328
                ];
329 630
                $inArray = $node instanceof NodeList || is_array($node);
330
331 630
                $keys  = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
332 630
                $index = -1;
333 630
                $edits = [];
334 630
                if ($parent !== null) {
335 616
                    $ancestors[] = $parent;
336
                }
337 630
                $parent = $node;
338
            }
339 630
        } while ($stack);
340
341 630
        if (count($edits) !== 0) {
342 205
            $newRoot = $edits[0][1];
343
        }
344
345 630
        return $newRoot;
346
    }
347
348
    /**
349
     * Returns marker for visitor break
350
     *
351
     * @return VisitorOperation
352
     *
353
     * @api
354
     */
355 6
    public static function stop()
356
    {
357 6
        $r          = new VisitorOperation();
358 6
        $r->doBreak = true;
359
360 6
        return $r;
361
    }
362
363
    /**
364
     * Returns marker for skipping current node
365
     *
366
     * @return VisitorOperation
367
     *
368
     * @api
369
     */
370 192
    public static function skipNode()
371
    {
372 192
        $r             = new VisitorOperation();
373 192
        $r->doContinue = true;
374
375 192
        return $r;
376
    }
377
378
    /**
379
     * Returns marker for removing a node
380
     *
381
     * @return VisitorOperation
382
     *
383
     * @api
384
     */
385 4
    public static function removeNode()
386
    {
387 4
        $r             = new VisitorOperation();
388 4
        $r->removeNode = true;
389
390 4
        return $r;
391
    }
392
393
    /**
394
     * @param callable[][] $visitors
395
     *
396
     * @return callable[][]
397
     */
398 585
    public static function visitInParallel($visitors)
399
    {
400 585
        $visitorsCount = count($visitors);
401 585
        $skipping      = new SplFixedArray($visitorsCount);
402
403
        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...
404
            'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
405 585
                for ($i = 0; $i < $visitorsCount; $i++) {
406 585
                    if (! empty($skipping[$i])) {
407 194
                        continue;
408
                    }
409
410 585
                    $fn = self::getVisitFn(
411 585
                        $visitors[$i],
412 585
                        $node->kind, /* isLeaving */
413 585
                        false
414
                    );
415
416 585
                    if (! $fn) {
417 566
                        continue;
418
                    }
419
420 528
                    $result = call_user_func_array($fn, func_get_args());
421
422 528
                    if ($result instanceof VisitorOperation) {
423 193
                        if ($result->doContinue) {
424 190
                            $skipping[$i] = $node;
425 3
                        } elseif ($result->doBreak) {
426 2
                            $skipping[$i] = $result;
427 1
                        } elseif ($result->removeNode) {
428 193
                            return $result;
429
                        }
430 483
                    } elseif ($result !== null) {
431
                        return $result;
432
                    }
433
                }
434 585
            },
435
            'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
436 585
                for ($i = 0; $i < $visitorsCount; $i++) {
437 585
                    if (empty($skipping[$i])) {
438 582
                        $fn = self::getVisitFn(
439 582
                            $visitors[$i],
440 582
                            $node->kind, /* isLeaving */
441 582
                            true
442
                        );
443
444 582
                        if ($fn) {
445 249
                            $result = call_user_func_array($fn, func_get_args());
446 249
                            if ($result instanceof VisitorOperation) {
447 13
                                if ($result->doBreak) {
448 2
                                    $skipping[$i] = $result;
449 11
                                } elseif ($result->removeNode) {
450 13
                                    return $result;
451
                                }
452 249
                            } elseif ($result !== null) {
453 582
                                return $result;
454
                            }
455
                        }
456 194
                    } elseif ($skipping[$i] === $node) {
457 190
                        $skipping[$i] = null;
458
                    }
459
                }
460 585
            },
461
        ];
462
    }
463
464
    /**
465
     * Creates a new visitor instance which maintains a provided TypeInfo instance
466
     * along with visiting visitor.
467
     */
468 581
    public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
469
    {
470
        return [
471
            'enter' => static function (Node $node) use ($typeInfo, $visitor) {
472 581
                $typeInfo->enter($node);
473 581
                $fn = self::getVisitFn($visitor, $node->kind, false);
474
475 581
                if ($fn) {
476 581
                    $result = call_user_func_array($fn, func_get_args());
477 581
                    if ($result !== null) {
478 61
                        $typeInfo->leave($node);
479 61
                        if ($result instanceof Node) {
480 1
                            $typeInfo->enter($result);
481
                        }
482
                    }
483
484 581
                    return $result;
485
                }
486
487 163
                return null;
488 581
            },
489
            'leave' => static function (Node $node) use ($typeInfo, $visitor) {
490 581
                $fn     = self::getVisitFn($visitor, $node->kind, true);
491 581
                $result = $fn ? call_user_func_array($fn, func_get_args()) : null;
492 581
                $typeInfo->leave($node);
493
494 581
                return $result;
495 581
            },
496
        ];
497
    }
498
499
    /**
500
     * @param callable[]|null $visitor
501
     * @param string          $kind
502
     * @param bool            $isLeaving
503
     *
504
     * @return callable|null
505
     */
506 630
    public static function getVisitFn($visitor, $kind, $isLeaving)
507
    {
508 630
        if ($visitor === null) {
509 1
            return null;
510
        }
511
512 630
        $kindVisitor = $visitor[$kind] ?? null;
513
514 630
        if (! $isLeaving && is_callable($kindVisitor)) {
515
            // { Kind() {} }
516 470
            return $kindVisitor;
517
        }
518
519 630
        if (is_array($kindVisitor)) {
520 320
            if ($isLeaving) {
521 320
                $kindSpecificVisitor = $kindVisitor['leave'] ?? null;
522
            } else {
523 320
                $kindSpecificVisitor = $kindVisitor['enter'] ?? null;
524
            }
525
526 320
            if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
527
                // { Kind: { enter() {}, leave() {} } }
528 320
                return $kindSpecificVisitor;
529
            }
530
531 261
            return null;
532
        }
533
534 629
        $visitor += ['leave' => null, 'enter' => null];
535
536 629
        $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
537
538 629
        if ($specificVisitor) {
539 627
            if (is_callable($specificVisitor)) {
540
                // { enter() {}, leave() {} }
541 598
                return $specificVisitor;
542
            }
543 138
            $specificKindVisitor = $specificVisitor[$kind] ?? null;
544
545 138
            if (is_callable($specificKindVisitor)) {
546
                // { enter: { Kind() {} }, leave: { Kind() {} } }
547 138
                return $specificKindVisitor;
548
            }
549
        }
550
551 608
        return null;
552
    }
553
}
554