Visitor   F
last analyzed

Complexity

Total Complexity 82

Size/Duplication

Total Lines 440
Duplicated Lines 0 %

Test Coverage

Coverage 98.97%

Importance

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

7 Methods

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

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', 'directives'],
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 775
    public static function visit($root, $visitor, $keyMap = null)
188
    {
189 775
        $visitorKeys = $keyMap ?: self::$visitorKeys;
190
191 775
        $stack     = null;
192 775
        $inArray   = $root instanceof NodeList || is_array($root);
0 ignored issues
show
introduced by
The condition is_array($root) is always false.
Loading history...
193 775
        $keys      = [$root];
194 775
        $index     = -1;
195 775
        $edits     = [];
196 775
        $parent    = null;
197 775
        $path      = [];
198 775
        $ancestors = [];
199 775
        $newRoot   = $root;
200
201 775
        $UNDEFINED = null;
202
203
        do {
204 775
            $index++;
205 775
            $isLeaving = $index === count($keys);
206 775
            $key       = null;
207 775
            $node      = null;
208 775
            $isEdited  = $isLeaving && count($edits) !== 0;
209
210 775
            if ($isLeaving) {
211 773
                $key    = ! $ancestors ? $UNDEFINED : $path[count($path) - 1];
212 773
                $node   = $parent;
213 773
                $parent = array_pop($ancestors);
214
215 773
                if ($isEdited) {
216 146
                    if ($inArray) {
217
                        // $node = $node; // arrays are value types in PHP
218 138
                        if ($node instanceof NodeList) {
219 138
                            $node = clone $node;
220
                        }
221
                    } else {
222 146
                        $node = clone $node;
223
                    }
224 146
                    $editOffset = 0;
225 146
                    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 146
                        $editKey   = $edits[$ii][0];
227 146
                        $editValue = $edits[$ii][1];
228
229 146
                        if ($inArray) {
230 138
                            $editKey -= $editOffset;
231
                        }
232 146
                        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 146
                            if ($node instanceof NodeList || is_array($node)) {
241 138
                                $node[$editKey] = $editValue;
242
                            } else {
243 146
                                $node->{$editKey} = $editValue;
244
                            }
245
                        }
246
                    }
247
                }
248 773
                $index   = $stack['index'];
249 773
                $keys    = $stack['keys'];
250 773
                $edits   = $stack['edits'];
251 773
                $inArray = $stack['inArray'];
252 773
                $stack   = $stack['prev'];
253
            } else {
254 775
                $key  = $parent !== null
255 768
                    ? ($inArray
256 763
                        ? $index
257 768
                        : $keys[$index]
258
                    )
259 775
                    : $UNDEFINED;
260 775
                $node = $parent !== null
261 768
                    ? ($parent instanceof NodeList || is_array($parent)
262 763
                        ? $parent[$key]
263 768
                        : $parent->{$key}
264
                    )
265 775
                    : $newRoot;
266 775
                if ($node === null || $node === $UNDEFINED) {
267 764
                    continue;
268
                }
269 775
                if ($parent !== null) {
270 768
                    $path[] = $key;
271
                }
272
            }
273
274 775
            $result = null;
275 775
            if (! $node instanceof NodeList && ! is_array($node)) {
276 775
                if (! ($node instanceof Node)) {
277 2
                    throw new Exception('Invalid AST Node: ' . json_encode($node));
278
                }
279
280 773
                $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
281
282 773
                if ($visitFn) {
283 773
                    $result    = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
284 773
                    $editValue = null;
285
286 773
                    if ($result !== null) {
287 214
                        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 207
                            $editValue = $result;
300
                        }
301
302 211
                        $edits[] = [$key, $editValue];
303 211
                        if (! $isLeaving) {
304 68
                            if (! ($editValue instanceof Node)) {
305 64
                                array_pop($path);
306 64
                                continue;
307
                            }
308
309 4
                            $node = $editValue;
310
                        }
311
                    }
312
                }
313
            }
314
315 773
            if ($result === null && $isEdited) {
316 138
                $edits[] = [$key, $node];
317
            }
318
319 773
            if ($isLeaving) {
320 773
                array_pop($path);
321
            } else {
322
                $stack   = [
323 773
                    'inArray' => $inArray,
324 773
                    'index'   => $index,
325 773
                    'keys'    => $keys,
326 773
                    'edits'   => $edits,
327 773
                    'prev'    => $stack,
328
                ];
329 773
                $inArray = $node instanceof NodeList || is_array($node);
330
331 773
                $keys  = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
332 773
                $index = -1;
333 773
                $edits = [];
334 773
                if ($parent !== null) {
335 768
                    $ancestors[] = $parent;
336
                }
337 773
                $parent = $node;
338
            }
339 773
        } while ($stack);
340
341 773
        if (count($edits) !== 0) {
342 211
            $newRoot = $edits[0][1];
343
        }
344
345 773
        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 193
    public static function skipNode()
