diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index de3c4179b22..ec059692165 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1574,7 +1574,7 @@ public function enterPropertyHook( $realParameterTypes = $this->getRealParameterTypes($hook); - return $this->enterFunctionLike( + $scope = $this->enterFunctionLike( new PhpMethodFromParserNodeReflection( $this->getClassReflection(), $hook, @@ -1606,6 +1606,18 @@ public function enterPropertyHook( ), true, ); + + if ($hookName === 'set') { + $classReflection = $this->getClassReflection(); + if ( + !$classReflection->hasNativeProperty($propertyName) + || !$classReflection->getNativeProperty($propertyName)->getNativeReflection()->hasDefaultValue() + ) { + $scope->exitPropertyInitialization($propertyName); + } + } + + return $scope; } private function transformStaticType(Type $type): Type @@ -2832,6 +2844,15 @@ public function assignInitializedProperty(Type $fetchedOnType, string $propertyN return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType()); } + public function exitPropertyInitialization(string $propertyName): self + { + $initExprKey = $this->getNodeKey(new PropertyInitializationExpr($propertyName)); + unset($this->expressionTypes[$initExprKey]); + unset($this->nativeExpressionTypes[$initExprKey]); + + return $this; + } + public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self { $expressionTypes = $this->expressionTypes; diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f616b644ab3..25d46bdeaa2 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -712,6 +712,27 @@ public function processStmtNode( $classReflection = $scope->getClassReflection(); + if (!$isConstructor && !$stmt->isStatic()) { + $stackName = sprintf('%s::%s', $classReflection->getName(), $stmt->name->toString()); + $calledMethodScope = $this->calledMethodResults[$stackName] ?? null; + if ($calledMethodScope !== null) { + foreach ($calledMethodScope->expressionTypes as $typeHolder) { + $expr = $typeHolder->getExpr(); + if (!$expr instanceof PropertyInitializationExpr) { + continue; + } + $propertyName = $expr->getPropertyName(); + if ( + $classReflection->hasNativeProperty($propertyName) + && $classReflection->getNativeProperty($propertyName)->getNativeReflection()->hasDefaultValue() + ) { + continue; + } + $methodScope = $methodScope->exitPropertyInitialization($propertyName); + } + } + } + if ($isConstructor) { foreach ($stmt->params as $param) { if ($param->flags === 0 && $param->hooks === []) { diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index fd841e49b16..38aafa7016b 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -562,4 +562,29 @@ public function testBug14393(): void ]); } + #[RequiresPhp('>= 8.4')] + public function testBug13473(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-13473.php'], [ + [ + 'Property Bug13473\FooWithDefault::$bar in isset() is not nullable nor uninitialized.', + 28, + ], + ]); + } + + public function testBug13473Method(): void + { + $this->treatPhpDocTypesAsCertain = true; + + $this->analyse([__DIR__ . '/data/bug-13473-method.php'], [ + [ + 'Property Bug13473Method\FooWithDefault::$bar in isset() is not nullable nor uninitialized.', + 34, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/bug-13473-method.php b/tests/PHPStan/Rules/Variables/data/bug-13473-method.php new file mode 100644 index 00000000000..893fc71a966 --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13473-method.php @@ -0,0 +1,39 @@ +setBar($bar); + } + + public function setBar(int $bar): void + { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } +} + +class FooWithDefault { + private int $bar = 1; + + public function __construct(int $bar) + { + $this->setBar($bar); + } + + public function setBar(int $bar): void + { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } +} diff --git a/tests/PHPStan/Rules/Variables/data/bug-13473.php b/tests/PHPStan/Rules/Variables/data/bug-13473.php new file mode 100644 index 00000000000..cdafdff10fe --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/bug-13473.php @@ -0,0 +1,39 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug13473; + +class Foo { + private(set) int $bar { + get => $this->bar; + set(int $bar) { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } + } + + public function __construct(int $bar) + { + $this->bar = $bar; + } +} + +class FooWithDefault { + private(set) int $bar = 1 { + get => $this->bar; + set(int $bar) { + if (isset($this->bar)) { + throw new \Exception('bar is set'); + } + $this->bar = $bar; + } + } + + public function __construct(int $bar) + { + $this->bar = $bar; + } +}