Skip to content

More precise types after assignment when strict-types=0 #3965

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 38 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
475c75e
tests
ondrejmirtes Apr 29, 2025
f1b04fc
Fix assigning properties
ondrejmirtes Apr 29, 2025
e1455c0
fix failling tests
staabm Apr 29, 2025
536af14
fix bools
staabm Apr 29, 2025
47a1d73
fix floats
staabm Apr 29, 2025
6c4aea1
Update StringType.php
staabm Apr 29, 2025
cebbfcb
test int range
staabm Apr 29, 2025
6f6bca9
test null coerce to int
staabm Apr 29, 2025
411e447
test union and intersection
staabm Apr 29, 2025
2625bb8
Update bug-12393b.php
staabm Apr 29, 2025
72aa6ab
test mixed, array
staabm Apr 29, 2025
7a95a46
fix php 7.4 build
staabm Apr 29, 2025
4d20e2a
more precise ints
staabm Apr 29, 2025
ccb1889
more precise bools
staabm Apr 29, 2025
78e8df9
more precise floats
staabm Apr 29, 2025
872af82
more precise strings
staabm Apr 29, 2025
1a8c921
cs
staabm Apr 29, 2025
3a64515
more precise strings
staabm Apr 29, 2025
2c75fb4
simplify
staabm Apr 30, 2025
65b9d3b
more tests
staabm Apr 30, 2025
604aed1
even more string tests
staabm Apr 30, 2025
016c260
more bool tests
staabm Apr 30, 2025
da17fe4
remove outdated comment
staabm Apr 30, 2025
9e815d3
Added regression test
staabm Apr 30, 2025
76e1258
test array accessories and HasOffset*Type
staabm May 1, 2025
4bb13d8
support Stringable objects
staabm May 1, 2025
d338a47
test mix of array value types
staabm May 1, 2025
9d19f30
fix has_method('__toString') coercison
staabm May 1, 2025
0560ae2
suport callable arrays
staabm May 1, 2025
113c7d4
test void can be null
staabm May 1, 2025
3d34649
support callable as string
staabm May 1, 2025
c16582a
fix callable strings
staabm May 1, 2025
c6db4e7
test array-accessory to X
staabm May 1, 2025
61e0eea
added strict-type test-variants for callable array/string
staabm May 1, 2025
a8abeae
test not stringable and bcmath
staabm May 1, 2025
016d44a
Update bug-12393b.php
staabm May 1, 2025
95186fe
test stringable in strict-types
staabm May 1, 2025
b14719a
separated php 8.4 tests
staabm May 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 22 additions & 20 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5628,24 +5628,25 @@ static function (): void {
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
if ($propertyReflection->canChangeTypeAfterAssignment()) {
if ($propertyReflection->hasNativeType()) {
$assignedNativeType = $scope->getNativeType($assignedExpr);
$propertyNativeType = $propertyReflection->getNativeType();

$assignedTypeIsCompatible = false;
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
$assignedTypeIsCompatible = true;
break;
$assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
if (!$assignedTypeIsCompatible) {
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
if ($type->isSuperTypeOf($assignedExprType)->yes()) {
$assignedTypeIsCompatible = true;
break;
}
}
}

if ($assignedTypeIsCompatible) {
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
} elseif ($scope->isDeclareStrictTypes()) {
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
} else {
$scope = $scope->assignExpression(
$var,
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
);
}
} else {
Expand Down Expand Up @@ -5716,24 +5717,25 @@ static function (): void {
$nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
if ($propertyReflection->hasNativeType()) {
$assignedNativeType = $scope->getNativeType($assignedExpr);
$propertyNativeType = $propertyReflection->getNativeType();
$assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();

$assignedTypeIsCompatible = false;
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
if ($type->isSuperTypeOf($assignedNativeType)->yes()) {
$assignedTypeIsCompatible = true;
break;
if (!$assignedTypeIsCompatible) {
foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
if ($type->isSuperTypeOf($assignedExprType)->yes()) {
$assignedTypeIsCompatible = true;
break;
}
}
}

if ($assignedTypeIsCompatible) {
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
} elseif ($scope->isDeclareStrictTypes()) {
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
} else {
$scope = $scope->assignExpression(
$var,
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType),
TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
);
}
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/AccessoryLiteralStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;

Expand Down Expand Up @@ -215,6 +216,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/AccessoryLowercaseStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;

Expand Down Expand Up @@ -212,6 +213,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/Accessory/AccessoryNonEmptyStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/Accessory/AccessoryNonFalsyStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/Accessory/AccessoryNumericStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Type/Accessory/AccessoryUppercaseStringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;

Expand Down Expand Up @@ -212,6 +213,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/BooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this);
}

return $this;
}

