Skip to content

Commit 995eff9

Browse files
committed
Fluent interface method only invalidates its own object by default, not arguments
1 parent 03d01ea commit 995eff9

File tree

3 files changed

+94
-7
lines changed

3 files changed

+94
-7
lines changed

src/Analyser/MutatingScope.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5156,6 +5156,17 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ?
51565156
return $type->getMethod($methodName, $this);
51575157
}
51585158

5159+
/** @api */
5160+
public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
5161+
{
5162+
$type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
5163+
if ($type === null) {
5164+
return null;
5165+
}
5166+
5167+
return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod();
5168+
}
5169+
51595170
/**
51605171
* @param MethodCall|Node\Expr\StaticCall $methodCall
51615172
*/

src/Analyser/NodeScopeResolver.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
use PHPStan\PhpDoc\Tag\VarTag;
119119
use PHPStan\Reflection\Assertions;
120120
use PHPStan\Reflection\ClassReflection;
121+
use PHPStan\Reflection\ExtendedMethodReflection;
121122
use PHPStan\Reflection\FunctionReflection;
122123
use PHPStan\Reflection\InitializerExprTypeResolver;
123124
use PHPStan\Reflection\MethodReflection;
@@ -160,6 +161,7 @@
160161
use PHPStan\Type\StaticType;
161162
use PHPStan\Type\StaticTypeFactory;
162163
use PHPStan\Type\StringType;
164+
use PHPStan\Type\ThisType;
163165
use PHPStan\Type\Type;
164166
use PHPStan\Type\TypeCombinator;
165167
use PHPStan\Type\TypeTraverser;
@@ -2080,7 +2082,7 @@ static function (): void {
20802082
if ($parametersAcceptor !== null) {
20812083
$expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
20822084
}
2083-
$result = $this->processArgs($stmt, $functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2085+
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
20842086
$scope = $result->getScope();
20852087
$hasYield = $result->hasYield();
20862088
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
@@ -2298,7 +2300,16 @@ static function (): void {
22982300
if ($parametersAcceptor !== null) {
22992301
$expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
23002302
}
2301-
$result = $this->processArgs($stmt, $methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2303+
$result = $this->processArgs(
2304+
$stmt,
2305+
$methodReflection,
2306+
$methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null,
2307+
$parametersAcceptor,
2308+
$expr->getArgs(),
2309+
$scope,
2310+
$nodeCallback,
2311+
$context,
2312+
);
23022313
$scope = $result->getScope();
23032314

23042315
if ($methodReflection !== null) {
@@ -2457,7 +2468,7 @@ static function (): void {
24572468
if ($parametersAcceptor !== null) {
24582469
$expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
24592470
}
2460-
$result = $this->processArgs($stmt, $methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null);
2471+
$result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null);
24612472
$scope = $result->getScope();
24622473
$scopeFunction = $scope->getFunction();
24632474

@@ -2792,7 +2803,7 @@ static function (): void {
27922803
if ($parametersAcceptor !== null) {
27932804
$expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
27942805
}
2795-
$result = $this->processArgs($stmt, $constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2806+
$result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
27962807
$scope = $result->getScope();
27972808
$hasYield = $hasYield || $result->hasYield();
27982809
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
@@ -3697,6 +3708,7 @@ private function processAttributeGroups(
36973708
private function processArgs(
36983709
Node\Stmt $stmt,
36993710
$calleeReflection,
3711+
?ExtendedMethodReflection $nakedMethodReflection,
37003712
?ParametersAcceptor $parametersAcceptor,
37013713
array $args,
37023714
MutatingScope $scope,
@@ -3821,7 +3833,25 @@ private function processArgs(
38213833
}
38223834
} elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
38233835
$argType = $scope->getType($arg->value);
3824-
if (!$argType->isObject()->no() || !(new ResourceType())->isSuperTypeOf($argType)->no()) {
3836+
if (!$argType->isObject()->no()) {
3837+
$nakedReturnType = null;
3838+
if ($nakedMethodReflection !== null) {
3839+
$nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3840+
$scope,
3841+
$args,
3842+
$nakedMethodReflection->getVariants(),
3843+
$nakedMethodReflection->getNamedArgumentsVariants(),
3844+
);
3845+
$nakedReturnType = $nakedParametersAcceptor->getReturnType();
3846+
}
3847+
if (
3848+
$nakedReturnType === null
3849+
|| !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
3850+
|| $nakedMethodReflection->isPure()->no()
3851+
) {
3852+
$scope = $scope->invalidateExpression($arg->value, true);
3853+
}
3854+
} elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
38253855
$scope = $scope->invalidateExpression($arg->value, true);
38263856
}
38273857
}

tests/PHPStan/Analyser/data/impure-method.php

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ImpureMethod;
44

5+
use stdClass;
56
use function PHPStan\Testing\assertType;
67

78
/**
@@ -21,7 +22,16 @@ public function voidMethod(): void
2122
/**
2223
* @return $this
2324
*/
24-
public function returnsThis()
25+
public function returnsThis($arg)
26+
{
27+
$this->fooProp = rand(0, 1);
28+
}
29+
30+
/**
31+
* @return $this
32+
* @phpstan-impure
33+
*/
34+
public function returnsThisImpure($arg)
2535
{
2636
$this->fooProp = rand(0, 1);
2737
}
@@ -67,7 +77,7 @@ public function doFluent(): void
6777
$this->fooProp = 1;
6878
assertType('1', $this->fooProp);
6979

70-
$this->returnsThis();
80+
$this->returnsThis(new stdClass());
7181
assertType('int', $this->fooProp);
7282
}
7383

@@ -108,3 +118,39 @@ public function doLorem(): void
108118
}
109119

110120
}
121+
122+
class Person
123+
{
124+
125+
public function getName(): ?string
126+
{
127+
}
128+
129+
}
130+
131+
class Bar
132+
{
133+
134+
public function doFoo(): void
135+
{
136+
$f = new Foo();
137+
138+
$p = new Person();
139+
assert($p->getName() !== null);
140+
assertType('string', $p->getName());
141+
$f->returnsThis($p);
142+
assertType('string', $p->getName());
143+
}
144+
145+
public function doFoo2(): void
146+
{
147+
$f = new Foo();
148+
149+
$p = new Person();
150+
assert($p->getName() !== null);
151+
assertType('string', $p->getName());
152+
$f->returnsThisImpure($p);
153+
assertType('string|null', $p->getName());
154+
}
155+
156+
}

0 commit comments

Comments
 (0)