From 475c75e3efed12e1547c2eb2b459c6eca986a2b3 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 29 Apr 2025 12:37:10 +0200 Subject: [PATCH 01/38] tests --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 145 ++++++++++++++++++ .../remember-nullable-property-non-strict.php | 45 ++++++ ...rictComparisonOfDifferentTypesRuleTest.php | 5 + .../Rules/Comparison/data/bug-12946.php | 37 +++++ 4 files changed, 232 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12393b.php create mode 100644 tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12946.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php new file mode 100644 index 0000000000..b8446b181f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -0,0 +1,145 @@ +name = $plugin["name"]; + assertType('string', $this->name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + $this->untypedName = $plugin["name"]; + assertType('mixed', $this->untypedName); + } + + public function doBar(int $i){ + $this->float = $i; + assertType('float', $this->float); + } + + public function doBaz(int $i){ + $this->untypedFloat = $i; + assertType('int', $this->untypedFloat); + } + + public function doLorem(): void + { + $this->a = ['a' => 1]; + assertType('array{a: 1}', $this->a); + } + + public function doFloatTricky(){ + $this->float = 1; + assertType('1.0', $this->float); + } +} + +class HelloWorldStatic +{ + private static string $name; + + /** @var string */ + private static $untypedName; + + private static float $float; + + /** @var float */ + private static $untypedFloat; + + private static array $a; + + /** + * @param mixed[] $plugin + */ + public function __construct(array $plugin){ + self::$name = $plugin["name"]; + assertType('string', self::$name); + } + + /** + * @param mixed[] $plugin + */ + public function doFoo(array $plugin){ + self::$untypedName = $plugin["name"]; + assertType('mixed', self::$untypedName); + } + + public function doBar(int $i){ + self::$float = $i; + assertType('float', self::$float); + } + + public function doBaz(int $i){ + self::$untypedFloat = $i; + assertType('int', self::$untypedFloat); + } + + public function doLorem(): void + { + self::$a = ['a' => 1]; + assertType('array{a: 1}', self::$a); + } +} + +class EntryPointLookup +{ + + /** @var array|null */ + private ?array $entriesData = null; + + /** + * @return array + */ + public function doFoo(): void + { + if ($this->entriesData !== null) { + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + + $data = $this->getMixed(); + if ($data !== null) { + $this->entriesData = $data; + assertType('array', $this->entriesData); + assertNativeType('array', $this->entriesData); + return; + } + + assertType('null', $this->entriesData); + assertNativeType('null', $this->entriesData); + } + + /** + * @return mixed + */ + public function getMixed() + { + + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php new file mode 100644 index 0000000000..9618bc818f --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/remember-nullable-property-non-strict.php @@ -0,0 +1,45 @@ += 8.1 + +declare(strict_types = 0); + +namespace RememberNullablePropertyWhenStrictTypesDisabled; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +interface ObjectDataMapper +{ + /** + * @template OutType of object + * + * @param literal-string&class-string $class + * @param mixed $data + * + * @return OutType + * + * @throws \Exception + */ + public function map(string $class, $data): object; +} + +final class ApiProductController +{ + + protected ?SearchProductsVM $searchProductsVM = null; + + protected static ?SearchProductsVM $searchProductsVMStatic = null; + + public function search(ObjectDataMapper $dataMapper): void + { + $this->searchProductsVM = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', $this->searchProductsVM); + } + + public function searchStatic(ObjectDataMapper $dataMapper): void + { + self::$searchProductsVMStatic = $dataMapper->map(SearchProductsVM::class, $_REQUEST); + assertType('RememberNullablePropertyWhenStrictTypesDisabled\SearchProductsVM', self::$searchProductsVMStatic); + } +} + +class SearchProductsVM {} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 68cd2cc059..4c27bfd80f 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1016,4 +1016,9 @@ public function testBug11019(): void $this->analyse([__DIR__ . '/data/bug-11019.php'], []); } + public function testBug12946(): void + { + $this->analyse([__DIR__ . '/data/bug-12946.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12946.php b/tests/PHPStan/Rules/Comparison/data/bug-12946.php new file mode 100644 index 0000000000..895b0573bf --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12946.php @@ -0,0 +1,37 @@ += 8.1 + +namespace Bug12946; + +interface UserInterface {} +class User implements UserInterface{} + +class UserMapper { + function getFromId(int $id) : ?UserInterface { + return $id === 10 ? new User : null; + } +} + +class GetUserCommand { + + private ?UserInterface $currentUser = null; + + public function __construct( + private readonly UserMapper $userMapper, + private readonly int $id, + ) { + } + + public function __invoke() : UserInterface { + if( $this->currentUser ) { + return $this->currentUser; + } + + $this->currentUser = $this->userMapper->getFromId($this->id); + if( $this->currentUser === null ) { + throw new \Exception; + } + + return $this->currentUser; + } + +} From f1b04fce6e8660ecaa0e9d5d010519fedb3fb258 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Tue, 29 Apr 2025 12:33:44 +0200 Subject: [PATCH 02/38] Fix assigning properties --- src/Analyser/NodeScopeResolver.php | 38 +++++++++++++--------- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 2 +- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index a769db8499..3e2713115f 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5628,25 +5628,28 @@ 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); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } elseif ($scope->isDeclareStrictTypes()) { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), ); + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); @@ -5716,25 +5719,28 @@ 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); + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } elseif ($scope->isDeclareStrictTypes()) { $scope = $scope->assignExpression( $var, TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), ); + } else { + $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index b8446b181f..daf917afd5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -53,7 +53,7 @@ public function doLorem(): void public function doFloatTricky(){ $this->float = 1; - assertType('1.0', $this->float); + assertType('float', $this->float); } } From e1455c0f8b025fa19e3fbc6b29609d7cd06ab455 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:21:00 +0200 Subject: [PATCH 03/38] fix failling tests --- src/Analyser/NodeScopeResolver.php | 12 ++++++++++-- src/Type/Constant/ConstantIntegerType.php | 10 ++++++++++ src/Type/StringType.php | 4 ++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3e2713115f..68f1244ee4 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5649,7 +5649,11 @@ static function (): void { TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), ); } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(false), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(false), $propertyNativeType), + ); } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); @@ -5740,7 +5744,11 @@ static function (): void { TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), ); } else { - $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); + $scope = $scope->assignExpression( + $var, + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(false), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(false), $propertyNativeType), + ); } } else { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 52f29d37a2..78e2949b89 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -7,6 +7,7 @@ use PHPStan\PhpDocParser\Ast\Type\TypeNode; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; @@ -14,6 +15,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; @@ -92,6 +94,14 @@ public function toArrayKey(): Type return $this; } + public function toCoercedArgumentType(bool $strictTypes): Type + { + if (!$strictTypes) { + return TypeCombinator::union($this, new FloatType()); + } + return TypeCombinator::union($this, $this->toFloat()); + } + public function generalize(GeneralizePrecision $precision): Type { return new IntegerType(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index c0114d8462..f5e2d356aa 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -183,6 +183,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), $this, new BooleanType()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index daf917afd5..d784539c96 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -143,3 +143,21 @@ public function getMixed() } } + +class Foo +{ + + public int $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('int', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('int', $this->foo); + } +} From 536af14ebb269252e254453adaf962e0ca456c1b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:27:59 +0200 Subject: [PATCH 04/38] fix bools --- src/Type/BooleanType.php | 4 ++ src/Type/Constant/ConstantBooleanType.php | 8 ++++ src/Type/Constant/ConstantIntegerType.php | 5 +- src/Type/IntegerType.php | 4 ++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 54 ++++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 679c0b9824..e7e7627522 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -113,6 +113,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index ea1c4b09ef..061f72586b 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -8,12 +8,16 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\ConstantScalarType; +use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StaticTypeFactory; +use PHPStan\Type\StringType; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; /** @api */ @@ -109,6 +113,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 78e2949b89..c3063f9feb 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -5,6 +5,7 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; +use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; use PHPStan\Type\FloatType; @@ -12,6 +13,7 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IsSuperTypeOfResult; +use PHPStan\Type\StringType; use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; @@ -97,8 +99,9 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union($this, new FloatType()); + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); } + return TypeCombinator::union($this, $this->toFloat()); } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index e974888bc7..095ac31d64 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -100,6 +100,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return TypeCombinator::union($this, $this->toFloat()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index d784539c96..fc398764aa 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -161,3 +161,57 @@ public function doBar(): void assertType('int', $this->foo); } } + +class FooBool +{ + + public int $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType('int', $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType('int', $this->foo); + } +} + +class FooBoolString +{ + + public string $foo; + + public function doFoo(bool $b): void + { + $this->foo = $b; + assertType('string', $this->foo); + } + + public function doBar(): void + { + $this->foo = true; + assertType('string', $this->foo); + } +} + +class FooIntString +{ + + public string $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('string', $this->foo); // could be numeric-string + } + + public function doBar(): void + { + $this->foo = 1; + assertType('string', $this->foo); // could be numeric-string + } +} From 47a1d735527cbd7c853cfc25a51966387e248300 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:35:39 +0200 Subject: [PATCH 05/38] fix floats --- src/Type/FloatType.php | 4 ++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 253af75e4e..0660089041 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -145,6 +145,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index fc398764aa..c1758dbdd2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -215,3 +215,21 @@ public function doBar(): void assertType('string', $this->foo); // could be numeric-string } } + +class FooFloatString +{ + + public string $foo; + + public function doFoo(float $b): void + { + $this->foo = $b; + assertType('string', $this->foo); // could be numeric-string + } + + public function doBar(): void + { + $this->foo = 1.0; + assertType('string', $this->foo); // could be numeric-string + } +} From 6c4aea1d328f0d64046f69028cb1a67ce8269585 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:36:31 +0200 Subject: [PATCH 06/38] Update StringType.php --- src/Type/StringType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/StringType.php b/src/Type/StringType.php index f5e2d356aa..5595fc23d2 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -184,7 +184,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), $this, new BooleanType()); + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); } return $this; From cebbfcb2dfaec6ec26e72cb0a3feabc790558a2b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:38:36 +0200 Subject: [PATCH 07/38] test int range --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index c1758dbdd2..1146fccbfb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -216,6 +216,28 @@ public function doBar(): void } } +class FooIntRangeString +{ + + public string $foo; + + /** + * @param int<5, 10> $b + */ + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('string', $this->foo); // could be numeric-string + } + + public function doBar(): void + { + $i = rand(5, 10); + $this->foo = $i; + assertType('string', $this->foo); // could be numeric-string + } +} + class FooFloatString { From 6f6bca9c1220c33f319e2177f5b397bc4e3b64f0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 13:45:12 +0200 Subject: [PATCH 08/38] test null coerce to int --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 1146fccbfb..77155a109d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -238,6 +238,24 @@ public function doBar(): void } } +class FooNullableIntString +{ + + public string $foo; + + public function doFoo(?int $b): void + { + $this->foo = $b; + assertType('string', $this->foo); // could be numeric-string + } + + public function doBar(): void + { + $this->foo = null; + assertType('*NEVER*', $this->foo); // null cannot be coerced to string, see https://3v4l.org/5k1Dl + } +} + class FooFloatString { From 411e4475bcf70dcfabfe639fd3e7d8346f4e7d83 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 14:42:38 +0200 Subject: [PATCH 09/38] test union and intersection --- .../Accessory/AccessoryLiteralStringType.php | 5 ++ .../AccessoryLowercaseStringType.php | 5 ++ .../Accessory/AccessoryNonEmptyStringType.php | 4 ++ .../Accessory/AccessoryNonFalsyStringType.php | 4 ++ .../Accessory/AccessoryNumericStringType.php | 4 ++ .../AccessoryUppercaseStringType.php | 5 ++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 47 +++++++++++++++++++ 7 files changed, 74 insertions(+) diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 9af225a3c1..8e1b4aff55 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -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; @@ -215,6 +216,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 5ea351a924..398ee17b5e 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -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; @@ -212,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 961e2cbd95..621e40ed1b 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -213,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 90e7c1f64d..82269b1fd4 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -215,6 +215,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 447bf76ecc..68b0c60855 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -215,6 +215,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index 18ee7399bf..c2855a2d67 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -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; @@ -212,6 +213,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 77155a109d..d9dc72e199 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -273,3 +273,50 @@ public function doBar(): void assertType('string', $this->foo); // could be numeric-string } } + +class FooStringToUnion +{ + + public int|float $foo; + + public function doFoo(string $b): void + { + $this->foo = $b; + assertType('float|int', $this->foo); + } + + public function doBar(): void + { + $this->foo = "1.0"; + assertType('float|int', $this->foo); + } +} + +class FooNumericToString +{ + + public string $foo; + + public function doFoo(float|int $b): void + { + $this->foo = $b; + assertType('string', $this->foo); // could be numeric-string + } + +} + +class FooIntersectionToInt +{ + + public int $foo; + + /** + * @param numeric-string $b + */ + public function doFoo(string $b): void + { + $this->foo = $b; + assertType('int', $this->foo); + } + +} From 2625bb83e2f0f28fc26d552c1cabe63774a846c1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 14:50:06 +0200 Subject: [PATCH 10/38] Update bug-12393b.php --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index d9dc72e199..62d6f2c8d4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -144,7 +144,7 @@ public function getMixed() } -class Foo +class FooStringInt { public int $foo; @@ -195,6 +195,8 @@ public function doBar(): void { $this->foo = true; assertType('string', $this->foo); + $this->foo = false; + assertType('string', $this->foo); } } From 72aa6ab7e2f1fe80fc4c68beba65b78ad6cc4fa9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 14:59:37 +0200 Subject: [PATCH 11/38] test mixed, array --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 62d6f2c8d4..ed9dadf7f5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -322,3 +322,31 @@ public function doFoo(string $b): void } } + +class FooMixedToInt +{ + + public int $foo; + + public function doFoo(mixed $b): void + { + $this->foo = $b; + assertType('int', $this->foo); + } + +} + + +class FooArrayToInt +{ + + public int $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + +} + From 7a95a46fb8f45b7f8cd9f145622a29064c6636cd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:12:25 +0200 Subject: [PATCH 12/38] fix php 7.4 build --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index ed9dadf7f5..d6840f41f6 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -1,4 +1,6 @@ -= 8.0 + +declare(strict_types = 0); namespace Bug12393b; From 4d20e2ae9bf5e97f6cb8e67c55533f8d59bdd618 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:31:18 +0200 Subject: [PATCH 13/38] more precise ints --- src/Type/Constant/ConstantIntegerType.php | 2 +- src/Type/IntegerType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index c3063f9feb..5fd192eeee 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -99,7 +99,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); } return TypeCombinator::union($this, $this->toFloat()); diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 095ac31d64..fcb6fcd893 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -101,7 +101,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this, $this->toFloat(), $this->toString(), $this->toBoolean()); } return TypeCombinator::union($this, $this->toFloat()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index d6840f41f6..2f6f1f8a08 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -55,7 +55,7 @@ public function doLorem(): void public function doFloatTricky(){ $this->float = 1; - assertType('float', $this->float); + assertType('1.0', $this->float); } } @@ -210,13 +210,13 @@ class FooIntString public function doFoo(int $b): void { $this->foo = $b; - assertType('string', $this->foo); // could be numeric-string + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); } public function doBar(): void { $this->foo = 1; - assertType('string', $this->foo); // could be numeric-string + assertType("'1'", $this->foo); } } @@ -231,14 +231,14 @@ class FooIntRangeString public function doFoo(int $b): void { $this->foo = $b; - assertType('string', $this->foo); // could be numeric-string + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); } public function doBar(): void { $i = rand(5, 10); $this->foo = $i; - assertType('string', $this->foo); // could be numeric-string + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); // could be numeric-string } } @@ -250,7 +250,7 @@ class FooNullableIntString public function doFoo(?int $b): void { $this->foo = $b; - assertType('string', $this->foo); // could be numeric-string + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); // could be numeric-string } public function doBar(): void From ccb18899b6fc02aa95bb1c1e88796462ffb3fc25 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:33:50 +0200 Subject: [PATCH 14/38] more precise bools --- src/Type/BooleanType.php | 2 +- src/Type/Constant/ConstantBooleanType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index e7e7627522..a703decac4 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -114,7 +114,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); } return $this; diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 061f72586b..09f4d7e663 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -114,7 +114,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this->toString(), $this); } return $this; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 2f6f1f8a08..f2b5dc728f 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -172,13 +172,13 @@ class FooBool public function doFoo(bool $b): void { $this->foo = $b; - assertType('int', $this->foo); + assertType('0|1', $this->foo); } public function doBar(): void { $this->foo = true; - assertType('int', $this->foo); + assertType('1', $this->foo); } } @@ -190,15 +190,15 @@ class FooBoolString public function doFoo(bool $b): void { $this->foo = $b; - assertType('string', $this->foo); + assertType("''|'1'", $this->foo); } public function doBar(): void { $this->foo = true; - assertType('string', $this->foo); + assertType("'1'", $this->foo); $this->foo = false; - assertType('string', $this->foo); + assertType("''", $this->foo); } } From 78e8df92806c79f9e2c1dffdd378dd6daf3192f3 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:36:03 +0200 Subject: [PATCH 15/38] more precise floats --- src/Type/FloatType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 0660089041..e38e5be35a 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -146,7 +146,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this, $this->toString(), $this->toBoolean()); } return $this; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index f2b5dc728f..af44dc5b7d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -268,13 +268,13 @@ class FooFloatString public function doFoo(float $b): void { $this->foo = $b; - assertType('string', $this->foo); // could be numeric-string + assertType('numeric-string&uppercase-string', $this->foo); } public function doBar(): void { $this->foo = 1.0; - assertType('string', $this->foo); // could be numeric-string + assertType("'1'", $this->foo); } } @@ -304,7 +304,7 @@ class FooNumericToString public function doFoo(float|int $b): void { $this->foo = $b; - assertType('string', $this->foo); // could be numeric-string + assertType('numeric-string&uppercase-string', $this->foo); } } From 872af827167cd62efed90d0256203c4145af5f8b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:42:53 +0200 Subject: [PATCH 16/38] more precise strings --- src/Type/Accessory/AccessoryLiteralStringType.php | 2 +- src/Type/Accessory/AccessoryLowercaseStringType.php | 2 +- src/Type/Accessory/AccessoryNonEmptyStringType.php | 2 +- src/Type/Accessory/AccessoryNonFalsyStringType.php | 2 +- src/Type/Accessory/AccessoryNumericStringType.php | 2 +- src/Type/Accessory/AccessoryUppercaseStringType.php | 2 +- src/Type/StringType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index 8e1b4aff55..8bcc663327 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -217,7 +217,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 398ee17b5e..1e3b55b0ee 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -214,7 +214,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 621e40ed1b..f9fce63d94 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -214,7 +214,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index 82269b1fd4..6600512da1 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -216,7 +216,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 68b0c60855..72f81cabdb 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -216,7 +216,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index c2855a2d67..3fee19deb3 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -214,7 +214,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 5595fc23d2..9bb7564371 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -184,7 +184,7 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { - return TypeCombinator::union(new IntegerType(), new FloatType(), new StringType(), new BooleanType()); + return TypeCombinator::union($this->toInteger(), $this->toFloat(), $this, $this->toBoolean()); } return $this; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index af44dc5b7d..989bace229 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -160,7 +160,7 @@ public function doFoo(string $s): void public function doBar(): void { $this->foo = 'foo'; - assertType('int', $this->foo); + assertType('0', $this->foo); } } @@ -292,7 +292,7 @@ public function doFoo(string $b): void public function doBar(): void { $this->foo = "1.0"; - assertType('float|int', $this->foo); + assertType('1|1.0', $this->foo); } } From 1a8c9211fd9e3bdca2be3f6c72a1732175b5a669 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:43:59 +0200 Subject: [PATCH 17/38] cs --- src/Type/Constant/ConstantBooleanType.php | 3 --- src/Type/Constant/ConstantIntegerType.php | 3 --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 09f4d7e663..282b005c15 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -8,13 +8,10 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\BooleanType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; -use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\StaticTypeFactory; -use PHPStan\Type\StringType; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 5fd192eeee..6b482c62e6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -5,15 +5,12 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; -use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; use PHPStan\Type\ConstantScalarType; -use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IsSuperTypeOfResult; -use PHPStan\Type\StringType; use PHPStan\Type\Traits\ConstantNumericComparisonTypeTrait; use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 989bace229..0b7e22b5d5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -160,7 +160,7 @@ public function doFoo(string $s): void public function doBar(): void { $this->foo = 'foo'; - assertType('0', $this->foo); + assertType('0', $this->foo); // should be *NEVER* } } From 3a645158e009b18b13a76a23875d3e51be5d1562 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Tue, 29 Apr 2025 15:55:02 +0200 Subject: [PATCH 18/38] more precise strings --- src/Type/StringType.php | 3 +++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 9bb7564371..0f1778aa21 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -184,6 +184,9 @@ 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()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 0b7e22b5d5..4b0075a8df 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -160,7 +160,7 @@ public function doFoo(string $s): void public function doBar(): void { $this->foo = 'foo'; - assertType('0', $this->foo); // should be *NEVER* + assertType('*NEVER*', $this->foo); } } From 2c75fb4772a7aee4945bbe6931cc064129ed7853 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 08:38:38 +0200 Subject: [PATCH 19/38] simplify --- src/Analyser/NodeScopeResolver.php | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 68f1244ee4..d979a0f24a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -5642,17 +5642,11 @@ static function (): void { if ($assignedTypeIsCompatible) { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); - } elseif ($scope->isDeclareStrictTypes()) { - $scope = $scope->assignExpression( - $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), - ); } else { $scope = $scope->assignExpression( $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(false), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(false), $propertyNativeType), + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { @@ -5737,17 +5731,11 @@ static function (): void { if ($assignedTypeIsCompatible) { $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr)); - } elseif ($scope->isDeclareStrictTypes()) { - $scope = $scope->assignExpression( - $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(true), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(true), $propertyNativeType), - ); } else { $scope = $scope->assignExpression( $var, - TypeCombinator::intersect($assignedExprType->toCoercedArgumentType(false), $propertyNativeType), - TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType(false), $propertyNativeType), + TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), + TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType), ); } } else { From 65b9d3b31016582ec9546550bcf855316c54a5d2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 08:45:16 +0200 Subject: [PATCH 20/38] more tests --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 49 ++++++++++++++-------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 4b0075a8df..cca15b3a04 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -161,6 +161,31 @@ public function doBar(): void { $this->foo = 'foo'; assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('int', $this->foo); + $this->foo = $nonFalsy; + assertType('int|int<1, max>', $this->foo); + $this->foo = $numeric; + assertType('int', $this->foo); + $this->foo = $literal; + assertType('int', $this->foo); + $this->foo = $lower; + assertType('int', $this->foo); + $this->foo = $upper; + assertType('int', $this->foo); } } @@ -179,6 +204,8 @@ public function doBar(): void { $this->foo = true; assertType('1', $this->foo); + $this->foo = false; + assertType('0', $this->foo); } } @@ -215,8 +242,12 @@ public function doFoo(int $b): void public function doBar(): void { + $this->foo = -1; + assertType("'-1'", $this->foo); $this->foo = 1; assertType("'1'", $this->foo); + $this->foo = 0; + assertType("'0'", $this->foo); } } @@ -309,22 +340,6 @@ public function doFoo(float|int $b): void } -class FooIntersectionToInt -{ - - public int $foo; - - /** - * @param numeric-string $b - */ - public function doFoo(string $b): void - { - $this->foo = $b; - assertType('int', $this->foo); - } - -} - class FooMixedToInt { @@ -341,7 +356,6 @@ public function doFoo(mixed $b): void class FooArrayToInt { - public int $foo; public function doFoo(array $arr): void @@ -351,4 +365,3 @@ public function doFoo(array $arr): void } } - From 604aed1daf2fbbe8562b8b1947174eed6232ed2a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 08:50:58 +0200 Subject: [PATCH 21/38] even more string tests --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 88 ++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index cca15b3a04..c9c6528822 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -189,6 +189,94 @@ function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { } } +class FooStringFloat +{ + + public float $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('float', $this->foo); + } + + public function doBar(): void + { + $this->foo = 'foo'; + assertType('*NEVER*', $this->foo); + $this->foo = '123'; + assertType('123.0', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('float', $this->foo); + $this->foo = $nonFalsy; + assertType('float', $this->foo); + $this->foo = $numeric; + assertType('float', $this->foo); + $this->foo = $literal; + assertType('float', $this->foo); + $this->foo = $lower; + assertType('float', $this->foo); + $this->foo = $upper; + assertType('float', $this->foo); + } +} + +class FooStringBool +{ + + public bool $foo; + + public function doFoo(string $s): void + { + $this->foo = $s; + assertType('bool', $this->foo); + } + + public function doBar(): void + { + $this->foo = '0'; + assertType('false', $this->foo); + $this->foo = 'foo'; + assertType('true', $this->foo); + $this->foo = '123'; + assertType('true', $this->foo); + } + + /** + * @param non-empty-string $nonEmpty + * @param non-falsy-string $nonFalsy + * @param numeric-string $numeric + * @param literal-string $literal + * @param lowercase-string $lower + * @param uppercase-string $upper + */ + function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { + $this->foo = $nonEmpty; + assertType('bool', $this->foo); + $this->foo = $nonFalsy; + assertType('true', $this->foo); + $this->foo = $numeric; + assertType('bool', $this->foo); + $this->foo = $literal; + assertType('bool', $this->foo); + $this->foo = $lower; + assertType('bool', $this->foo); + $this->foo = $upper; + assertType('bool', $this->foo); + } +} + class FooBool { From 016c260f72d1eb6afd5bd2e5b35dd477ce2e1eb0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 10:04:56 +0200 Subject: [PATCH 22/38] more bool tests --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index c9c6528822..d1adfed280 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -277,7 +277,7 @@ function doStrings($nonEmpty, $nonFalsy, $numeric, $literal, $lower, $upper) { } } -class FooBool +class FooBoolInt { public int $foo; @@ -339,6 +339,37 @@ public function doBar(): void } } +class FooIntBool +{ + + public bool $foo; + + public function doFoo(int $b): void + { + $this->foo = $b; + assertType('bool', $this->foo); + + if ($b !== 0) { + $this->foo = $b; + assertType('true', $this->foo); + } + if ($b !== 1) { + $this->foo = $b; + assertType('bool', $this->foo); + } + } + + public function doBar(): void + { + $this->foo = -1; + assertType("true", $this->foo); + $this->foo = 1; + assertType("true", $this->foo); + $this->foo = 0; + assertType("false", $this->foo); + } +} + class FooIntRangeString { From da17fe43cac552383907b3d6930e54f9539c6b3b Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 10:06:07 +0200 Subject: [PATCH 23/38] remove outdated comment --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index d1adfed280..124f076f9d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -388,7 +388,7 @@ public function doBar(): void { $i = rand(5, 10); $this->foo = $i; - assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); // could be numeric-string + assertType("'10'|'5'|'6'|'7'|'8'|'9'", $this->foo); } } @@ -400,7 +400,7 @@ class FooNullableIntString public function doFoo(?int $b): void { $this->foo = $b; - assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); // could be numeric-string + assertType('lowercase-string&numeric-string&uppercase-string', $this->foo); } public function doBar(): void From 9e815d3790896ee7b2fab0e5fcbfe2d7ad34e58a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 30 Apr 2025 10:18:06 +0200 Subject: [PATCH 24/38] Added regression test --- .../Rules/Methods/CallMethodsRuleTest.php | 10 +++++ .../PHPStan/Rules/Methods/data/bug-12940.php | 43 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/PHPStan/Rules/Methods/data/bug-12940.php diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index bc177e9d0b..decb237d34 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3616,4 +3616,14 @@ public function testBug12880(): void $this->analyse([__DIR__ . '/data/bug-12880.php'], []); } + public function testBug12940(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + + $this->analyse([__DIR__ . '/data/bug-12940.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-12940.php b/tests/PHPStan/Rules/Methods/data/bug-12940.php new file mode 100644 index 0000000000..ad00e11c1b --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-12940.php @@ -0,0 +1,43 @@ + $className + * @return T + */ + public static function makeInstance(string $className, mixed ...$args): object + { + return new $className(...$args); + } +} + +class PageRenderer +{ + public function setTemplateFile(string $path): void + { + } + + public function setLanguage(string $lang): void + { + } +} + +class TypoScriptFrontendController +{ + + protected ?PageRenderer $pageRenderer = null; + + public function initializePageRenderer(): void + { + if ($this->pageRenderer !== null) { + return; + } + $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); + $this->pageRenderer->setTemplateFile('EXT:frontend/Resources/Private/Templates/MainPage.html'); + $this->pageRenderer->setLanguage('DE'); + } +} From 76e125854f352f81ea0cf7e421c375c2875a4675 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:13:55 +0200 Subject: [PATCH 25/38] test array accessories and HasOffset*Type --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 124f076f9d..114eacfd9e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -484,3 +484,53 @@ public function doFoo(array $arr): void } } + +class FooArray +{ + public array $foo; + + /** + * @param non-empty-array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('non-empty-array', $this->foo); + + if (array_key_exists('foo', $arr)) { + $this->foo = $arr; + assertType("non-empty-array&hasOffset('foo')", $this->foo); + } + + if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') { + $this->foo = $arr; + assertType("non-empty-array&hasOffsetValue('foo', 'bar')", $this->foo); + } + } + +} + +class FooList +{ + public array $foo; + + /** + * @param non-empty-list $list + */ + public function doFoo(array $list): void + { + $this->foo = $list; + assertType('non-empty-list', $this->foo); + + if (array_key_exists(3, $list)) { + $this->foo = $list; + assertType("non-empty-list&hasOffset(3)", $this->foo); + } + + if (array_key_exists(3, $list) && is_string($list[3])) { + $this->foo = $list; + assertType("non-empty-list&hasOffsetValue(3, string)", $this->foo); + } + } + +} From 4bb13d8036476e78561716bf682116173980d1d6 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:19:58 +0200 Subject: [PATCH 26/38] support Stringable objects --- src/Type/ObjectType.php | 4 ++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index e5b2540d7b..a764001032 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -706,6 +706,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 114eacfd9e..dad1fca770 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -534,3 +534,15 @@ public function doFoo(array $list): void } } + +class StringableFoo { + private string $foo; + + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('string', $this->foo); + } + public function __toString(): string { + return 'Foo'; + } +} From d338a47cb86f42274d6141b209d9e1ff68dd246f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:23:37 +0200 Subject: [PATCH 27/38] test mix of array value types --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index dad1fca770..20cbac2e82 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -507,7 +507,32 @@ public function doFoo(array $arr): void assertType("non-empty-array&hasOffsetValue('foo', 'bar')", $this->foo); } } +} + +class FooTypedArray +{ + /** + * @var array + */ + public array $foo; + + /** + * @param array $arr + */ + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } + /** + * @param array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('array', $this->foo); + } } class FooList From 9d19f303abcbf52fb1d4a8c1cabfc6a5a27be710 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:30:46 +0200 Subject: [PATCH 28/38] fix has_method('__toString') coercison --- src/Type/Traits/ObjectTypeTrait.php | 5 +++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index fe2a3f6ee6..c600f2d74a 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -21,6 +21,7 @@ use PHPStan\Type\MixedType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; trait ObjectTypeTrait { @@ -275,6 +276,10 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { + if (!$strictTypes) { + return TypeCombinator::union($this, $this->toString()); + } + return $this; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 20cbac2e82..dc05be17df 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -571,3 +571,17 @@ public function __toString(): string { return 'Foo'; } } + +class ObjectWithToStringMethod { + private string $foo; + + public function doFoo(object $foo): void { + if (method_exists($foo, '__toString')) { + $this->foo = $foo; + assertType('string', $this->foo); + } + } + public function __toString(): string { + return 'Foo'; + } +} From 0560ae2a112894fe2d19d490d5a98b5e4599e95e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:37:21 +0200 Subject: [PATCH 29/38] suport callable arrays --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index dc05be17df..d38e37ccc5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -560,6 +560,16 @@ public function doFoo(array $list): void } +// https://3v4l.org/VvUsp +class CallableArray { + private array $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('array', $this->foo); // could be non-empty-array + } +} + class StringableFoo { private string $foo; @@ -585,3 +595,4 @@ public function __toString(): string { return 'Foo'; } } + From 113c7d4f7f914bc780fe377eb5aa7815ed8be67e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:43:23 +0200 Subject: [PATCH 30/38] test void can be null --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index d38e37ccc5..284be321d9 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -297,6 +297,24 @@ public function doBar(): void } } +class FooVoidInt { + private ?int $foo; + private int $fooNonNull; + + public function doFoo(): void { + $this->foo = $this->returnVoid(); + assertType('null', $this->foo); + + $this->fooNonNull = $this->returnVoid(); + assertType('null', $this->foo); // should be *ERROR* + } + + public function returnVoid(): void { + return; + } +} + + class FooBoolString { From 3d3464923f58772880ef5b2852ada2f98b6f6f18 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:48:34 +0200 Subject: [PATCH 31/38] support callable as string --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 284be321d9..785ff93a1e 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -304,9 +304,9 @@ class FooVoidInt { public function doFoo(): void { $this->foo = $this->returnVoid(); assertType('null', $this->foo); - + $this->fooNonNull = $this->returnVoid(); - assertType('null', $this->foo); // should be *ERROR* + assertType('int|null', $this->foo); // should be *ERROR* } public function returnVoid(): void { @@ -578,6 +578,16 @@ public function doFoo(array $list): void } +// https://3v4l.org/LJiRB +class CallableString { + private string $foo; + + public function doFoo(callable $foo): void { + $this->foo = $foo; + assertType('string', $this->foo); // could be non-empty-string + } +} + // https://3v4l.org/VvUsp class CallableArray { private array $foo; From c16582af0b2a13b2a1ca605ec51371507024d961 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 08:59:54 +0200 Subject: [PATCH 32/38] fix callable strings --- src/Type/CallableType.php | 8 +++++++- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 72784cf114..568c847711 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -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; @@ -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 diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 785ff93a1e..ccc19aa206 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -584,7 +584,7 @@ class CallableString { public function doFoo(callable $foo): void { $this->foo = $foo; - assertType('string', $this->foo); // could be non-empty-string + assertType('callable-string|non-empty-string', $this->foo); } } From c6db4e712ecbf20343d08c4bdc77edbc7141b43d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 09:05:26 +0200 Subject: [PATCH 33/38] test array-accessory to X --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index ccc19aa206..739dbba7b3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -501,6 +501,81 @@ public function doFoo(array $arr): void assertType('*NEVER*', $this->foo); } + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToFloat +{ + public float $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } +} + +class FooArrayToString +{ + public string $foo; + + public function doFoo(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-array $arr + */ + public function doBar(array $arr): void + { + $this->foo = $arr; + assertType('*NEVER*', $this->foo); + } + + /** + * @param non-empty-list $list + */ + public function doBaz(array $list): void + { + $this->foo = $list; + assertType('*NEVER*', $this->foo); + } } class FooArray From 61e0eea85605f71095efb997b34dc54bbf6ad638 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 09:21:48 +0200 Subject: [PATCH 34/38] added strict-type test-variants for callable array/string --- tests/PHPStan/Analyser/nsrt/bug-12393.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php index 9445d8632b..f29bb5f0a4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -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 + 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 + } +} From a8abeae10149ac169fb56e2e485bc81f90546ca7 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 14:02:54 +0200 Subject: [PATCH 35/38] test not stringable and bcmath --- src/Type/ObjectType.php | 8 ++++++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index a764001032..cc834e2950 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -707,6 +707,14 @@ public function toArrayKey(): Type public function toCoercedArgumentType(bool $strictTypes): Type { if (!$strictTypes) { + $classReflection = $this->getClassReflection(); + if ( + $classReflection === null + || !$classReflection->hasNativeMethod('__toString') + ) { + return $this; + } + return TypeCombinator::union($this, $this->toString()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 739dbba7b3..69106ceea8 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -306,7 +306,7 @@ public function doFoo(): void { assertType('null', $this->foo); $this->fooNonNull = $this->returnVoid(); - assertType('int|null', $this->foo); // should be *ERROR* + assertType('int|null', $this->foo); // should be *NEVER* } public function returnVoid(): void { @@ -680,11 +680,24 @@ public function doFoo(StringableFoo $foo): void { $this->foo = $foo; assertType('string', $this->foo); } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('non-empty-string&numeric-string', $this->foo); + } + public function __toString(): string { return 'Foo'; } } +final class NotStringable {} + class ObjectWithToStringMethod { private string $foo; From 016d44ae65083cb93dd200de9b7e4942ed8c5d69 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 14:03:58 +0200 Subject: [PATCH 36/38] Update bug-12393b.php --- tests/PHPStan/Analyser/nsrt/bug-12393b.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 69106ceea8..576a26fff3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -686,6 +686,7 @@ public function doFoo2(NotStringable $foo): void { assertType('*NEVER*', $this->foo); } + // https://3v4l.org/nelJF#v8.4.6 public function doFoo3(\BcMath\Number $foo): void { $this->foo = $foo; assertType('non-empty-string&numeric-string', $this->foo); From 95186fec15566f005f8d57e570e70d5de787a485 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 14:06:25 +0200 Subject: [PATCH 37/38] test stringable in strict-types --- tests/PHPStan/Analyser/nsrt/bug-12393.php | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php index f29bb5f0a4..5c2f7e0e59 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -163,3 +163,28 @@ public function doFoo(callable $foo): void { assertType('array', $this->foo); // could be non-empty-array } } + +class StringableFoo { + private string $foo; + + // https://3v4l.org/DQSgA#v8.4.6 + public function doFoo(StringableFoo $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function doFoo2(NotStringable $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + // https://3v4l.org/2SPPj#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} From b14719a922ce5421e467381b6a1fa37ca4cc6ec2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 1 May 2025 14:12:50 +0200 Subject: [PATCH 38/38] separated php 8.4 tests --- .../PHPStan/Analyser/nsrt/bug-12393-php84.php | 23 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-12393.php | 6 ----- .../Analyser/nsrt/bug-12393b-php84.php | 22 ++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-12393b.php | 6 ----- 4 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12393-php84.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php new file mode 100644 index 0000000000..b73906fdfd --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393-php84.php @@ -0,0 +1,23 @@ += 8.4 + +declare(strict_types = 1); + +namespace Bug12393Php84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + + +class StringableFoo { + private string $foo; + + // https://3v4l.org/2SPPj#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('*NEVER*', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393.php b/tests/PHPStan/Analyser/nsrt/bug-12393.php index 5c2f7e0e59..4edd2300c1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393.php @@ -178,12 +178,6 @@ public function doFoo2(NotStringable $foo): void { assertType('*NEVER*', $this->foo); } - // https://3v4l.org/2SPPj#v8.4.6 - public function doFoo3(\BcMath\Number $foo): void { - $this->foo = $foo; - assertType('*NEVER*', $this->foo); - } - public function __toString(): string { return 'Foo'; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php new file mode 100644 index 0000000000..ae1946cdb2 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b-php84.php @@ -0,0 +1,22 @@ += 8.4 + +declare(strict_types = 0); + +namespace Bug12393bPhp84; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +class StringableFoo { + private string $foo; + + // https://3v4l.org/nelJF#v8.4.6 + public function doFoo3(\BcMath\Number $foo): void { + $this->foo = $foo; + assertType('non-empty-string&numeric-string', $this->foo); + } + + public function __toString(): string { + return 'Foo'; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12393b.php b/tests/PHPStan/Analyser/nsrt/bug-12393b.php index 576a26fff3..7ec8f3012b 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12393b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12393b.php @@ -686,12 +686,6 @@ public function doFoo2(NotStringable $foo): void { assertType('*NEVER*', $this->foo); } - // https://3v4l.org/nelJF#v8.4.6 - public function doFoo3(\BcMath\Number $foo): void { - $this->foo = $foo; - assertType('non-empty-string&numeric-string', $this->foo); - } - public function __toString(): string { return 'Foo'; }