Skip to content

Commit 0c1a698

Browse files
staabmondrejmirtes
authored andcommitted
Introduce LastConditionVisitor to prevent annoying last-condition errors
1 parent 8efca2a commit 0c1a698

12 files changed

+172
-13
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,11 @@ services:
488488
tags:
489489
- phpstan.parser.richParserNodeVisitor
490490

491+
-
492+
class: PHPStan\Parser\LastConditionVisitor
493+
tags:
494+
- phpstan.parser.richParserNodeVisitor
495+
491496
-
492497
class: PhpParser\NodeVisitor\NodeConnectingVisitor
493498

src/Parser/LastConditionVisitor.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Parser;
4+
5+
use PhpParser\Node;
6+
use PhpParser\NodeVisitorAbstract;
7+
use function count;
8+
9+
class LastConditionVisitor extends NodeVisitorAbstract
10+
{
11+
12+
public const ATTRIBUTE_NAME = 'isLastCondition';
13+
14+
public function enterNode(Node $node): ?Node
15+
{
16+
if ($node instanceof Node\Stmt\If_ && $node->elseifs !== []) {
17+
$lastElseIf = count($node->elseifs) - 1;
18+
19+
$node->elseifs[$lastElseIf]->cond->setAttribute(self::ATTRIBUTE_NAME, true);
20+
}
21+
22+
if ($node instanceof Node\Expr\Match_ && $node->arms !== []) {
23+
$lastArm = count($node->arms) - 1;
24+
25+
if ($node->arms[$lastArm]->conds !== null && $node->arms[$lastArm]->conds !== []) {
26+
$node->arms[$lastArm]->conds[0]->setAttribute(self::ATTRIBUTE_NAME, true);
27+
}
28+
}
29+
30+
return null;
31+
}
32+
33+
}

