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

dg / dresscode / 39713065381

03 Sep 2026 05:06AM UTC coverage: 97.877% (+0.3%) from 97.606%
39713065381

push

github

dg
released version 1.0.0

17195 of 17568 relevant lines covered (97.88%)

0.98 hits per line

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

99.15
/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};
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. Only an array literal and what a declaration in
40
 * 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, 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
                $reference = $function === null ? Tristate::No : self::passesReference($condition, $foreach, $context);
1 ✔
84
                if (
85
                        $function === null
1 ✔
86
                        || $overArray === Tristate::No
1 ✔
87
                        || $reference === Tristate::Yes
1 ✔
88
                        || !$context->report(
1 ✔
89
                                $foreach,
90
                                "The loop must be written with `$function()`.",
1 ✔
91
                                risk: $overArray === Tristate::Maybe || $reference === Tristate::Maybe ? Risk::TypeUnknown : null,
1 ✔
92
                                because: match (true) {
93
                                        $overArray === Tristate::Maybe => 'the loop may go through an object, which `' . $function . '()` does not take',
1 ✔
94
                                        $reference === Tristate::Maybe
1 ✔
95
                                                => 'the condition may pass a variable by reference, which the arrow function writes only for itself',
1 ✔
96
                                        default => null,
1 ✔
97
                                },
98
                        )
99
                ) {
100
                        return;
1 ✔
101
                }
102

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

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

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

127

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

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

166

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

187

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

205

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

234

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

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

259

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

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

285
                        $call = $list->parent?->parent;
1 ✔
286
                        $parameters = $call instanceof Expression\FunctionCallNode
1 ✔
287
                                ? NodeHelpers::findParameters($call, $context)
1 ✔
288
                                : null;
1 ✔
289
                        if ($parameters === null) {
1 ✔
290
                                $result = Tristate::Maybe;
1 ✔
291
                                continue;
1 ✔
292
                        }
293

294
                        $position = (int) array_search($argument, $list->getItems(), strict: true);
1 ✔
295
                        $last = $parameters[count($parameters) - 1] ?? null;
1 ✔
296
                        $taking = match (true) {
1 ✔
297
                                $argument->name !== null => array_filter(
1 ✔
298
                                        $parameters,
299
                                        fn(Parameter $parameter) => $parameter->name === $argument->name->text,
1 ✔
300
                                ),
301
                                $argument->ellipsis !== null => array_slice($parameters, $position),
1 ✔
302
                                default => [$parameters[$position] ?? ($last?->variadic ? $last : null)],
1 ✔
303
                        };
304
                        if (array_any($taking, fn(?Parameter $parameter) => $parameter?->byReference === true)) {
1 ✔
305
                                return Tristate::Yes;
1 ✔
306
                        }
307
                }
308

309
                return $result;
1 ✔
310
        }
311

312

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

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

343
                } elseif ($class === null) {
1 ✔
344
                        return Tristate::Maybe;
1 ✔
345

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

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

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

405
                return Tristate::Maybe;
1 ✔
406
        }
407

408

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

426
                return $type instanceof IntersectionTypeNode ? Tristate::No : Tristate::Maybe;
×
427
        }
428

429

430
        /** Whether the node lies in the subtree of the other one. */
431
        private static function isInside(Node $node, Node $ancestor): bool
432
        {
433
                for ($parent = $node->parent; $parent !== null; $parent = $parent->parent) {
1 ✔
434
                        if ($parent === $ancestor) {
1 ✔
435
                                return true;
1 ✔
436
                        }
437
                }
438

439
                return false;
1 ✔
440
        }
441

442

443
        /** Removes a statement the call took over, the blank lines above it going with it, not to what follows. */
444
        private static function removeStatement(?Node $statement, string $lineEnding): void
445
        {
446
                $first = $statement?->getFirstToken();
1 ✔
447
                if ($first !== null && $first->startsLine()) {
1 ✔
448
                        $first->setBlankLinesBefore(0, $lineEnding);
1 ✔
449
                }
450

451
                $statement?->remove();
1 ✔
452
        }
1 ✔
453

454

455
        private static function findNextStatement(Node $statement): ?Node
456
        {
457
                return $statement->parent instanceof PlainNodeList ? $statement->getNextSibling() : null;
1 ✔
458
        }
459

460

461
        private static function isBoolean(ExpressionNode $expression, bool $value): bool
462
        {
463
                return $expression instanceof BooleanNode && $expression->toValue() === $value;
1 ✔
464
        }
465

466

467
        private static function isVariable(ExpressionNode $expression, ExpressionNode $variable): bool
468
        {
469
                return $expression instanceof Expression\VariableNode
1 ✔
470
                        && $variable instanceof Expression\VariableNode
1 ✔
471
                        && $expression->plainName !== null
1 ✔
472
                        && $expression->plainName === $variable->plainName;
1 ✔
473
        }
474
}
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