Failed Conditions
Push — master ( cc39b3...a3ef1b )
by Šimon
10s
created

Visitor::visitInParallel()   C

Complexity

Conditions 17
Paths 1

Size

Total Lines 60
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 17.0045

Importance

Changes 0
Metric Value
eloc 41
dl 0
loc 60
ccs 39
cts 40
cp 0.975
rs 5.2166
c 0
b 0
f 0
cc 17
nc 1
nop 1
crap 17.0045

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 564
    public static function visit($root, $visitor, $keyMap = null)
181
    {
182 564
        $visitorKeys = $keyMap ?: self::$visitorKeys;
183
184 564
        $stack     = null;
185 564
        $inArray   = $root instanceof NodeList || is_array($root);
0 ignored issues
show
introduced by
The condition is_array($root) is always false.
Loading history...
186 564
        $keys      = [$root];
187 564
        $index     = -1;
188 564
        $edits     = [];
189 564
        $parent    = null;
190 564
        $path      = [];
191 564
        $ancestors = [];
192 564
        $newRoot   = $root;
193
194 564
        $UNDEFINED = null;
195
196
        do {
197 564
            $index++;
198 564
            $isLeaving = $index === count($keys);
199 564
            $key       = null;
200 564
            $node      = null;
201 564
            $isEdited  = $isLeaving && count($edits) !== 0;
202
203 564
            if ($isLeaving) {
204 562
                $key    = ! $ancestors ? $UNDEFINED : $path[count($path) - 1];
205 562
                $node   = $parent;
206 562
                $parent = array_pop($ancestors);
207
208 562
                if ($isEdited) {
209 84
                    if ($inArray) {
210
                        // $node = $node; // arrays are value types in PHP
211 76
                        if ($node instanceof NodeList) {
212 76
                            $node = clone $node;
213
                        }
214
                    } else {
215 84
                        $node = clone $node;
216
                    }
217 84
                    $editOffset = 0;
218 84
                    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 84
                        $editKey   = $edits[$ii][0];
220 84
                        $editValue = $edits[$ii][1];
221
222 84
                        if ($inArray) {
223 76
                            $editKey -= $editOffset;
224
                        }
225 84
                        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 84
                            if ($node instanceof NodeList || is_array($node)) {
234 76
                                $node[$editKey] = $editValue;
235
                            } else {
236 84
                                $node->{$editKey} = $editValue;
237
                            }
238
                        }
239
                    }
240
                }
241 562
                $index   = $stack['index'];
242 562
                $keys    = $stack['keys'];
243 562
                $edits   = $stack['edits'];
244 562
                $inArray = $stack['inArray'];
245 562
                $stack   = $stack['prev'];
246
            } else {
247 564
                $key  = $parent ? ($inArray ? $index : $keys[$index]) : $UNDEFINED;
248 564
                $node = $parent ? (($parent instanceof NodeList || is_array($parent)) ? $parent[$key] : $parent->{$key}) : $newRoot;
249 564
                if ($node === null || $node === $UNDEFINED) {
250 545
                    continue;
251
                }
252 564
                if ($parent) {
253 549
                    $path[] = $key;
254
                }
255
            }
256
257 564
            $result = null;
258 564
            if (! $node instanceof NodeList && ! is_array($node)) {
259 564
                if (! ($node instanceof Node)) {
260 2
                    throw new \Exception('Invalid AST Node: ' . json_encode($node));
261
                }
262
263 562
                $visitFn = self::getVisitFn($visitor, $node->kind, $isLeaving);
264
265 562
                if ($visitFn) {
266 562
                    $result = call_user_func($visitFn, $node, $key, $parent, $path, $ancestors);
267
268 562
                    if ($result !== null) {
269 149
                        if ($result instanceof VisitorOperation) {
270 7
                            if ($result->doBreak) {
271 2
                                break;
272
                            }
273 5
                            if (! $isLeaving && $result->doContinue) {
274 1
                                array_pop($path);
275 1
                                continue;
276
                            }
277 4
                            if ($result->removeNode) {
278 4
                                $editValue = null;
279
                            }
280
                        } else {
281 142
                            $editValue = $result;
282
                        }
283
284 146
                        $edits[] = [$key, $editValue];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $editValue does not seem to be defined for all execution paths leading up to this point.
Loading history...
285 146
                        if (! $isLeaving) {
286 64
                            if (! ($editValue instanceof Node)) {
287 60
                                array_pop($path);
288 60
                                continue;
289
                            }
290
291 4
                            $node = $editValue;
292
                        }
293
                    }
294
                }
295
            }