371
    {
372 193
        $r             = new VisitorOperation();
373 193
        $r->doContinue = true;
374
375 193
        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 array<string, callable>
397
     */
398 735
    public static function visitInParallel($visitors)
399
    {
400 735
        $visitorsCount = count($visitors);
401 735
        $skipping      = new SplFixedArray($visitorsCount);
402
403
        return [
404
            'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
405 735
                for ($i = 0; $i < $visitorsCount; $i++) {
406 735
                    if (! empty($skipping[$i])) {
407 195
                        continue;
408
                    }
409
410 735
                    $fn = self::getVisitFn(
411 735
                        $visitors[$i],
412 735
                        $node->kind, /* isLeaving */
413 735
                        false
414
                    );
415
416 735
                    if (! $fn) {
417 715
                        continue;
418
                    }
419
420 677
                    $result = call_user_func_array($fn, func_get_args());
421
422 677
                    if ($result instanceof VisitorOperation) {
423 194
                        if ($result->doContinue) {
424 191
                            $skipping[$i] = $node;
425 3
                        } elseif ($result->doBreak) {
426 2
                            $skipping[$i] = $result;
427 1
                        } elseif ($result->removeNode) {
428 194
                            return $result;
429
                        }
430 638
                    } elseif ($result !== null) {
431
                        return $result;
432
                    }
433
                }
434 735
            },
435
            'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
436 735
                for ($i = 0; $i < $visitorsCount; $i++) {
437 735
                    if (empty($skipping[$i])) {
438 732
                        $fn = self::getVisitFn(
439 732
                            $visitors[$i],
440 732
                            $node->kind, /* isLeaving */
441 732
                            true
442
                        );
443
444 732
                        if ($fn) {
445 260
                            $result = call_user_func_array($fn, func_get_args());
446 260
                            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 260
                            } elseif ($result !== null) {
453 732
                                return $result;
454
                            }
455
                        }
456 195
                    } elseif ($skipping[$i] === $node) {
457 191
                        $skipping[$i] = null;
458
                    }
459
                }
460 735
            },
461
        ];
462
    }
463
464
    /**
465
     * Creates a new visitor instance which maintains a provided TypeInfo instance
466
     * along with visiting visitor.
467
     */
468 535
    public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
469
    {
470
        return [
471
            'enter' => static function (Node $node) use ($typeInfo, $visitor) {
472 535
                $typeInfo->enter($node);
473 535
                $fn = self::getVisitFn($visitor, $node->kind, false);
474
475 535
                if ($fn) {
476 535
                    $result = call_user_func_array($fn, func_get_args());
477 535
                    if ($result !== null) {
478 63
                        $typeInfo->leave($node);
479 63
                        if ($result instanceof Node) {
480 1
                            $typeInfo->enter($result);
481
                        }
482
                    }
483
484 535
                    return $result;
485
                }
486
487 170
                return null;
488 535
            },
489
            'leave' => static function (Node $node) use ($typeInfo, $visitor) {
490 535
                $fn     = self::getVisitFn($visitor, $node->kind, true);
491 535
                $result = $fn ? call_user_func_array($fn, func_get_args()) : null;
492 535
                $typeInfo->leave($node);
493
494 535
                return $result;
495 535
            },
496
        ];
497
    }
498
499
    /**
500
     * @param callable[]|null $visitor
501
     * @param string          $kind
502
     * @param bool            $isLeaving
503
     *
504
     * @return callable|null
505
     */
506 773
    public static function getVisitFn($visitor, $kind, $isLeaving)
507
    {
508 773
        if ($visitor === null) {
509 1
            return null;
510
        }
511
512 773
        $kindVisitor = $visitor[$kind] ?? null;
513
514 773
        if (! $isLeaving && is_callable($kindVisitor)) {
515
            // { Kind() {} }
516 521
            return $kindVisitor;
517
        }
518
519 773
        if (is_array($kindVisitor)) {
520 332
            if ($isLeaving) {
521 332
                $kindSpecificVisitor = $kindVisitor['leave'] ?? null;
522
            } else {
523 332
                $kindSpecificVisitor = $kindVisitor['enter'] ?? null;
524
            }
525
526 332
            if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
527
                // { Kind: { enter() {}, leave() {} } }
528 332
                return $kindSpecificVisitor;
529
            }
530
531 271
            return null;
532
        }
533
534 772
        $visitor += ['leave' => null, 'enter' => null];
535
536 772
        $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
537
538 772
        if ($specificVisitor) {
539 770
            if (is_callable($specificVisitor)) {
540
                // { enter() {}, leave() {} }
541 749
                return $specificVisitor;
542
            }
543 142
            $specificKindVisitor = $specificVisitor[$kind] ?? null;
544
545 142
            if (is_callable($specificKindVisitor)) {
546
                // { enter: { Kind() {} }, leave: { Kind() {} } }
547 142
                return $specificKindVisitor;
548
            }
549
        }
550
551 750
        return null;
552
    }
553
}
554