src/Rules/Classes/ImpossibleInstanceOfRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Parser\LastConditionVisitor;
78
use PHPStan\Rules\Rule;
89
use PHPStan\Rules\RuleErrorBuilder;
910
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -81,6 +82,10 @@ public function processNode(Node $node, Scope $scope): array
8182
)))->build(),
8283
];
8384
} elseif ($this->checkAlwaysTrueInstanceof) {
85+
if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) {
86+
return [];
87+
}
88+
8489
return [
8590
$addTip(RuleErrorBuilder::message(sprintf(
8691
'Instanceof between %s and %s will always evaluate to true.',

src/Rules/Comparison/ConstantConditionRuleHelper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PhpParser\Node\Expr\FuncCall;
77
use PhpParser\Node\Expr\MethodCall;
88
use PHPStan\Analyser\Scope;
9+
use PHPStan\Parser\LastConditionVisitor;
910
use PHPStan\Type\BooleanType;
1011

1112
class ConstantConditionRuleHelper

src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Parser\LastConditionVisitor;
78
use PHPStan\Rules\Rule;
89
use PHPStan\Rules\RuleErrorBuilder;
910
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -49,6 +50,10 @@ public function processNode(Node $node, Scope $scope): array
4950
))->build(),
5051
];
5152
} elseif ($this->checkAlwaysTrueStrictComparison) {
53+
if ($node->getAttribute(LastConditionVisitor::ATTRIBUTE_NAME) === true) {
54+
return [];
55+
}
56+
5257
return [
5358
RuleErrorBuilder::message(sprintf(
5459
'Strict comparison using %s between %s and %s will always evaluate to true.',

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,9 @@ public function dataFileAsserts(): iterable
11491149
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8467b.php');
11501150
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8442.php');
11511151
yield from $this->gatherAssertTypes(__DIR__ . '/data/PDOStatement.php');
1152+
if (PHP_VERSION_ID >= 80000) {
1153+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Comparison/data/bug-8485.php');
1154+
}
11521155
yield from $this->gatherAssertTypes(__DIR__ . '/data/discussion-8447.php');
11531156
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7805.php');
11541157
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-82.php');

tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Rules\Rule;
66
use PHPStan\Testing\RuleTestCase;
7+
use const PHP_VERSION_ID;
78

89
/**
910
* @extends RuleTestCase<ImpossibleInstanceOfRule>
@@ -368,4 +369,24 @@ public function testBug5333(): void
368369
]);
369370
}
370371

372+
public function testBug8042(): void
373+
{
374+
if (PHP_VERSION_ID < 80000) {
375+
$this->markTestSkipped('This test needs PHP 8.0');
376+
}
377+
378+
$this->checkAlwaysTrueInstanceOf = true;
379+
$this->treatPhpDocTypesAsCertain = true;
380+
$this->analyse([__DIR__ . '/data/bug-8042.php'], [
381+
[
382+
'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.',
383+
18
384+
],
385+
[
386+
'Instanceof between Bug8042\B and Bug8042\B will always evaluate to true.',
387+
26
388+
],
389+
]);
390+
}
391+
371392
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug8042;
4+
5+
class A {}
6+
class B {}
7+
8+
function test(A|B $ab): string {
9+
return match (true) {
10+
$ab instanceof A => 'a',
11+
$ab instanceof B => 'b',
12+
};
13+
}
14+
15+
function test2(A|B $ab): string {
16+
return match (true) {
17+
$ab instanceof A => 'a',
18+
$ab instanceof B => 'b',
19+
rand(0, 1) => 'never'
20+
};
21+
}
22+
23+
function test3(A|B $ab): string {
24+
return match (true) {
25+
$ab instanceof A => 'a',
26+
$ab instanceof B => 'b',
27+
default => 'never'
28+
};
29+
}

tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,15 +140,15 @@ public function testBug8485(): void
140140
$this->analyse([__DIR__ . '/data/bug-8485.php'], [
141141
[
142142
'If condition is always true.',
143-
19,
143+
21,
144144
],
145145
[
146146
'If condition is always false.',
147-
24,
147+
26,
148148
],
149149
[
150150
'If condition is always false.',
151-
29,
151+
31,
152152
],
153153
]);
154154
}

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,6 @@ public function testStrictComparison(): void
182182
'Strict comparison using === between int<10, max> and \'foo\' will always evaluate to false.',
183183
635,
184184
],
185-
[
186-
'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.',
187-
654,
188-
],
189185
[
190186
'Strict comparison using === between string|null and 1 will always evaluate to false.',
191187
685,
@@ -250,6 +246,10 @@ public function testStrictComparison(): void
250246
'Strict comparison using !== between NAN and NAN will always evaluate to true.',
251247
983,
252248
],
249+
[
250+
'Strict comparison using === between \'foofoofoofoofoofoof…\' and \'foofoofoofoofoofoof…\' will always evaluate to true.',
251+
996,
252+
],
253253
],
254254
);
255255
}
@@ -617,23 +617,27 @@ public function testBug8485(): void
617617
$this->analyse([__DIR__ . '/data/bug-8485.php'], [
618618
[
619619
'Strict comparison using === between Bug8485\E::c and Bug8485\E::c will always evaluate to true.',
620-
17,
620+
19,
621621
],
622622
[
623623
'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.',
624-
22,
624+
24,
625625
],
626626
[
627627
'Strict comparison using === between Bug8485\F::c and Bug8485\E::c will always evaluate to false.',
628-
27,
628+
29,
629629
],
630630
[
631631
'Strict comparison using === between Bug8485\F and Bug8485\E will always evaluate to false.',
632-
35,
632+
36,
633633
],
634634
[
635635
'Strict comparison using === between Bug8485\F and Bug8485\E::c will always evaluate to false.',
636-
40,
636+
41,
637+
],
638+
[
639+
'Strict comparison using === between Bug8485\FooEnum::C and Bug8485\FooEnum::C will always evaluate to true.',
640+
74,
637641
],
638642
]);
639643
}

0 commit comments

Comments
 (0)