Skip to content

Commit 67fb7a6

Browse files
authored
Fix imprecise property native types after assignment
1 parent d831c93 commit 67fb7a6

File tree

4 files changed

+306
-4
lines changed

4 files changed

+306
-4
lines changed

Diff for: src/Analyser/NodeScopeResolver.php

+38-4
Original file line numberDiff line numberDiff line change
@@ -5625,10 +5625,27 @@ static function (): void {
56255625
$assignedExprType = $scope->getType($assignedExpr);
56265626
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
56275627
if ($propertyReflection->canChangeTypeAfterAssignment()) {
5628-
if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) {
5628+
if ($propertyReflection->hasNativeType()) {
5629+
$assignedNativeType = $scope->getNativeType($assignedExpr);
56295630
$propertyNativeType = $propertyReflection->getNativeType();
56305631

5631-
$scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType));
5632+
$assignedTypeIsCompatible = false;
5633+
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5634+
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
5635+
$assignedTypeIsCompatible = true;
5636+
break;
5637+
}
5638+
}
5639+
5640+
if ($assignedTypeIsCompatible) {
5641+
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
5642+
} elseif ($scope->isDeclareStrictTypes()) {
5643+
$scope = $scope->assignExpression(
5644+
$var,
5645+
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
5646+
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
5647+
);
5648+
}
56325649
} else {
56335650
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
56345651
}
@@ -5696,10 +5713,27 @@ static function (): void {
56965713
$assignedExprType = $scope->getType($assignedExpr);
56975714
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
56985715
if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
5699-
if ($propertyReflection->hasNativeType() && $scope->isDeclareStrictTypes()) {
5716+
if ($propertyReflection->hasNativeType()) {
5717+
$assignedNativeType = $scope->getNativeType($assignedExpr);
57005718
$propertyNativeType = $propertyReflection->getNativeType();
57015719

5702-
$scope = $scope->assignExpression($var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType));
5720+
$assignedTypeIsCompatible = false;
5721+
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5722+
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
5723+
$assignedTypeIsCompatible = true;
5724+
break;
5725+
}
5726+
}
5727+
5728+
if ($assignedTypeIsCompatible) {
5729+
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
5730+
} elseif ($scope->isDeclareStrictTypes()) {
5731+
$scope = $scope->assignExpression(
5732+
$var,
5733+
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
5734+
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
5735+
);
5736+
}
57035737
} else {
57045738
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
57055739
}

Diff for: tests/PHPStan/Analyser/nsrt/bug-12902-non-strict.php

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 0);
4+
5+
namespace Bug12902NonStrict;
6+
7+
use function PHPStan\Testing\assertNativeType;
8+
use function PHPStan\Testing\assertType;
9+
10+
class NarrowsNativeConstantValue
11+
{
12+
private readonly int|float $i;
13+
14+
public function __construct()
15+
{
16+
$this->i = 1;
17+
}
18+
19+
public function doFoo(): void
20+
{
21+
assertType('1', $this->i);
22+
assertNativeType('1', $this->i);
23+
}
24+
}
25+
26+
class NarrowsNativeReadonlyUnion {
27+
private readonly int|float $i;
28+
29+
public function __construct()
30+
{
31+
$this->i = getInt();
32+
assertType('int', $this->i);
33+
assertNativeType('int', $this->i);
34+
}
35+
36+
public function doFoo(): void {
37+
assertType('int', $this->i);
38+
assertNativeType('int', $this->i);
39+
}
40+
}
41+
42+
class NarrowsNativeUnion {
43+
private int|float $i;
44+
45+
public function __construct()
46+
{
47+
$this->i = getInt();
48+
assertType('int', $this->i);
49+
assertNativeType('int', $this->i);
50+
51+
$this->impureCall();
52+
assertType('float|int', $this->i);
53+
assertNativeType('float|int', $this->i);
54+
}
55+
56+
public function doFoo(): void {
57+
assertType('float|int', $this->i);
58+
assertNativeType('float|int', $this->i);
59+
}
60+
61+
/** @phpstan-impure */
62+
public function impureCall(): void {}
63+
}
64+
65+
class NarrowsStaticNativeUnion {
66+
private static int|float $i;
67+
68+
public function __construct()
69+
{
70+
self::$i = getInt();
71+
assertType('int', self::$i);
72+
assertNativeType('int', self::$i);
73+
74+
$this->impureCall();
75+
assertType('int', self::$i); // should be float|int
76+
assertNativeType('int', self::$i); // should be float|int
77+
}
78+
79+
public function doFoo(): void {
80+
assertType('float|int', self::$i);
81+
assertNativeType('float|int', self::$i);
82+
}
83+
84+
/** @phpstan-impure */
85+
public function impureCall(): void {}
86+
}
87+
88+
function getInt(): int {
89+
return 1;
90+
}

