Skip to content
Merged
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