Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -1574,7 +1574,7 @@ public function enterPropertyHook(

$realParameterTypes = $this->getRealParameterTypes($hook);

return $this->enterFunctionLike(
$scope = $this->enterFunctionLike(
new PhpMethodFromParserNodeReflection(
$this->getClassReflection(),
$hook,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
21 changes: 21 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 === []) {
Expand Down
25 changes: 25 additions & 0 deletions tests/PHPStan/Rules/Variables/IssetRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
39 changes: 39 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-13473-method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types = 1);

namespace Bug13473Method;

class Foo {
private int $bar;

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;
}
}

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;
}
}
39 changes: 39 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-13473.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php // lint >= 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;
}
}
Loading