Skip to content

Commit b000c2b

Browse files
staabmphpstan-bot
authored andcommitted
Fix phpstan/phpstan#13902: Chaining phpstan-assert fails to assert types
- In TypeSpecifier::specifyTypesInCondition(), recurse into inner method calls when processing a method chain in null context, so assertions from all methods in the chain are applied - In TypeSpecifier::specifyTypesFromAsserts(), unwrap $this mapping through method calls whose return type equals the caller type, so outer assertions resolve to the original object - Only recurse when $expr->var is a MethodCall to avoid breaking ImpossibleCheckTypeHelper's rootExpr tracking - New regression test in tests/PHPStan/Analyser/nsrt/bug-13902.php
1 parent f08de42 commit b000c2b

File tree

2 files changed

+63
-1
lines changed

2 files changed

+63
-1
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,11 +590,18 @@ public function specifyTypesInCondition(
590590
));
591591
$specifiedTypes = $this->specifyTypesFromAsserts($context, $expr, $asserts, $parametersAcceptor, $scope);
592592
if ($specifiedTypes !== null) {
593+
if ($context->null() && $expr->var instanceof MethodCall) {
594+
return $specifiedTypes->unionWith($this->specifyTypesInCondition($scope, $expr->var, $context));
595+
}
593596
return $specifiedTypes;
594597
}
595598
}
596599
}
597600

601+
if ($context->null() && $expr->var instanceof MethodCall) {
602+
return $this->specifyTypesInCondition($scope, $expr->var, $context);
603+
}
604+
598605
return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
599606
} elseif ($expr instanceof StaticCall && $expr->name instanceof Node\Identifier) {
600607
if ($expr->class instanceof Name) {
@@ -1665,7 +1672,16 @@ private function specifyTypesFromAsserts(TypeSpecifierContext $context, Expr\Cal
16651672
}
16661673

16671674
if ($call instanceof MethodCall) {
1668-
$argsMap['this'] = [$call->var];
1675+
$callVar = $call->var;
1676+
while ($callVar instanceof MethodCall) {
1677+
$callerType = $scope->getType($callVar->var);
1678+
$returnType = $scope->getType($callVar);
1679+
if (!$callerType->equals($returnType)) {
1680+
break;
1681+
}
1682+
$callVar = $callVar->var;
1683+
}
1684+
$argsMap['this'] = [$callVar];
16691685
}
16701686

16711687
/** @var SpecifiedTypes|null $types */
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug13902;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class HelloWorld
10+
{
11+
public mixed $a = null;
12+
public mixed $b = null;
13+
14+
/**
15+
* @phpstan-assert int $this->a
16+
* @return $this
17+
*/
18+
public function setA()
19+
{
20+
$this->a = 1;
21+
return $this;
22+
}
23+
/**
24+
* @phpstan-assert int $this->b
25+
* @return $this
26+
*/
27+
public function setB()
28+
{
29+
$this->b = 1;
30+
return $this;
31+
}
32+
}
33+
34+
function test(): void
35+
{
36+
$o = new HelloWorld;
37+
$o->setA()->setB();
38+
assertType('int', $o->a);
39+
assertType('int', $o->b);
40+
41+
$o2 = new HelloWorld;
42+
$o2->setA();
43+
$o2->setB();
44+
assertType('int', $o2->a);
45+
assertType('int', $o2->b);
46+
}

0 commit comments

Comments
 (0)