• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dg / dresscode / 39714674839

17 Sep 2026 12:34PM UTC coverage: 97.639% (-0.2%) from 97.877%
39714674839

push

github

dg
released version 1.1.0

18571 of 19020 relevant lines covered (97.64%)

0.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.74
/src/Rules/ControlFlow/ArraySearchForForeachRule.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the DressCode, a coding style and upgrade tool for PHP (https://dresscode.run)
5
 * Copyright (c) 2026 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace DressCode\Rules\ControlFlow;
9

10
use DressCode\Analyses\{Parameter, PhpSignatures, Types};
11
use DressCode\{GlobalCalls, NodeRule, Risk, RuleContext, RuleInfo, Stage, Tristate};
12
use DressCode\Rules\NodeHelpers;
13
use PhpSyntax\Analyses\NameResolver;
14
use PhpSyntax\{Builder, Node, SymbolKind, Token};
15
use PhpSyntax\Nodes\{
16
        ArgumentNode, ClassLikeNode, ConstItemNode, Expression, ExpressionNode, FunctionLikeNode, IdentifierNode,
17
        NameNode, ParameterNode, PlainNodeList, SeparatedNodeList, Statement, StatementNode, TypeNode,
18
};
19
use PhpSyntax\Nodes\Member\{ClassConstNode, MethodNode, PropertyItemNode, PropertyNode};
20
use PhpSyntax\Nodes\Scalar\{BooleanNode, MagicConstantNode, NullNode};
21
use PhpSyntax\Nodes\Type\{IntersectionTypeNode, NamedTypeNode, NullableTypeNode, UnionTypeNode};
22
use function count, in_array;
23

24

25
/**
26
 * A foreach that only looks through the items and keeps a yes or a no, the item or its key, is one of the
27
 * functions PHP 8.4 gave arrays: `array_any()`, `array_all()`, `array_find()` and `array_find_key()`. Which
28
 * one the answer says: false turning into true asks whether any item passes, true turning into false whether
29
 * all of them do, and null turning into the item or its key looks for the first that passes.
30
 *
31
 * The loop is read in two shapes, the one that keeps the answer in a variable initialized right before it and
32
 * the one that returns it and returns the other answer after the loop. The body must be one `if` and nothing
33
 * else, the loop must take its items by value, and the variables of the loop must appear nowhere else in their
34
 * scope: the loop writes them for the code around it, the closure of the call does not. For the same reason the
35
 * condition must not pass a variable of the scope by reference, and the fix is risky where the call it passes one to
36
 * is not known.
37
 *
38
 * The functions take an array and a foreach any iterable, so a loop over what is no array is left alone and the
39
 * fix is risky wherever the loop may go through something else. Without the types, only an array literal and what
40
 * a declaration in sight says are told from a Traversable.
41
 */
42
#[RuleInfo(
43
        Stage::Structure,
44
        requires: ['php' => '>=8.4'],
45
        decisions: ['upgrading.functions.arraySearchFunctions'],
46
        analyses: [PhpSignatures::class, Types::class, NameResolver::class],
47
)]
48
final class ArraySearchForForeachRule extends NodeRule
49
{
50
        public function getVisitedNodes(): array
51
        {
52
                return [Statement\ExpressionStatementNode::class, Statement\ForeachNode::class];
1 ✔
53
        }
54

55

56
        public function enter(Node|Token $node, RuleContext $context): void
57
        {
58
                [$foreach, $outcome, $condition, $answer] = $node instanceof Statement\ForeachNode
1 ✔
59
                        ? self::readReturning($node, $context)
1 ✔
60
                        : self::readAssigning($node, $context);
1 ✔
61
                if (
62
                        $foreach === null
1 ✔
63
                        || $outcome === null
1 ✔
64
                        || $condition === null
1 ✔
65
                        || $answer === null
1 ✔
66
                        || !self::isConvertible($foreach, $context)
1 ✔
67
                ) {
68
                        return;
1 ✔
69
                }
70

71
                $value = $foreach->value;
1 ✔
72
                $key = $foreach->key;
1 ✔
73
                assert($value instanceof Expression\VariableNode);
74
                [$function, $negate] = match (true) {
1 ✔
75
                        self::isBoolean($answer, value: false) && self::isBoolean($outcome, value: true) => ['array_any', false],
1 ✔
76
                        self::isBoolean($answer, value: true) && self::isBoolean($outcome, value: false) => ['array_all', true],
1 ✔
77
                        !$answer instanceof NullNode => [null, false],
1 ✔
78
                        self::isVariable($outcome, $value) => ['array_find', false],
1 ✔
79
                        $key !== null && self::isVariable($outcome, $key) => ['array_find_key', false],
1 ✔
80
                        default => [null, false],
1 ✔
81
                };
82
                $overArray = self::isArray($foreach);
1 ✔
83
                if ($overArray === Tristate::Maybe) {
1 ✔
84
                        $overArray = $context->findAnalysis(Types::class)?->isOfType($foreach->expression, 'array') ?? Tristate::Maybe;
1 ✔
85
                }
86

87
                $reference = $function === null ? Tristate::No : self::passesReference($condition, $foreach, $context);
1 ✔
88
                if (
89
                        $function === null
1 ✔
90
                        || $overArray === Tristate::No
1 ✔
91
                        || $reference === Tristate::Yes
1 ✔
92
                        || !$context->report(
1 ✔
93
                                $foreach,
94
                                "The loop must be written with `$function()`.",
1 ✔
95
                                risk: $overArray === Tristate::Maybe || $reference === Tristate::Maybe ? Risk::TypeUnknown : null,
1 ✔
96
                                because: match (true) {
97
                                        $overArray === Tristate::Maybe => 'the loop may go through an object, which `' . $function . '()` does not take',
1 ✔
98
                                        $reference === Tristate::Maybe
1 ✔
99
                                                => 'the condition may pass a variable by reference, which the arrow function writes only for itself',
1 ✔
100
                                        default => null,
1 ✔
101
                                },
102
                        )
103
                ) {
104
                        return;
1 ✔
105
                }
106

107
                $parameters = $key !== null
1 ✔
108
                        && $condition->find(Expression\VariableNode::class, fn(Expression\VariableNode $v) => self::isVariable($v, $key)) !== []
1 ✔
109
                        ? $value->text . ', ' . $key->text
1 ✔
110
                        : $value->text;
1 ✔
111
                $builder = new Builder;
1 ✔
112
                $arrow = $builder->fragment(Expression\ArrowFunctionNode::class, "fn($parameters) => 0");
1 ✔
113
                $arrow->expression->replaceWith($negate ? NodeHelpers::negate($condition) : $condition->withoutEdgeTrivia());
1 ✔
114
                $name = $builder->name($function);
1 ✔
115
                $call = $builder->call($name, [$foreach->expression->withoutEdgeTrivia(), $arrow]);
1 ✔
116

117
                if ($node instanceof Statement\ForeachNode) {
1 ✔
118
                        $return = $builder->fragment(Statement\ReturnNode::class, 'return $call;', call: $call);
1 ✔
119
                        self::removeStatement(self::findNextStatement($foreach), $context->style->lineEnding);
1 ✔
120
                        $foreach->replaceWith($return);
1 ✔
121
                } else {
122
                        assert($node instanceof Statement\ExpressionStatementNode && $node->expression instanceof Expression\AssignmentNode);
123
                        $node->expression->expression->replaceWith($call);
1 ✔
124
                        self::removeStatement($foreach, $context->style->lineEnding);
1 ✔
125
                }
126

127
                // the name is spelled once the call stands where the namespace can be read off it
128
                $name->text = $context->getAnalysis(NameResolver::class)->shortenName($function, SymbolKind::Function, $name);
1 ✔
129
        }
1 ✔
130

131

132
        /**
133
         * The loop of the shape that keeps the answer in a variable: the statement is its initialization, the loop
134
         * follows it, and the body of the loop assigns the other answer to that same variable and leaves.
135
         * @return array{?Statement\ForeachNode, ?ExpressionNode, ?ExpressionNode, ?ExpressionNode}
136
         */
137
        private static function readAssigning(Node|Token $node, RuleContext $context): array
138
        {
139
                $none = [null, null, null, null];
1 ✔
140
                $assignment = $node instanceof Statement\ExpressionStatementNode ? $node->expression : null;
1 ✔
141
                $foreach = $node instanceof Node ? self::findNextStatement($node) : null;
1 ✔
142
                if (
143
                        !$assignment instanceof Expression\AssignmentNode
1 ✔
144
                        || !$assignment->target instanceof Expression\VariableNode
1 ✔
145
                        || !$foreach instanceof Statement\ForeachNode
1 ✔
146
                        || $node->hasInnerComment()
1 ✔
147
                ) {
148
                        return $none;
1 ✔
149
                }
150

151
                $statements = self::readBody($foreach);
1 ✔
152
                $inner = $statements[0] ?? null;
1 ✔
153
                $outcome = $inner instanceof Statement\ExpressionStatementNode ? $inner->expression : null;
1 ✔
154
                $condition = self::readCondition($foreach, $context);
1 ✔
155
                return count($statements) === 2
1 ✔
156
                        && $statements[1] instanceof Statement\BreakNode
1 ✔
157
                        && $statements[1]->level === null
1 ✔
158
                        && $outcome instanceof Expression\AssignmentNode
1 ✔
159
                        && $outcome->target instanceof Expression\VariableNode
1 ✔
160
                        && self::isVariable($outcome->target, $assignment->target)
1 ✔
161
                        // the arrow function would read the variable before the call assigns it
162
                        && $condition?->find(
1 ✔
163
                                Expression\VariableNode::class,
1 ✔
164
                                fn(Expression\VariableNode $variable) => self::isVariable($variable, $assignment->target),
1 ✔
165
                        ) === []
1 ✔
166
                        ? [$foreach, $outcome->expression, $condition, $assignment->expression]
1 ✔
167
                        : $none;
1 ✔
168
        }
169

170

171
        /**
172
         * The loop of the shape that returns the answer: the body returns one, and the statement after the loop
173
         * returns the other.
174
         * @return array{?Statement\ForeachNode, ?ExpressionNode, ?ExpressionNode, ?ExpressionNode}
175
         */
176
        private static function readReturning(Statement\ForeachNode $foreach, RuleContext $context): array
177
        {
178
                $none = [null, null, null, null];
1 ✔
179
                $statements = self::readBody($foreach);
1 ✔
180
                $after = self::findNextStatement($foreach);
1 ✔
181
                return count($statements) === 1
1 ✔
182
                        && $statements[0] instanceof Statement\ReturnNode
1 ✔
183
                        && $statements[0]->expression !== null
1 ✔
184
                        && $after instanceof Statement\ReturnNode
1 ✔
185
                        && $after->expression !== null
1 ✔
186
                        && !$after->hasInnerComment()
1 ✔
187
                        ? [$foreach, $statements[0]->expression, self::readCondition($foreach, $context), $after->expression]
1 ✔
188
                        : $none;
1 ✔
189
        }
190

191

192
        /**
193
         * The statements the single `if` of the loop holds, empty where the loop does anything else.
194
         * @return list<StatementNode>
195
         */
196
        private static function readBody(Statement\ForeachNode $foreach): array
197
        {
198
                $if = $foreach->body instanceof Statement\BlockNode && count($foreach->body->statements) === 1
1 ✔
199
                        ? $foreach->body->statements->getItems()[0]
1 ✔
200
                        : null;
1 ✔
201
                return $if instanceof Statement\IfNode
1 ✔
202
                        && count($if->elseifs) === 0
1 ✔
203
                        && $if->else === null
1 ✔
204
                        && $if->body instanceof Statement\BlockNode
1 ✔
205
                        ? $if->body->statements->getItems()
1 ✔
206
                        : [];
1 ✔
207
        }
208

209

210
        private static function readCondition(Statement\ForeachNode $foreach, RuleContext $context): ?ExpressionNode
211
        {
212
                $if = $foreach->body instanceof Statement\BlockNode ? $foreach->body->statements->getItems()[0] ?? null : null;
1 ✔
213
                $condition = $if instanceof Statement\IfNode ? $if->condition : null;
1 ✔
214
                // a condition spread over lines becomes the body of an arrow function inside a call, which reads
215
                // worse than the loop it came from, so the loop keeps it; a variable it writes would be the arrow
216
                // function's own and gone after the call, a yield would make the arrow function a generator, and
217
                // the name of the function and its arguments would be those of the arrow function
218
                $changes = fn(Node $node) => $node instanceof Expression\YieldNode
1 ✔
219
                        || $node instanceof Expression\YieldFromNode
1 ✔
220
                        || ($node instanceof MagicConstantNode && in_array(strtolower($node->token->text), ['__function__', '__method__'], true))
1 ✔
221
                        || (
222
                                $node instanceof Expression\FunctionCallNode
1 ✔
223
                                && GlobalCalls::findFunction(
1 ✔
224
                                        $node,
225
                                        ['func_get_args' => true, 'func_get_arg' => true, 'func_num_args' => true],
1 ✔
226
                                        $context,
227
                                ) !== null
1 ✔
228
                        );
229
                return $condition !== null
1 ✔
230
                        && !$condition->isMultiLine()
1 ✔
231
                        && $condition->find(Expression\VariableNode::class, fn(Expression\VariableNode $variable) => $variable->isWritten()) === []
1 ✔
232
                        && !$changes($condition)
1 ✔
233
                        && $condition->find(ExpressionNode::class, $changes) === []
1 ✔
234
                        ? $condition
1 ✔
235
                        : null;
1 ✔
236
        }
237

238

239
        /** Whether the loop takes its items by value, spells out its variables and leaves none of them behind. */
240
        private static function isConvertible(Statement\ForeachNode $foreach, RuleContext $context): bool
241
        {
242
                $scope = $foreach->findAncestor(FunctionLikeNode::class) ?? $foreach->getFile();
1 ✔
243
                if (
244
                        $foreach->ampersand !== null
1 ✔
245
                        || !$foreach->value instanceof Expression\VariableNode
1 ✔
246
                        || $foreach->value->plainName === null
1 ✔
247
                        || ($foreach->key !== null && (!$foreach->key instanceof Expression\VariableNode || $foreach->key->plainName === null))
1 ✔
248
                        || $foreach->hasInnerComment()
1 ✔
249
                        || $scope === null
1 ✔
250
                        || NodeHelpers::findDynamicVariableAccesses($scope, $context) !== []
1 ✔
251
                ) {
252
                        return false;
1 ✔
253
                }
254

255
                $names = [$foreach->value->plainName, $foreach->key?->plainName];
1 ✔
256
                $outside = $scope->find(
1 ✔
257
                        Expression\VariableNode::class,
1 ✔
258
                        fn(Expression\VariableNode $v) => in_array($v->plainName, $names, true) && !self::isInside($v, $foreach),
1 ✔
259
                );
260
                return $outside === [];
1 ✔
261
        }
262

263

264
        /**
265
         * Whether a call of the condition takes a variable of the scope, or an element of one, by reference, which the
266
         * arrow function writes only for itself, as the declaration in the file, the signature of PHP or, of a method, the
267
         * types say; maybe where none of them tells.
268
         */
269
        private static function passesReference(ExpressionNode $condition, Statement\ForeachNode $foreach, RuleContext $context): Tristate
270
        {
271
                $result = Tristate::No;
1 ✔
272
                foreach ($condition->find(ArgumentNode::class) as $argument) {
1 ✔
273
                        $variable = $argument->value;
1 ✔
274
                        while ($variable instanceof Expression\ArrayAccessNode) {
1 ✔
275
                                $variable = $variable->expression;
1 ✔
276
                        }
277

278
                        $list = $argument->parent;
1 ✔
279
                        if (
280
                                !$variable instanceof Expression\VariableNode
1 ✔
281
                                || $variable->isThis()
1 ✔
282
                                || ($foreach->value instanceof Expression\VariableNode && self::isVariable($variable, $foreach->value))
1 ✔
283
                                || ($foreach->key instanceof Expression\VariableNode && self::isVariable($variable, $foreach->key))
1 ✔
284
                                || !$list instanceof SeparatedNodeList
1 ✔
285
                        ) {
286
                                continue;
1 ✔
287
                        }
288

289
                        $call = $list->parent?->parent;
1 ✔
290
                        $parameters = $call instanceof Expression\FunctionCallNode
1 ✔
291
                                || $call instanceof Expression\MethodCallNode
1 ✔
292
                                || $call instanceof Expression\StaticMethodCallNode
1 ✔
293
                                || $call instanceof Expression\NewNode
1 ✔
294
                                ? NodeHelpers::findParameters($call, $context)
1 ✔
295
                                : null;
×
296
                        if ($parameters === null) {
1 ✔
297
                                $result = Tristate::Maybe;
1 ✔
298
                                continue;
1 ✔
299
                        }
300

301
                        $position = (int) array_search($argument, $list->getItems(), strict: true);
1 ✔
302
                        $last = $parameters[count($parameters) - 1] ?? null;
1 ✔
303
                        $taking = match (true) {
1 ✔
304
                                $argument->name !== null => array_filter(
1 ✔
305
                                        $parameters,
306
                                        fn(Parameter $parameter) => $parameter->name === $argument->name->text,
1 ✔
307
                                ),
308
                                $argument->ellipsis !== null => array_slice($parameters, $position),
1 ✔
309
                                default => [$parameters[$position] ?? ($last?->variadic ? $last : null)],
1 ✔
310
                        };
311
                        if (array_any($taking, fn(?Parameter $parameter) => $parameter?->byReference === true)) {
1 ✔
312
                                return Tristate::Yes;
1 ✔
313
                        }
314
                }
315

316
                return $result;
1 ✔
317
        }
318

319

320
        /**
321
         * Whether the loop goes through an array, as the declarations in sight tell: an array literal, a variadic
322
         * parameter, and by its declared type a parameter nothing writes, and inside a method a property or a method of
323
         * `$this` and a constant of the class. The type of a property or of what a method returns holds in a child too,
324
         * which may not turn it into another; a constant is certain only through `self` or with a type, the untyped one
325
         * a child may declare again.
326
         */
327
        private static function isArray(Statement\ForeachNode $foreach): Tristate
328
        {
329
                $subject = $foreach->expression;
1 ✔
330
                $function = $foreach->findAncestor(FunctionLikeNode::class);
1 ✔
331
                $class = $function instanceof MethodNode ? $function->findAncestor(ClassLikeNode::class) : null;
1 ✔
332
                if ($subject instanceof Expression\ArrayNode) {
1 ✔
333
                        return Tristate::Yes;
1 ✔
334

335
                } elseif ($subject instanceof Expression\VariableNode) {
1 ✔
336
                        $parameter = array_find(
1 ✔
337
                                $function?->parameters?->getItems() ?? [],
1 ✔
338
                                fn(ParameterNode $parameter) => $parameter->variable->plainName === $subject->plainName,
1 ✔
339
                        );
340
                        return match (true) {
1 ✔
341
                                $parameter === null,
342
                                $function?->find(
1 ✔
343
                                        Expression\VariableNode::class,
1 ✔
344
                                        fn(Expression\VariableNode $variable) => $variable->plainName === $subject->plainName && $variable->isWritten(),
1 ✔
345
                                ) !== [] => Tristate::Maybe,
1 ✔
346
                                $parameter->ellipsis !== null => Tristate::Yes,
1 ✔
347
                                default => self::isArrayType($parameter->type),
1 ✔
348
                        };
349

350
                } elseif ($class === null) {
1 ✔
351
                        return Tristate::Maybe;
1 ✔
352

353
                } elseif (
354
                        $subject instanceof Expression\PropertyFetchNode
1 ✔
355
                        && $subject->isOfThis()
1 ✔
356
                        && !$subject->nullsafe
1 ✔
357
                        && ($name = $subject->plainName) !== null
1 ✔
358
                ) {
359
                        foreach ($class->members as $member) {
1 ✔
360
                                if (
361
                                        $member instanceof PropertyNode
1 ✔
362
                                        && array_any($member->items->getItems(), fn(PropertyItemNode $item) => $item->plainName === $name)
1 ✔
363
                                ) {
364
                                        // a static property is not what `$this->` reads
365
                                        return $member->modifiers->static ? Tristate::Maybe : self::isArrayType($member->type);
1 ✔
366
                                } elseif ($member instanceof MethodNode && $member->isConstructor()) {
1 ✔
367
                                        $promoted = array_find(
1 ✔
368
                                                $member->parameters->getItems(),
1 ✔
369
                                                fn(ParameterNode $parameter) => $parameter->promoted && $parameter->variable->plainName === $name,
1 ✔
370
                                        );
371
                                        if ($promoted !== null) {
1 ✔
372
                                                return self::isArrayType($promoted->type);
1 ✔
373
                                        }
374
                                }
375
                        }
376

377
                } elseif (
378
                        $subject instanceof Expression\MethodCallNode
1 ✔
379
                        && $subject->object instanceof Expression\VariableNode
1 ✔
380
                        && $subject->object->isThis()
1 ✔
381
                        && !$subject->nullsafe
1 ✔
382
                        && $subject->name instanceof IdentifierNode
1 ✔
383
                        && !$class instanceof Statement\TraitNode // the class using the trait may declare the method anew, in any type
1 ✔
384
                ) {
385
                        foreach ($class->members as $member) {
1 ✔
386
                                if ($member instanceof MethodNode && $member->name->equals($subject->name->text)) {
1 ✔
387
                                        return self::isArrayType($member->returnType);
1 ✔
388
                                }
389
                        }
390

391
                } elseif (
392
                        $subject instanceof Expression\ClassConstantFetchNode
1 ✔
393
                        && $subject->class instanceof NameNode
1 ✔
394
                        && in_array($own = strtolower($subject->class->text), ['self', 'static'], true)
1 ✔
395
                        && $subject->name instanceof IdentifierNode
1 ✔
396
                ) {
397
                        $name = $subject->name->text;
1 ✔
398
                        foreach ($class->members as $member) {
1 ✔
399
                                if (
400
                                        $member instanceof ClassConstNode
1 ✔
401
                                        && ($item = array_find($member->items->getItems(), fn(ConstItemNode $item) => $item->name->text === $name)) !== null
1 ✔
402
                                ) {
403
                                        return match (true) {
1 ✔
404
                                                $member->type !== null => self::isArrayType($member->type),
1 ✔
405
                                                $own === 'self' && $item->value instanceof Expression\ArrayNode => Tristate::Yes,
1 ✔
406
                                                default => Tristate::Maybe,
1 ✔
407
                                        };
408
                                }
409
                        }
410
                }
411

412
                return Tristate::Maybe;
1 ✔
413
        }
414

415

416
        /** Whether a value of the declared type is an array; no type at all may be anything. */
417
        private static function isArrayType(?TypeNode $type): Tristate
418
        {
419
                if ($type instanceof NamedTypeNode) {
1 ✔
420
                        return match (strtolower($type->name->text)) {
1 ✔
421
                                'array' => Tristate::Yes,
1 ✔
422
                                'iterable', 'callable', 'mixed' => Tristate::Maybe,
1 ✔
423
                                default => Tristate::No,
1 ✔
424
                        };
425
                } elseif ($type instanceof NullableTypeNode) {
1 ✔
426
                        return self::isArrayType($type->type) === Tristate::No ? Tristate::No : Tristate::Maybe;
1 ✔
427
                } elseif ($type instanceof UnionTypeNode) {
1 ✔
428
                        return array_all($type->types->getItems(), fn(TypeNode $member) => self::isArrayType($member) === Tristate::No)
1 ✔
429
                                ? Tristate::No
×
430
                                : Tristate::Maybe;
1 ✔
431
                }
432

433
                return $type instanceof IntersectionTypeNode ? Tristate::No : Tristate::Maybe;
×
434
        }
435

436

437
        /** Whether the node lies in the subtree of the other one. */
438
        private static function isInside(Node $node, Node $ancestor): bool
439
        {
440
                for ($parent = $node->parent; $parent !== null; $parent = $parent->parent) {
1 ✔
441
                        if ($parent === $ancestor) {
1 ✔
442
                                return true;
1 ✔
443
                        }
444
                }
445

446
                return false;
1 ✔
447
        }
448

449

450
        /** Removes a statement the call took over, the blank lines above it going with it, not to what follows. */
451
        private static function removeStatement(?Node $statement, string $lineEnding): void
452
        {
453
                $first = $statement?->getFirstToken();
1 ✔
454
                if ($first !== null && $first->startsLine()) {
1 ✔
455
                        $first->setBlankLinesBefore(0, $lineEnding);
1 ✔
456
                }
457

458
                $statement?->remove();
1 ✔
459
        }
1 ✔
460

461

462
        private static function findNextStatement(Node $statement): ?Node
463
        {
464
                return $statement->parent instanceof PlainNodeList ? $statement->getNextSibling() : null;
1 ✔
465
        }
466

467

468
        private static function isBoolean(ExpressionNode $expression, bool $value): bool
469
        {
470
                return $expression instanceof BooleanNode && $expression->toValue() === $value;
1 ✔
471
        }
472

473

474
        private static function isVariable(ExpressionNode $expression, ExpressionNode $variable): bool
475
        {
476
                return $expression instanceof Expression\VariableNode
1 ✔
477
                        && $variable instanceof Expression\VariableNode
1 ✔
478
                        && $expression->plainName !== null
1 ✔
479
                        && $expression->plainName === $variable->plainName;
1 ✔
480
        }
481
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc