Skip to content

Commit ea7072c

Browse files
authored
More precise property types after assignment when strict_types=0
1 parent 2ac87fc commit ea7072c

25 files changed

+1040
-21
lines changed

src/Analyser/NodeScopeResolver.php

+22-20
Original file line numberDiff line numberDiff line change
@@ -5628,24 +5628,25 @@ static function (): void {
56285628
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
56295629
if ($propertyReflection->canChangeTypeAfterAssignment()) {
56305630
if ($propertyReflection->hasNativeType()) {
5631-
$assignedNativeType = $scope->getNativeType($assignedExpr);
56325631
$propertyNativeType = $propertyReflection->getNativeType();
56335632

5634-
$assignedTypeIsCompatible = false;
5635-
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5636-
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
5637-
$assignedTypeIsCompatible = true;
5638-
break;
5633+
$assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
5634+
if (!$assignedTypeIsCompatible) {
5635+
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5636+
if ($type->isSuperTypeOf($assignedExprType)->yes()) {
5637+
$assignedTypeIsCompatible = true;
5638+
break;
5639+
}
56395640
}
56405641
}
56415642

56425643
if ($assignedTypeIsCompatible) {
5643-
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
5644-
} elseif ($scope->isDeclareStrictTypes()) {
5644+
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5645+
} else {
56455646
$scope = $scope->assignExpression(
56465647
$var,
5647-
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
5648-
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
5648+
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
5649+
TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
56495650
);
56505651
}
56515652
} else {
@@ -5716,24 +5717,25 @@ static function (): void {
57165717
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
57175718
if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
57185719
if ($propertyReflection->hasNativeType()) {
5719-
$assignedNativeType = $scope->getNativeType($assignedExpr);
57205720
$propertyNativeType = $propertyReflection->getNativeType();
5721+
$assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
57215722

5722-
$assignedTypeIsCompatible = false;
5723-
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5724-
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
5725-
$assignedTypeIsCompatible = true;
5726-
break;
5723+
if (!$assignedTypeIsCompatible) {
5724+
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
5725+
if ($type->isSuperTypeOf($assignedExprType)->yes()) {
5726+
$assignedTypeIsCompatible = true;
5727+
break;
5728+
}
57275729
}
57285730
}
57295731

57305732
if ($assignedTypeIsCompatible) {
5731-
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
5732-
} elseif ($scope->isDeclareStrictTypes()) {
5733+
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5734+
} else {
57335735
$scope = $scope->assignExpression(
57345736
$var,
5735-
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
5736-
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
5737+
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
5738+
TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
57375739
);
57385740
}
57395741
} else {

src/Type/Accessory/AccessoryLiteralStringType.php

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
3030
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
3131
use PHPStan\Type\Type;
32+
use PHPStan\Type\TypeCombinator;
3233
use PHPStan\Type\UnionType;
3334
use PHPStan\Type\VerbosityLevel;
3435

@@ -215,6 +216,10 @@ public function toArrayKey(): Type
215216

216217
public function toCoercedArgumentType(bool $strictTypes): Type
217218
{
219+
if (!$strictTypes) {
220+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
221+
}
222+
218223
return $this;
219224
}
220225

src/Type/Accessory/AccessoryLowercaseStringType.php

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
3030
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
3131
use PHPStan\Type\Type;
32+
use PHPStan\Type\TypeCombinator;
3233
use PHPStan\Type\UnionType;
3334
use PHPStan\Type\VerbosityLevel;
3435

@@ -212,6 +213,10 @@ public function toArrayKey(): Type
212213

213214
public function toCoercedArgumentType(bool $strictTypes): Type
214215
{
216+
if (!$strictTypes) {
217+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
218+
}
219+
215220
return $this;
216221
}
217222

src/Type/Accessory/AccessoryNonEmptyStringType.php

+4
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ public function toArrayKey(): Type
213213

214214
public function toCoercedArgumentType(bool $strictTypes): Type
215215
{
216+
if (!$strictTypes) {
217+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
218+
}
219+
216220
return $this;
217221
}
218222

src/Type/Accessory/AccessoryNonFalsyStringType.php

+4
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ public function toArrayKey(): Type
215215

216216
public function toCoercedArgumentType(bool $strictTypes): Type
217217
{
218+
if (!$strictTypes) {
219+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
220+
}
221+
218222
return $this;
219223
}
220224

src/Type/Accessory/AccessoryNumericStringType.php

+4
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ public function toArrayKey(): Type
215215

216216
public function toCoercedArgumentType(bool $strictTypes): Type
217217
{
218+
if (!$strictTypes) {
219+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
220+
}
221+
218222
return $this;
219223
}
220224

src/Type/Accessory/AccessoryUppercaseStringType.php

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
3030
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
3131
use PHPStan\Type\Type;
32+
use PHPStan\Type\TypeCombinator;
3233
use PHPStan\Type\UnionType;
3334
use PHPStan\Type\VerbosityLevel;
3435

@@ -212,6 +213,10 @@ public function toArrayKey(): Type
212213

213214
public function toCoercedArgumentType(bool $strictTypes): Type
214215
{
216+
if (!$strictTypes) {
217+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
218+
}
219+
215220
return $this;
216221
}
217222

src/Type/BooleanType.php

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ public function toArrayKey(): Type
113113

114114
public function toCoercedArgumentType(bool $strictTypes): Type
115115
{
116+
if (!$strictTypes) {
117+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this);
118+
}
119+
116120
return $this;
117121
}
118122

src/Type/CallableType.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use PHPStan\Reflection\Php\DummyParameter;
2525
use PHPStan\ShouldNotHappenException;
2626
use PHPStan\TrinaryLogic;
27+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
2728
use PHPStan\Type\Generic\TemplateType;
2829
use PHPStan\Type\Generic\TemplateTypeHelper;
2930
use PHPStan\Type\Generic\TemplateTypeMap;
@@ -331,7 +332,12 @@ public function toArrayKey(): Type
331332

332333
public function toCoercedArgumentType(bool $strictTypes): Type
333334
{
334-
return TypeCombinator::union($this, new StringType(), new ArrayType(new MixedType(true), new MixedType(true)), new ObjectType(Closure::class));
335+
return TypeCombinator::union(
336+
$this,
337+
TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()),
338+
new ArrayType(new MixedType(true), new MixedType(true)),
339+
new ObjectType(Closure::class),
340+
);
335341
}
336342

337343
public function isOffsetAccessLegal(): TrinaryLogic

src/Type/Constant/ConstantBooleanType.php

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Type\StaticTypeFactory;
1515
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
1616
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
1718
use PHPStan\Type\VerbosityLevel;
1819

1920
/** @api */
@@ -109,6 +110,10 @@ public function toArrayKey(): Type
109110

110111
public function toCoercedArgumentType(bool $strictTypes): Type
111112
{
113+
if (!$strictTypes) {
114+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this);
115+
}
116+
112117
return $this;
113118
}
114119

