diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index feaa721167..be1d52b3f9 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3736,7 +3736,10 @@ public function specifyExpressionType(Expr $expr, Type $type, ?Type $nativeType public function assignExpression(Expr $expr, Type $type): self { $scope = $this; - if ($expr instanceof PropertyFetch || $expr instanceof Expr\StaticPropertyFetch) { + if ($expr instanceof PropertyFetch) { + $scope = $this->invalidateExpression($expr) + ->invalidateMethodsOnExpression($expr->var); + } elseif ($expr instanceof Expr\StaticPropertyFetch) { $scope = $this->invalidateExpression($expr); } @@ -3806,6 +3809,64 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require ); } + public function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self + { + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + $moreSpecificTypeHolders = $this->moreSpecificTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; + $nodeFinder = new NodeFinder(); + foreach (array_keys($moreSpecificTypeHolders) as $exprString) { + $exprString = (string) $exprString; + + try { + $expr = $this->parser->parseString('findFirst([$expr->expr], function (Node $node) use ($exprStringToInvalidate): bool { + if (!$node instanceof MethodCall) { + return false; + } + + return $this->getNodeKey($node->var) === $exprStringToInvalidate; + }); + if ($found === null) { + continue; + } + + unset($moreSpecificTypeHolders[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->constantTypes, + $this->getFunction(), + $this->getNamespace(), + $this->getVariableTypes(), + $moreSpecificTypeHolders, + $this->conditionalExpressions, + $this->inClosureBindScopeClass, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $nativeExpressionTypes, + [], + $this->afterExtractCall, + $this->parentScope + ); + } + public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self { $exprType = $this->getType($expr); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index e441801ed3..826b433188 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -510,6 +510,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/eval-implicit-throw.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5628.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5501.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-5501.php b/tests/PHPStan/Analyser/data/bug-5501.php new file mode 100644 index 0000000000..13f5261152 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-5501.php @@ -0,0 +1,61 @@ +prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->damage = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + public function applyDamage2(int $amount): void + { + $this->prop2 = 5; + + if ($this->isBroken()){ + return; + } + + assertType('false', $this->isBroken()); + assertType('5', $this->prop2); + + $this->array['foo'] = min($this->damage + $amount, 5); + + assertType('bool', $this->isBroken()); + assertType('5', $this->prop2); + } + + protected function onBroken(): void + { + + } + + public function isBroken(): bool{ + return $this->damage >= 5; + } +}