From ad88033c234d042ce489a69080f495db40310fc2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 12:14:12 +0200 Subject: [PATCH 01/16] Fix imprecise types after assignment when strict-types=1 --- tests/PHPStan/Analyser/nsrt/bug-12902.php | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12902.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php new file mode 100644 index 0000000000..9d7a396ea7 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -0,0 +1,40 @@ +i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class NarrowsStaticNativeUnion { + private static int|float $i; + + public function __construct() + { + self::$i = getInt(); + assertType('int', self::$i); + assertNativeType('int', self::$i); + } + + public function doFoo(): void { + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } +} + +function getInt(): int { + return 1; +} From 96f8a786f9fc0e38951819368d71566feba07d1b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 12:15:15 +0200 Subject: [PATCH 02/16] fix --- src/Analyser/NodeScopeResolver.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6910dfa051..73d160754e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5626,9 +5626,20 @@ static function (): void { $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { + $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType)); + $newAssignedType = TypeCombinator::intersect($assignedExprType, $propertyNativeType); + if ($newAssignedType instanceof NeverType) { + $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); + } + + $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType, $propertyNativeType); + if ($newAssignedNativeType instanceof NeverType) { + $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); + } + + $scope = $scope->assignExpression($var, $newAssignedType, $newAssignedNativeType); } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } @@ -5697,9 +5708,20 @@ static function (): void { $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { + $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType)); + $newAssignedType = TypeCombinator::intersect($assignedExprType, $propertyNativeType); + if ($newAssignedType instanceof NeverType) { + $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); + } + + $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType, $propertyNativeType); + if ($newAssignedNativeType instanceof NeverType) { + $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); + } + + $scope = $scope->assignExpression($var, $newAssignedType, $newAssignedNativeType); } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } From 602ff9c391dd14350460dfed7a84601db91a0982 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 12:16:43 +0200 Subject: [PATCH 03/16] namespace --- tests/PHPStan/Analyser/nsrt/bug-12902.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 9d7a396ea7..a50aab9419 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -1,5 +1,7 @@ Date: Fri, 18 Apr 2025 12:23:02 +0200 Subject: [PATCH 04/16] Update bug-12902.php --- tests/PHPStan/Analyser/nsrt/bug-12902.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index a50aab9419..bcf53681e9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -29,12 +29,20 @@ public function __construct() self::$i = getInt(); assertType('int', self::$i); assertNativeType('int', self::$i); + + $this->impureCall(); + + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); } public function doFoo(): void { assertType('float|int', self::$i); assertNativeType('float|int', self::$i); } + + /** @phpstan-impure */ + public function impureCall(): void {} } function getInt(): int { From e78b85aa3119a263137b4404576bbaa99454260a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 12:27:23 +0200 Subject: [PATCH 05/16] works only on instance methods --- src/Analyser/NodeScopeResolver.php | 11 ++------- tests/PHPStan/Analyser/nsrt/bug-12902.php | 30 ++++++++++++++++++++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 73d160754e..a780ff2050 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5711,15 +5711,8 @@ static function (): void { $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $newAssignedType = TypeCombinator::intersect($assignedExprType, $propertyNativeType); - if ($newAssignedType instanceof NeverType) { - $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); - } - - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType, $propertyNativeType); - if ($newAssignedNativeType instanceof NeverType) { - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); - } + $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); + $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); $scope = $scope->assignExpression($var, $newAssignedType, $newAssignedNativeType); } else { diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index bcf53681e9..40282b48c4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -5,7 +5,7 @@ use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; -class NarrowsNativeUnion { +class NarrowsNativeReadonlyUnion { private readonly int|float $i; public function __construct() @@ -21,17 +21,39 @@ public function doFoo(): void { } } +class NarrowsNativeUnion { + private int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + + $this->impureCall();; + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + class NarrowsStaticNativeUnion { private static int|float $i; public function __construct() { self::$i = getInt(); - assertType('int', self::$i); - assertNativeType('int', self::$i); + assertType('float|int', self::$i); // could be int + assertNativeType('float|int', self::$i); // could be int $this->impureCall(); - assertType('float|int', self::$i); assertNativeType('float|int', self::$i); } From 7b436383945ed1ce2833c44e379e17308cb56993 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 12:29:06 +0200 Subject: [PATCH 06/16] Update bug-12902.php --- tests/PHPStan/Analyser/nsrt/bug-12902.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 40282b48c4..54c3441284 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -1,4 +1,4 @@ -= 8.1 namespace Bug12902; @@ -30,7 +30,7 @@ public function __construct() assertType('int', $this->i); assertNativeType('int', $this->i); - $this->impureCall();; + $this->impureCall(); assertType('float|int', $this->i); assertNativeType('float|int', $this->i); } From eeb6150ea2512d2066bee26b1fd6c47064391c3a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 18 Apr 2025 11:24:56 +0200 Subject: [PATCH 07/16] TypeInferenceTestCase: support declare(); before // lint --- src/Testing/TypeInferenceTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index 0d7e0306d1..e45110b51c 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -398,7 +398,7 @@ private static function isFileLintSkipped(string $file): bool @fclose($f); - if (preg_match('~ Date: Fri, 18 Apr 2025 12:47:44 +0200 Subject: [PATCH 08/16] coerce native/non-native both or none --- src/Analyser/NodeScopeResolver.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a780ff2050..d3e0e6728e 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5630,12 +5630,9 @@ static function (): void { $propertyNativeType = $propertyReflection->getNativeType(); $newAssignedType = TypeCombinator::intersect($assignedExprType, $propertyNativeType); - if ($newAssignedType instanceof NeverType) { - $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); - } - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType, $propertyNativeType); - if ($newAssignedNativeType instanceof NeverType) { + if ($newAssignedType instanceof NeverType || $newAssignedNativeType instanceof NeverType) { + $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); } From 75fafa7dcdf133890c0d6615d2eea0146031b9ff Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:00:20 +0200 Subject: [PATCH 09/16] implement feedback --- src/Analyser/NodeScopeResolver.php | 23 ++++++++++++++++------- tests/PHPStan/Analyser/nsrt/bug-12902.php | 22 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index d3e0e6728e..2bd7324a40 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5625,18 +5625,27 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { - if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { + if ($propertyReflection->hasNativeType()) { $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $newAssignedType = TypeCombinator::intersect($assignedExprType, $propertyNativeType); - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType, $propertyNativeType); - if ($newAssignedType instanceof NeverType || $newAssignedNativeType instanceof NeverType) { - $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); + $assignedTypeIsCompatible = false; + foreach(TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedNativeType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } } - $scope = $scope->assignExpression($var, $newAssignedType, $newAssignedNativeType); + if (!$assignedTypeIsCompatible && $scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType) + ); + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 54c3441284..5315a51206 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -5,6 +5,28 @@ use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; +class NarrowsNativeConstantValue +{ + private readonly int|float $i; + + public function __construct() + { + $this->i = 1; + } + + public function doFoo(): void + { + assertType('1', $this->i); + assertNativeType('1', $this->i); + } +} + +function getInt(): int +{ + return 1; +} + + class NarrowsNativeReadonlyUnion { private readonly int|float $i; From be51451db5ec78769735d3663ce1b8374b7071e2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:03:43 +0200 Subject: [PATCH 10/16] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 2bd7324a40..7874680e73 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5630,7 +5630,7 @@ static function (): void { $propertyNativeType = $propertyReflection->getNativeType(); $assignedTypeIsCompatible = false; - foreach(TypeUtils::flattenTypes($propertyNativeType) as $type) { + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { if ($type->isSuperTypeOf($assignedNativeType)->yes()) { $assignedTypeIsCompatible = true; break; @@ -5641,7 +5641,7 @@ static function (): void { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType) + TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), ); } else { $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); @@ -5714,13 +5714,9 @@ static function (): void { $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { - $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $newAssignedType = TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType); - $newAssignedNativeType = TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType); - - $scope = $scope->assignExpression($var, $newAssignedType, $newAssignedNativeType); + $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType)); } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } From c6f0e16071bbd0aed5ddf3e0027e9a12c9c353d7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:07:08 +0200 Subject: [PATCH 11/16] fix name collision --- tests/PHPStan/Analyser/nsrt/bug-12902.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 5315a51206..6abfc67183 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -21,12 +21,6 @@ public function doFoo(): void } } -function getInt(): int -{ - return 1; -} - - class NarrowsNativeReadonlyUnion { private readonly int|float $i; From d0a40f8cb5c02cf84ea096ff2da771b6f19e2be9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:38:44 +0200 Subject: [PATCH 12/16] fix --- src/Analyser/NodeScopeResolver.php | 6 +- .../Analyser/nsrt/bug-12902-non-strict.php | 88 +++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7874680e73..36ebbadb11 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5637,14 +5637,14 @@ static function (): void { } } - if (!$assignedTypeIsCompatible && $scope->isDeclareStrictTypes()) { + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } elseif ($scope->isDeclareStrictTypes()) { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), ); - } else { - $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php new file mode 100644 index 0000000000..b2f560aaac --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug12902; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class NarrowsNativeConstantValue +{ + private readonly int|float $i; + + public function __construct() + { + $this->i = 1; + } + + public function doFoo(): void + { + assertType('1', $this->i); + assertNativeType('1', $this->i); + } +} + +class NarrowsNativeReadonlyUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class NarrowsNativeUnion { + private int|float $i; + + public function __construct() + { + $this->i = getInt(); + assertType('int', $this->i); + assertNativeType('int', $this->i); + + $this->impureCall(); + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +class NarrowsStaticNativeUnion { + private static int|float $i; + + public function __construct() + { + self::$i = getInt(); + assertType('int', self::$i); + assertNativeType('int', self::$i); + + $this->impureCall(); + assertType('int', self::$i); // should be float|int + assertNativeType('int', self::$i); // should be float|int + } + + public function doFoo(): void { + assertType('float|int', self::$i); + assertNativeType('float|int', self::$i); + } + + /** @phpstan-impure */ + public function impureCall(): void {} +} + +function getInt(): int { + return 1; +} From 7fdb85b3a8088f57c47d315112afd6928f5a92bc Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:42:20 +0200 Subject: [PATCH 13/16] static property fetch --- src/Analyser/NodeScopeResolver.php | 21 +++++++++++++++++++-- tests/PHPStan/Analyser/nsrt/bug-12902.php | 8 ++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 36ebbadb11..756ea8db31 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5713,10 +5713,27 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { + if ($propertyReflection->hasNativeType()) { + $assignedNativeType = $scope->getNativeType($assignedExpr); $propertyNativeType = $propertyReflection->getNativeType(); - $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType)); + $assignedTypeIsCompatible = false; + foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) { + if ($type->isSuperTypeOf($assignedNativeType)->yes()) { + $assignedTypeIsCompatible = true; + break; + } + } + + if ($assignedTypeIsCompatible) { + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } elseif ($scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), + ); + } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 6abfc67183..0975b0b9f0 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -66,12 +66,12 @@ class NarrowsStaticNativeUnion { public function __construct() { self::$i = getInt(); - assertType('float|int', self::$i); // could be int - assertNativeType('float|int', self::$i); // could be int + assertType('int', self::$i); + assertNativeType('int', self::$i); $this->impureCall(); - assertType('float|int', self::$i); - assertNativeType('float|int', self::$i); + assertType('int', self::$i); // should be float|int + assertNativeType('int', self::$i); // should be float|int } public function doFoo(): void { From 52e4b42307849e40503d0bc37b6bfdced7a110ea Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 09:45:59 +0200 Subject: [PATCH 14/16] test nullable --- .../Analyser/nsrt/bug-12902-non-strict.php | 2 +- ...ember-non-nullable-property-non-strict.php | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php index b2f560aaac..41f8b9a337 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php @@ -1,6 +1,6 @@ = 8.1 -namespace Bug12902; +namespace Bug12902NonStrict; use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; diff --git a/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php new file mode 100644 index 0000000000..cb808d96d4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php @@ -0,0 +1,86 @@ += 8.1 + +namespace RememberNonNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class KeepsPropertyNonNullable { + private readonly int $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +class DontCoercePhpdocType { + /** @var int */ + private $i; + + public function __construct() + { + $this->i = getIntOrNull(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('mixed', $this->i); + } +} + +function getIntOrNull(): ?int { + if (rand(0, 1) === 0) { + return null; + } + return 1; +} + + +class KeepsPropertyNonNullable2 { + private int|float $i; + + public function __construct() + { + $this->i = getIntOrFloatOrNull(); + } + + public function doFoo(): void { + assertType('float|int', $this->i); + assertNativeType('float|int', $this->i); + } +} + +function getIntOrFloatOrNull(): null|int|float { + if (rand(0, 1) === 0) { + return null; + } + + if (rand(0, 10) === 0) { + return 1.0; + } + return 1; +} + +class NarrowsNativeUnion { + private readonly int|float $i; + + public function __construct() + { + $this->i = getInt(); + } + + public function doFoo(): void { + assertType('int', $this->i); + assertNativeType('int', $this->i); + } +} + +function getInt(): int { + return 1; +} From 179f0d9d075f3495e3ffdc2e84e34baa880620f8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 19 Apr 2025 10:34:34 +0200 Subject: [PATCH 15/16] Discard changes to src/Testing/TypeInferenceTestCase.php --- src/Testing/TypeInferenceTestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Testing/TypeInferenceTestCase.php b/src/Testing/TypeInferenceTestCase.php index e45110b51c..0d7e0306d1 100644 --- a/src/Testing/TypeInferenceTestCase.php +++ b/src/Testing/TypeInferenceTestCase.php @@ -398,7 +398,7 @@ private static function isFileLintSkipped(string $file): bool @fclose($f); - if (preg_match('~ Date: Sat, 19 Apr 2025 10:35:27 +0200 Subject: [PATCH 16/16] fix lint condition --- tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php | 4 +++- tests/PHPStan/Analyser/nsrt/bug-12902.php | 4 +++- .../nsrt/remember-non-nullable-property-non-strict.php | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php index 41f8b9a337..d294016ec5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 0); namespace Bug12902NonStrict; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php index 0975b0b9f0..cbdc816074 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12902.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 1); namespace Bug12902; diff --git a/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php index cb808d96d4..ed949a846f 100644 --- a/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php +++ b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php @@ -1,4 +1,6 @@ -= 8.1 += 8.1 + +declare(strict_types = 0); namespace RememberNonNullablePropertyWhenStrictTypesDisabled;