src/Type/Constant/ConstantIntegerType.php

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait;
1515
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
1616
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
1718
use PHPStan\Type\VerbosityLevel;
1819
use function abs;
1920
use function sprintf;
@@ -92,6 +93,15 @@ public function toArrayKey(): Type
9293
return $this;
9394
}
9495

96+
public function toCoercedArgumentType(bool $strictTypes): Type
97+
{
98+
if (!$strictTypes) {
99+
return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean());
100+
}
101+
102+
return TypeCombinator::union($this, $this->toFloat());
103+
}
104+
95105
public function generalize(GeneralizePrecision $precision): Type
96106
{
97107
return new IntegerType();

src/Type/FloatType.php

+4
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ public function toArrayKey(): Type
145145

146146
public function toCoercedArgumentType(bool $strictTypes): Type
147147
{
148+
if (!$strictTypes) {
149+
return TypeCombinator::union($this->toInteger(), $this, $this->toString(), $this->toBoolean());
150+
}
151+
148152
return $this;
149153
}
150154

src/Type/IntegerType.php

+4
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ public function toArrayKey(): Type
100100

101101
public function toCoercedArgumentType(bool $strictTypes): Type
102102
{
103+
if (!$strictTypes) {
104+
return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean());
105+
}
106+
103107
return TypeCombinator::union($this, $this->toFloat());
104108
}
105109

