diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6910dfa051..756ea8db31 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5625,10 +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(); - $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)); } @@ -5696,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-non-strict.php b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php new file mode 100644 index 0000000000..d294016ec5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php @@ -0,0 +1,90 @@ += 8.1 + +declare(strict_types = 0); + +namespace Bug12902NonStrict; + +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; +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12902.php b/tests/PHPStan/Analyser/nsrt/bug-12902.php new file mode 100644 index 0000000000..cbdc816074 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12902.php @@ -0,0 +1,90 @@ += 8.1 + +declare(strict_types = 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; +} 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..ed949a846f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-non-nullable-property-non-strict.php @@ -0,0 +1,88 @@ += 8.1 + +declare(strict_types = 0); + +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; +}