diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6910dfa051..57337bc218 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5625,12 +5625,22 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection->canChangeTypeAfterAssignment()) { - if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { - $propertyNativeType = $propertyReflection->getNativeType(); + $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)); + if ($propertyReflection->hasNativeType()) { + if ($scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType)); + } elseif ($propertyNativeType->isNull()->no()) { + $assignedExprType = TypeCombinator::removeNull($assignedExprType); + $assignedNativeType = TypeCombinator::removeNull($assignedNativeType); + + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); } } $declaringClass = $propertyReflection->getDeclaringClass(); @@ -5696,12 +5706,22 @@ static function (): void { $assignedExprType = $scope->getType($assignedExpr); $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope); if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) { - if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) { - $propertyNativeType = $propertyReflection->getNativeType(); + $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)); + if ($propertyReflection->hasNativeType()) { + if ($scope->isDeclareStrictTypes()) { + $scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType)); + } elseif ($propertyNativeType->isNull()->no()) { + $assignedExprType = TypeCombinator::removeNull($assignedExprType); + $assignedNativeType = TypeCombinator::removeNull($assignedNativeType); + + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); + } } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType); } } } else { 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('~= 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; +}