Skip to content

Commit e9265f2

Browse files
authored
[CodeQuality] Fix MatchAssertSameExpectedTypeRector rewriting a passing assert into a failing one (#771)
1 parent 753e68c commit e9265f2

6 files changed

Lines changed: 134 additions & 2 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Source\Criteria;
7+
8+
final class SkipStaleNarrowedUnionReturnType extends TestCase
9+
{
10+
public function test()
11+
{
12+
$criteria = new Criteria();
13+
assert(is_string($criteria->getType()));
14+
15+
$criteria->setTypeId(100);
16+
17+
$this->assertSame(100, $criteria->getType());
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Source\Criteria;
7+
8+
final class SkipStaleNarrowedUnionReturnTypeToInt extends TestCase
9+
{
10+
public function test()
11+
{
12+
$criteria = new Criteria();
13+
assert(is_int($criteria->getType()));
14+
15+
$criteria->setAnyType(true);
16+
17+
$this->assertSame('Any', $criteria->getType());
18+
}
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Source\Criteria;
7+
8+
final class SkipUnionReturnType extends TestCase
9+
{
10+
public function test()
11+
{
12+
$criteria = new Criteria();
13+
14+
$this->assertSame(100, $criteria->getType());
15+
}
16+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\MatchAssertSameExpectedTypeRector\Source;
6+
7+
final class Criteria
8+
{
9+
private ?int $typeId = null;
10+
11+
private ?bool $anyType = null;
12+
13+
public function setTypeId(int $typeId): self
14+
{
15+
$this->typeId = $typeId;
16+
$this->anyType = null;
17+
18+
return $this;
19+
}
20+
21+
public function setAnyType(bool $anyType): self
22+
{
23+
$this->anyType = $anyType;
24+
$this->typeId = null;
25+
26+
return $this;
27+
}
28+
29+
public function getType(): int|string|null
30+
{
31+
return $this->anyType ? 'Any' : $this->typeId;
32+
}
33+
}

‎rules/CodeQuality/Rector/MethodCall/MatchAssertSameExpectedTypeRector.php‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
use PhpParser\Node\Expr\StaticCall;
1010
use PhpParser\Node\Scalar\Int_;
1111
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Type\Type;
1213
use PHPStan\Type\TypeCombinator;
14+
use Rector\PHPUnit\CodeQuality\Reflection\MethodParametersAndReturnTypesResolver;
1315
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
1416
use Rector\Rector\AbstractRector;
1517
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
@@ -21,7 +23,8 @@
2123
final class MatchAssertSameExpectedTypeRector extends AbstractRector
2224
{
2325
public function __construct(
24-
private readonly TestsNodeAnalyzer $testsNodeAnalyzer
26+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
27+
private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver
2528
) {
2629
}
2730

@@ -103,7 +106,16 @@ public function refactor(Node $node): ?Node
103106

104107
$variableExpr = $node->getArgs()[1]
105108
->value;
106-
$variableType = $this->nodeTypeResolver->getNativeType($variableExpr);
109+
110+
if ($variableExpr instanceof MethodCall || $variableExpr instanceof StaticCall) {
111+
// an earlier assert can leave the scope type narrowed, even after the caller is mutated
112+
$variableType = $this->methodParametersAndReturnTypesResolver->resolveCallReturnType($variableExpr);
113+
if (! $variableType instanceof Type) {
114+
return null;
115+
}
116+
} else {
117+
$variableType = $this->nodeTypeResolver->getNativeType($variableExpr);
118+
}
107119

108120
$directVariableType = TypeCombinator::removeNull($variableType);
109121

‎rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,39 @@ public function resolveCallParameterTypes(MethodCall|StaticCall|New_ $callLike):
119119
return $this->resolveParameterTypes($extendedMethodReflection, $classReflection);
120120
}
121121

122+
public function resolveCallReturnType(MethodCall|StaticCall $callLike): ?Type
123+
{
124+
if (! $callLike->name instanceof Identifier) {
125+
return null;
126+
}
127+
128+
$methodName = $callLike->name->toString();
129+
130+
$callerType = $this->resolveObjectType(
131+
$this->nodeTypeResolver->getType($callLike instanceof MethodCall ? $callLike->var : $callLike->class)
132+
);
133+
134+
if (! $callerType instanceof ObjectType) {
135+
return null;
136+
}
137+
138+
$classReflection = $callerType->getClassReflection();
139+
if (! $classReflection instanceof ClassReflection) {
140+
return null;
141+
}
142+
143+
if (! $classReflection->hasNativeMethod($methodName)) {
144+
return null;
145+
}
146+
147+
$extendedParametersAcceptor = ParametersAcceptorSelector::combineAcceptors(
148+
$classReflection->getNativeMethod($methodName)
149+
->getVariants()
150+
);
151+
152+
return $extendedParametersAcceptor->getNativeReturnType();
153+
}
154+
122155
/**
123156
* @return string[]
124157
*/

0 commit comments

Comments
 (0)