Diff for: tests/PHPStan/Analyser/nsrt/bug-12902.php

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12902;
6+
7+
use function PHPStan\Testing\assertNativeType;
8+
use function PHPStan\Testing\assertType;
9+
10+
class NarrowsNativeConstantValue
11+
{
12+
private readonly int|float $i;
13+
14+
public function __construct()
15+
{
16+
$this->i = 1;
17+
}
18+
19+
public function doFoo(): void
20+
{
21+
assertType('1', $this->i);
22+
assertNativeType('1', $this->i);
23+
}
24+
}
25+
26+
class NarrowsNativeReadonlyUnion {
27+
private readonly int|float $i;
28+
29+
public function __construct()
30+
{
31+
$this->i = getInt();
32+
assertType('int', $this->i);
33+
assertNativeType('int', $this->i);
34+
}
35+
36+
public function doFoo(): void {
37+
assertType('int', $this->i);
38+
assertNativeType('int', $this->i);
39+
}
40+
}
41+
42+
class NarrowsNativeUnion {
43+
private int|float $i;
44+
45+
public function __construct()
46+
{
47+
$this->i = getInt();
48+
assertType('int', $this->i);
49+
assertNativeType('int', $this->i);
50+
51+
$this->impureCall();
52+
assertType('float|int', $this->i);
53+
assertNativeType('float|int', $this->i);
54+
}
55+
56+
public function doFoo(): void {
57+
assertType('float|int', $this->i);
58+
assertNativeType('float|int', $this->i);
59+
}
60+
61+
/** @phpstan-impure */
62+
public function impureCall(): void {}
63+
}
64+
65+
class NarrowsStaticNativeUnion {
66+
private static int|float $i;
67+
68+
public function __construct()
69+
{
70+
self::$i = getInt();
71+
assertType('int', self::$i);
72+
assertNativeType('int', self::$i);
73+
74+
$this->impureCall();
75+
assertType('int', self::$i); // should be float|int
76+
assertNativeType('int', self::$i); // should be float|int
77+
}
78+
79+
public function doFoo(): void {
80+
assertType('float|int', self::$i);
81+
assertNativeType('float|int', self::$i);
82+
}
83+
84+
/** @phpstan-impure */
85+
public function impureCall(): void {}
86+
}
87+
88+
function getInt(): int {
89+
return 1;
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 0);
4+
5+
namespace RememberNonNullablePropertyWhenStrictTypesDisabled;
6+
7+
use function PHPStan\Testing\assertNativeType;
8+
use function PHPStan\Testing\assertType;
9+
10+
class KeepsPropertyNonNullable {
11+
private readonly int $i;
12+
13+
public function __construct()
14+
{
15+
$this->i = getIntOrNull();
16+
}
17+
18+
public function doFoo(): void {
19+
assertType('int', $this->i);
20+
assertNativeType('int', $this->i);
21+
}
22+
}
23+
24+
class DontCoercePhpdocType {
25+
/** @var int */
26+
private $i;
27+
28+
public function __construct()
29+
{
30+
$this->i = getIntOrNull();
31+
}
32+
33+
public function doFoo(): void {
34+
assertType('int', $this->i);
35+
assertNativeType('mixed', $this->i);
36+
}
37+
}
38+
39+
function getIntOrNull(): ?int {
40+
if (rand(0, 1) === 0) {
41+
return null;
42+
}
43+
return 1;
44+
}
45+
46+
47+
class KeepsPropertyNonNullable2 {
48+
private int|float $i;
49+
50+
public function __construct()
51+
{
52+
$this->i = getIntOrFloatOrNull();
53+
}
54+
55+
public function doFoo(): void {
56+
assertType('float|int', $this->i);
57+
assertNativeType('float|int', $this->i);
58+
}
59+
}
60+
61+
function getIntOrFloatOrNull(): null|int|float {
62+
if (rand(0, 1) === 0) {
63+
return null;
64+
}
65+
66+
if (rand(0, 10) === 0) {
67+
return 1.0;
68+
}
69+
return 1;
70+
}
71+
72+
class NarrowsNativeUnion {
73+
private readonly int|float $i;
74+
75+
public function __construct()
76+
{
77+
$this->i = getInt();
78+
}
79+
80+
public function doFoo(): void {
81+
assertType('int', $this->i);
82+
assertNativeType('int', $this->i);
83+
}
84+
}
85+
86+
function getInt(): int {
87+
return 1;
88+
}

0 commit comments

Comments
 (0)