296
297 562
            if ($result === null && $isEdited) {
298 76
                $edits[] = [$key, $node];
299
            }
300
301 562
            if ($isLeaving) {
302 562
                array_pop($path);
303
            } else {
304
                $stack   = [
305 562
                    'inArray' => $inArray,
306 562
                    'index'   => $index,
307 562
                    'keys'    => $keys,
308 562
                    'edits'   => $edits,
309 562
                    'prev'    => $stack,
310
                ];
311 562
                $inArray = $node instanceof NodeList || is_array($node);
312
313 562
                $keys  = ($inArray ? $node : $visitorKeys[$node->kind]) ?: [];
0 ignored issues
show
Bug introduced by
The property kind does not seem to exist on GraphQL\Language\AST\NodeList.
Loading history...
314 562
                $index = -1;
315 562
                $edits = [];
316 562
                if ($parent) {
317 549
                    $ancestors[] = $parent;
318
                }
319 562
                $parent = $node;
320
            }
321 562
        } while ($stack);
322
323 562
        if (count($edits) !== 0) {
324 146
            $newRoot = $edits[0][1];
325
        }
326
327 562
        return $newRoot;
328
    }
329
330
    /**
331
     * Returns marker for visitor break
332
     *
333
     * @api
334
     * @return VisitorOperation
335
     */
336 6
    public static function stop()
337
    {
338 6
        $r          = new VisitorOperation();
339 6
        $r->doBreak = true;
340
341 6
        return $r;
342
    }
343
344
    /**
345
     * Returns marker for skipping current node
346
     *
347
     * @api
348
     * @return VisitorOperation
349
     */
350 177
    public static function skipNode()
351
    {
352 177
        $r             = new VisitorOperation();
353 177
        $r->doContinue = true;
354
355 177
        return $r;
356
    }
357
358
    /**
359
     * Returns marker for removing a node
360
     *
361
     * @api
362
     * @return VisitorOperation
363
     */
364 4
    public static function removeNode()
365
    {
366 4
        $r             = new VisitorOperation();
367 4
        $r->removeNode = true;
368
369 4
        return $r;
370
    }
371
372
    /**
373
     * @param callable[][] $visitors
374
     * @return callable[][]
375
     */
376 520
    public static function visitInParallel($visitors)
377
    {
378 520
        $visitorsCount = count($visitors);
379 520
        $skipping      = new \SplFixedArray($visitorsCount);
380
381
        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...
382
            'enter' => function (Node $node) use ($visitors, $skipping, $visitorsCount) {
383 520
                for ($i = 0; $i < $visitorsCount; $i++) {
384 520
                    if (! empty($skipping[$i])) {
385 179
                        continue;
386
                    }
387
388 520
                    $fn = self::getVisitFn(
389 520
                        $visitors[$i],
390 520
                        $node->kind, /* isLeaving */
391 520
                        false
392
                    );
393
394 520
                    if (! $fn) {
395 501
                        continue;
396
                    }
397
398 460
                    $result = call_user_func_array($fn, func_get_args());
399
400 460
                    if ($result instanceof VisitorOperation) {
401 178
                        if ($result->doContinue) {
402 175
                            $skipping[$i] = $node;
403 3
                        } elseif ($result->doBreak) {
404 2
                            $skipping[$i] = $result;
405 1
                        } elseif ($result->removeNode) {
406 178
                            return $result;
407
                        }
408 415
                    } elseif ($result !== null) {
409
                        return $result;
410
                    }
411
                }
412 520
            },
413
            'leave' => function (Node $node) use ($visitors, $skipping, $visitorsCount) {
414 520
                for ($i = 0; $i < $visitorsCount; $i++) {
415 520
                    if (empty($skipping[$i])) {
416 517
                        $fn = self::getVisitFn(
417 517
                            $visitors[$i],
418 517
                            $node->kind, /* isLeaving */
419 517
                            true
420
                        );
421
422 517
                        if ($fn) {
423 236
                            $result = call_user_func_array($fn, func_get_args());
424 236
                            if ($result instanceof VisitorOperation) {
425 12
                                if ($result->doBreak) {
426 2
                                    $skipping[$i] = $result;
427 10
                                } elseif ($result->removeNode) {
428 12
                                    return $result;
429
                                }
430 236
                            } elseif ($result !== null) {
431 517
                                return $result;
432
                            }
433
                        }
434 179
                    } elseif ($skipping[$i] === $node) {
435 175
                        $skipping[$i] = null;
436
                    }
437
                }
438 520
            },
439
        ];