src/Type/ObjectType.php

+12
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,18 @@ public function toArrayKey(): Type
706706

707707
public function toCoercedArgumentType(bool $strictTypes): Type
708708
{
709+
if (!$strictTypes) {
710+
$classReflection = $this->getClassReflection();
711+
if (
712+
$classReflection === null
713+
|| !$classReflection->hasNativeMethod('__toString')
714+
) {
715+
return $this;
716+
}
717+
718+
return TypeCombinator::union($this, $this->toString());
719+
}
720+
709721
return $this;
710722
}
711723

src/Type/StringType.php

+7
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,13 @@ public function toArrayKey(): Type
183183

184184
public function toCoercedArgumentType(bool $strictTypes): Type
185185
{
186+
if (!$strictTypes) {
187+
if ($this->isNumericString()->no()) {
188+
return TypeCombinator::union($this, $this->toBoolean());
189+
}
190+
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
191+
}
192+
186193
return $this;
187194
}
188195

src/Type/Traits/ObjectTypeTrait.php

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use PHPStan\Type\MixedType;
2222
use PHPStan\Type\StringType;
2323
use PHPStan\Type\Type;
24+
use PHPStan\Type\TypeCombinator;
2425

2526
trait ObjectTypeTrait
2627
{
@@ -275,6 +276,10 @@ public function toArrayKey(): Type
275276

276277
public function toCoercedArgumentType(bool $strictTypes): Type
277278
{
279+
if (!$strictTypes) {
280+
return TypeCombinator::union($this, $this->toString());
281+
}
282+
278283
return $this;
279284
}
280285

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php // lint >= 8.4
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug12393Php84;
6+
7+
use function PHPStan\Testing\assertNativeType;
8+
use function PHPStan\Testing\assertType;
9+
10+
11+
class StringableFoo {
12+
private string $foo;
13+
14+
// https://3v4l.org/2SPPj#v8.4.6
15+
public function doFoo3(\BcMath\Number $foo): void {
16+
$this->foo = $foo;
17+
assertType('*NEVER*', $this->foo);
18+
}
19+
20+
public function __toString(): string {
21+
return 'Foo';
22+
}
23+
}

tests/PHPStan/Analyser/nsrt/bug-12393.php

+39
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,42 @@ public function getMixed()
143143
}
144144

145145
}
146+
147+
// https://3v4l.org/LK6Rh
148+
class CallableString {
149+
private string $foo;
150+
151+
public function doFoo(callable $foo): void {
152+
$this->foo = $foo; // PHPStorm wrongly reports an error on this line
153+
assertType('callable-string|non-empty-string', $this->foo);
154+
}
155+
}
156+
157+
// https://3v4l.org/WJ8NW
158+
class CallableArray {
159+
private array $foo;
160+
161+
public function doFoo(callable $foo): void {
162+
$this->foo = $foo;
163+
assertType('array', $this->foo); // could be non-empty-array
164+
}
165+
}
166+
167+
class StringableFoo {
168+
private string $foo;
169+
170+
// https://3v4l.org/DQSgA#v8.4.6
171+
public function doFoo(StringableFoo $foo): void {
172+
$this->foo = $foo;
173+
assertType('*NEVER*', $this->foo);
174+
}
175+
176+
public function doFoo2(NotStringable $foo): void {
177+
$this->foo = $foo;
178+
assertType('*NEVER*', $this->foo);
179+
}
180+
181+
public function __toString(): string {
182+
return 'Foo';
183+
}
184+
}

0 commit comments

Comments
 (0)