Expand Down
8 changes: 7 additions & 1 deletion src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
Expand Down Expand Up @@ -331,7 +332,12 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
return TypeCombinator::union($this, new StringType(), new ArrayType(new MixedType(true), new MixedType(true)), new ObjectType(Closure::class));
return TypeCombinator::union(
$this,
TypeCombinator::intersect(new StringType(), new AccessoryNonEmptyStringType()),
new ArrayType(new MixedType(true), new MixedType(true)),
new ObjectType(Closure::class),
);
}

public function isOffsetAccessLegal(): TrinaryLogic
Expand Down
5 changes: 5 additions & 0 deletions src/Type/Constant/ConstantBooleanType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Type\StaticTypeFactory;
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VerbosityLevel;

/** @api */
Expand Down Expand Up @@ -109,6 +110,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this);
}

return $this;
}

Expand Down
10 changes: 10 additions & 0 deletions src/Type/Constant/ConstantIntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait;
use PHPStan\Type\Traits\ConstantScalarTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\VerbosityLevel;
use function abs;
use function sprintf;
Expand Down Expand Up @@ -92,6 +93,15 @@ public function toArrayKey(): Type
return $this;
}

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean());
}

return TypeCombinator::union($this, $this->toFloat());
}

public function generalize(GeneralizePrecision $precision): Type
{
return new IntegerType();
Expand Down
4 changes: 4 additions & 0 deletions src/Type/FloatType.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this->toInteger(), $this, $this->toString(), $this->toBoolean());
}

return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/IntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean());
}

return TypeCombinator::union($this, $this->toFloat());
}

Expand Down
4 changes: 4 additions & 0 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this, $this->toString());
Copy link
Member

Choose a reason for hiding this comment

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

If the object isn't a Stringable, this should just return $this. But instead it will return ErrorType.

Also I'm not sure what happens with BcMath\Number in strict_types=1 or 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added tests with links to 3v4l.org

}

return $this;
}

Expand Down
7 changes: 7 additions & 0 deletions src/Type/StringType.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
if ($this->isNumericString()->no()) {
return TypeCombinator::union($this, $this->toBoolean());
}
return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean());
}

return $this;
}

Expand Down
5 changes: 5 additions & 0 deletions src/Type/Traits/ObjectTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

trait ObjectTypeTrait
{
Expand Down Expand Up @@ -275,6 +276,10 @@ public function toArrayKey(): Type

public function toCoercedArgumentType(bool $strictTypes): Type
{
if (!$strictTypes) {
return TypeCombinator::union($this, $this->toString());
}

return $this;
}

Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12393.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,23 @@ public function getMixed()
}

}

// https://3v4l.org/LK6Rh
class CallableString {
private string $foo;

public function doFoo(callable $foo): void {
$this->foo = $foo; // PHPStorm wrongly reports an error on this line
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

To be fair PHPStan also reports an error for that (which is correct, we mostly don't differentiate between strict_types 1 or 0 in Type::accepts().

assertType('callable-string|non-empty-string', $this->foo);
}
}

// https://3v4l.org/WJ8NW
class CallableArray {
private array $foo;

public function doFoo(callable $foo): void {
$this->foo = $foo;
assertType('array', $this->foo); // could be non-empty-array
}
}
Loading
Loading