diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 7e12003bcca..1b1e6f614f6 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -174,6 +174,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex } } else { + if ($expr->name instanceof Expr) { + $scope = $scope->invalidateAllOnExpression($normalizedExpr->var); + } $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr); } $hasYield = $hasYield || $result->hasYield(); diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index c7f8b76580b..afb2faa7a73 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3350,6 +3350,48 @@ public function invalidateExpression(Expr $expressionToInvalidate, bool $require ); } + public function invalidateAllOnExpression(Expr $expressionToInvalidate): self + { + $expressionTypes = $this->expressionTypes; + $nativeExpressionTypes = $this->nativeExpressionTypes; + $invalidated = false; + $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate); + + foreach ($expressionTypes as $exprString => $exprTypeHolder) { + $exprExpr = $exprTypeHolder->getExpr(); + if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, true)) { + continue; + } + + unset($expressionTypes[$exprString]); + unset($nativeExpressionTypes[$exprString]); + $invalidated = true; + } + + if (!$invalidated) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $expressionTypes, + $nativeExpressionTypes, + $this->conditionalExpressions, + $this->inClosureBindScopeClasses, + $this->anonymousFunctionReflection, + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions, + $this->currentlyAllowedUndefinedExpressions, + [], + $this->afterExtractCall, + $this->parentScope, + $this->nativeTypesPromoted, + ); + } + private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool { if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) { diff --git a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php index 3d8320a53a1..4b647aa1748 100644 --- a/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php @@ -288,4 +288,9 @@ public function testBug13874(): void $this->analyse([__DIR__ . '/data/bug-13874.php'], []); } + public function testBug3831(): void + { + $this->analyse([__DIR__ . '/data/bug-3831.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-3831.php b/tests/PHPStan/Rules/Comparison/data/bug-3831.php new file mode 100644 index 00000000000..d46e9dd0171 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-3831.php @@ -0,0 +1,27 @@ + */ + public $footer = []; + + public function render() : string { + $content = ''; + $this->footer = []; + + // dynamic method call - could modify $this->footer + $this->{'compileSection'}(); + + if (count($this->footer) > 0) { + $content = str_replace('some', 'thing', $content); + } + return $content; + } + + private function compileSection(): void { + $this->footer[] = 'section-name'; + } + +}