Skip to content

Fix imprecise types after assignment when strict-types=1 #3945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 19, 2025
42 changes: 38 additions & 4 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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));
}
Expand Down
90 changes: 90 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php // lint >= 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
Comment on lines +74 to +76
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a unrelated bug, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix in #3950

}

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;
}
90 changes: 90 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12902.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php // lint >= 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php // lint >= 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;
}
Loading