440
    }
441
442
    /**
443
     * Creates a new visitor instance which maintains a provided TypeInfo instance
444
     * along with visiting visitor.
445
     */
446 516
    public static function visitWithTypeInfo(TypeInfo $typeInfo, $visitor)
447
    {
448
        return [
449
            'enter' => function (Node $node) use ($typeInfo, $visitor) {
450 516
                $typeInfo->enter($node);
451 516
                $fn = self::getVisitFn($visitor, $node->kind, false);
452
453 516
                if ($fn) {
454 516
                    $result = call_user_func_array($fn, func_get_args());
455 516
                    if ($result) {
456 1
                        $typeInfo->leave($node);
457 1
                        if ($result instanceof Node) {
458 1
                            $typeInfo->enter($result);
459
                        }
460
                    }
461
462 516
                    return $result;
463
                }
464
465 151
                return null;
466 516
            },
467
            'leave' => function (Node $node) use ($typeInfo, $visitor) {
468 516
                $fn     = self::getVisitFn($visitor, $node->kind, true);
469 516
                $result = $fn ? call_user_func_array($fn, func_get_args()) : null;
470 516
                $typeInfo->leave($node);
471
472 516
                return $result;
473 516
            },
474
        ];
475
    }
476
477
    /**
478
     * @param callable[]|null $visitor
479
     * @param string          $kind
480
     * @param bool            $isLeaving
481
     * @return callable|null
482
     */
483 562
    public static function getVisitFn($visitor, $kind, $isLeaving)
484
    {
485 562
        if ($visitor === null) {
486 1
            return null;
487
        }
488
489 562
        $kindVisitor = $visitor[$kind] ?? null;
490
491 562
        if (! $isLeaving && is_callable($kindVisitor)) {
492
            // { Kind() {} }
493 446
            return $kindVisitor;
494
        }
495
496 562
        if (is_array($kindVisitor)) {
497 231
            if ($isLeaving) {
498 231
                $kindSpecificVisitor = $kindVisitor['leave'] ?? null;
499
            } else {
500 231
                $kindSpecificVisitor = $kindVisitor['enter'] ?? null;
501
            }
502
503 231
            if ($kindSpecificVisitor && is_callable($kindSpecificVisitor)) {
504
                // { Kind: { enter() {}, leave() {} } }
505 231
                return $kindSpecificVisitor;
506
            }
507
508 173
            return null;
509
        }
510
511 561
        $visitor += ['leave' => null, 'enter' => null];
512
513 561
        $specificVisitor = $isLeaving ? $visitor['leave'] : $visitor['enter'];
514
515 561
        if ($specificVisitor) {
516 559
            if (is_callable($specificVisitor)) {
517
                // { enter() {}, leave() {} }
518 533
                return $specificVisitor;
519
            }
520 81
            $specificKindVisitor = $specificVisitor[$kind] ?? null;
521
522 81
            if (is_callable($specificKindVisitor)) {
523
                // { enter: { Kind() {} }, leave: { Kind() {} } }
524 81
                return $specificKindVisitor;
525
            }
526
        }
527
528 540
        return null;
529
    }
530
}
531