From 45c195a3a96b998868f1bcdfe86fb1421fb61f04 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 16 Feb 2026 20:47:56 +0000 Subject: [PATCH 1/4] Fix TypeError dead catch false positive when assigning mixed to typed property - Changed property assignment TypeError throw point check from accepts() to isSuperTypeOf() using native types - MixedType::isAcceptedBy() always returns yes, causing accepts() to miss that mixed-to-int can throw TypeError - Using isSuperTypeOf() on native types correctly identifies mixed as not guaranteed to be compatible - Widened property type check to include int when property type contains float, preserving PHP's int-to-float coercion - Added regression test with final classes to verify behavior independent of property hooks Closes https://github.com/phpstan/phpstan/issues/9146 --- src/Analyser/NodeScopeResolver.php | 8 +++- .../CatchWithUnthrownExceptionRuleTest.php | 5 +++ .../Rules/Exceptions/data/bug-9146.php | 42 +++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Exceptions/data/bug-9146.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..6cc7d21f97 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6366,8 +6366,14 @@ private function processAssignVar( $declaringClass = $propertyReflection->getDeclaringClass(); if ($declaringClass->hasNativeProperty($propertyName)) { $nativeProperty = $declaringClass->getNativeProperty($propertyName); + $propertyNativeType = $nativeProperty->getNativeType(); + $assignedNativeType = $scope->getNativeType($assignedExpr); + // Widen property type to accept int for float properties (PHP allows int-to-float coercion) + $propertyNativeTypeForCheck = !$propertyNativeType->isFloat()->no() + ? TypeCombinator::union($propertyNativeType, new IntegerType()) + : $propertyNativeType; if ( - !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes() + !$propertyNativeTypeForCheck->isSuperTypeOf($assignedNativeType)->yes() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); } diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 272c010d97..4ab71378fe 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -643,4 +643,9 @@ public function testPropertyHooks(): void ]); } + public function testBug9146(): void + { + $this->analyse([__DIR__ . '/data/bug-9146.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9146.php b/tests/PHPStan/Rules/Exceptions/data/bug-9146.php new file mode 100644 index 0000000000..1700d792db --- /dev/null +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9146.php @@ -0,0 +1,42 @@ +number = $number; + } catch (\TypeError $e) { + throw new \UnexpectedValueException(); + } + } +} + +final class HelloWorld2 +{ + public string $name; + public function setName(mixed $value): void + { + try { + $this->name = $value; + } catch (\TypeError $e) { + throw new \InvalidArgumentException('Expected string'); + } + } +} + +final class HelloWorld3 +{ + public float $amount; + public function setAmount(mixed $value): void + { + try { + $this->amount = $value; + } catch (\TypeError $e) { + echo "caught"; + } + } +} From efecc107c2986ce30c05fd6ef881d99ffec8aad7 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 21:41:29 +0000 Subject: [PATCH 2/4] Fix CI failures [claude-ci-fix] Automated fix attempt 1 for CI failures. --- src/Analyser/NodeScopeResolver.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 6cc7d21f97..3dd4cbd4a6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -174,6 +174,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; +use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -6367,13 +6368,12 @@ private function processAssignVar( if ($declaringClass->hasNativeProperty($propertyName)) { $nativeProperty = $declaringClass->getNativeProperty($propertyName); $propertyNativeType = $nativeProperty->getNativeType(); - $assignedNativeType = $scope->getNativeType($assignedExpr); - // Widen property type to accept int for float properties (PHP allows int-to-float coercion) - $propertyNativeTypeForCheck = !$propertyNativeType->isFloat()->no() + // Widen property type to accept int for float properties (PHP allows int-to-float coercion without TypeError) + $propertyNativeTypeForAccepts = $propertyNativeType->isSuperTypeOf(new FloatType())->yes() ? TypeCombinator::union($propertyNativeType, new IntegerType()) : $propertyNativeType; if ( - !$propertyNativeTypeForCheck->isSuperTypeOf($assignedNativeType)->yes() + !$propertyNativeTypeForAccepts->isSuperTypeOf($assignedExprType)->yes() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); } From 561aa9739ba81f67bf48b654e8940a9f3dc99e64 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 16 Feb 2026 22:21:35 +0000 Subject: [PATCH 3/4] Fix CI failures [claude-ci-fix] Automated fix attempt 2 for CI failures. --- src/Analyser/NodeScopeResolver.php | 3 +- .../CatchWithUnthrownExceptionRuleTest.php | 7 ++++- .../Rules/Exceptions/data/bug-9146.php | 29 +++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 3dd4cbd4a6..f1e74f5004 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -174,7 +174,6 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\FileTypeMapper; -use PHPStan\Type\FloatType; use PHPStan\Type\GeneralizePrecision; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Generic\TemplateTypeMap; @@ -6369,7 +6368,7 @@ private function processAssignVar( $nativeProperty = $declaringClass->getNativeProperty($propertyName); $propertyNativeType = $nativeProperty->getNativeType(); // Widen property type to accept int for float properties (PHP allows int-to-float coercion without TypeError) - $propertyNativeTypeForAccepts = $propertyNativeType->isSuperTypeOf(new FloatType())->yes() + $propertyNativeTypeForAccepts = !$propertyNativeType->isFloat()->no() ? TypeCombinator::union($propertyNativeType, new IntegerType()) : $propertyNativeType; if ( diff --git a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php index 4ab71378fe..7aa2cb5e67 100644 --- a/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php +++ b/tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php @@ -645,7 +645,12 @@ public function testPropertyHooks(): void public function testBug9146(): void { - $this->analyse([__DIR__ . '/data/bug-9146.php'], []); + $this->analyse([__DIR__ . '/data/bug-9146.php'], [ + [ + 'Dead catch - TypeError is never thrown in the try block.', + 52, + ], + ]); } } diff --git a/tests/PHPStan/Rules/Exceptions/data/bug-9146.php b/tests/PHPStan/Rules/Exceptions/data/bug-9146.php index 1700d792db..84213fa2db 100644 --- a/tests/PHPStan/Rules/Exceptions/data/bug-9146.php +++ b/tests/PHPStan/Rules/Exceptions/data/bug-9146.php @@ -40,3 +40,32 @@ public function setAmount(mixed $value): void } } } + +// Dead catch: int assigned to ?float, PHP coerces int to float without TypeError +final class FloatNullCoercion +{ + public ?float $amount; + public function setAmount(int $value): void + { + try { + $this->amount = $value; + } catch (\TypeError $e) { // error: Dead catch - TypeError is never thrown in the try block. + echo "caught"; + } + } +} + +// Not dead: int|string assigned to int, string part could throw TypeError +final class PartialTypeMatch +{ + public int $number; + /** @param int|string $value */ + public function setNumber($value): void + { + try { + $this->number = $value; + } catch (\TypeError $e) { + echo "caught"; + } + } +} From 2ca2364150644fa4acdeae040cc30136fca0b5af Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 17 Feb 2026 14:14:26 +0000 Subject: [PATCH 4/4] Implement Type->toCoercedPropertyType() to replace inline coercion logic Move int-to-float property coercion from NodeScopeResolver into the Type system as toCoercedPropertyType(), following the same polymorphic pattern as toCoercedArgumentType(). FloatType returns float|int; all other types return $this. Composite types (UnionType, IntersectionType) delegate to their members. Co-authored-by: Markus Staab Co-Authored-By: Claude Opus 4.6 --- src/Analyser/NodeScopeResolver.php | 7 +------ src/Type/Accessory/AccessoryArrayListType.php | 5 +++++ src/Type/Accessory/AccessoryLiteralStringType.php | 5 +++++ src/Type/Accessory/AccessoryLowercaseStringType.php | 5 +++++ src/Type/Accessory/AccessoryNonEmptyStringType.php | 5 +++++ src/Type/Accessory/AccessoryNonFalsyStringType.php | 5 +++++ src/Type/Accessory/AccessoryNumericStringType.php | 5 +++++ src/Type/Accessory/AccessoryUppercaseStringType.php | 5 +++++ src/Type/Accessory/HasOffsetType.php | 5 +++++ src/Type/Accessory/HasOffsetValueType.php | 5 +++++ src/Type/Accessory/NonEmptyArrayType.php | 5 +++++ src/Type/Accessory/OversizedArrayType.php | 5 +++++ src/Type/BooleanType.php | 5 +++++ src/Type/CallableType.php | 5 +++++ src/Type/ClosureType.php | 5 +++++ src/Type/Constant/ConstantBooleanType.php | 5 +++++ src/Type/Constant/ConstantIntegerType.php | 5 +++++ src/Type/FloatType.php | 5 +++++ src/Type/Generic/TemplateTypeTrait.php | 5 +++++ src/Type/IntegerType.php | 5 +++++ src/Type/IntersectionType.php | 5 +++++ src/Type/IterableType.php | 5 +++++ src/Type/MixedType.php | 5 +++++ src/Type/NeverType.php | 5 +++++ src/Type/NonexistentParentClassType.php | 5 +++++ src/Type/NullType.php | 5 +++++ src/Type/ObjectType.php | 5 +++++ src/Type/ResourceType.php | 5 +++++ src/Type/StaticType.php | 5 +++++ src/Type/StrictMixedType.php | 5 +++++ src/Type/StringType.php | 5 +++++ src/Type/Traits/ArrayTypeTrait.php | 5 +++++ src/Type/Traits/LateResolvableTypeTrait.php | 5 +++++ src/Type/Traits/ObjectTypeTrait.php | 5 +++++ src/Type/Type.php | 7 +++++++ src/Type/UnionType.php | 5 +++++ src/Type/VoidType.php | 5 +++++ 37 files changed, 183 insertions(+), 6 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index f1e74f5004..6cd7e90003 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -6366,13 +6366,8 @@ private function processAssignVar( $declaringClass = $propertyReflection->getDeclaringClass(); if ($declaringClass->hasNativeProperty($propertyName)) { $nativeProperty = $declaringClass->getNativeProperty($propertyName); - $propertyNativeType = $nativeProperty->getNativeType(); - // Widen property type to accept int for float properties (PHP allows int-to-float coercion without TypeError) - $propertyNativeTypeForAccepts = !$propertyNativeType->isFloat()->no() - ? TypeCombinator::union($propertyNativeType, new IntegerType()) - : $propertyNativeType; if ( - !$propertyNativeTypeForAccepts->isSuperTypeOf($assignedExprType)->yes() + !$nativeProperty->getNativeType()->toCoercedPropertyType()->isSuperTypeOf($assignedExprType)->yes() ) { $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false); } diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index d6624108f2..578f6270e0 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -484,6 +484,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index abf0b2cbc4..846f95a51f 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -228,6 +228,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryLowercaseStringType.php b/src/Type/Accessory/AccessoryLowercaseStringType.php index 13ce997f52..2a02ed32ed 100644 --- a/src/Type/Accessory/AccessoryLowercaseStringType.php +++ b/src/Type/Accessory/AccessoryLowercaseStringType.php @@ -225,6 +225,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index a635ea8618..904a6f1d0c 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -225,6 +225,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index dc5548789a..67fe56b38f 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -228,6 +228,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index 625c82675d..11c203b16d 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -233,6 +233,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/AccessoryUppercaseStringType.php b/src/Type/Accessory/AccessoryUppercaseStringType.php index f4f63666fe..f83e2db95f 100644 --- a/src/Type/Accessory/AccessoryUppercaseStringType.php +++ b/src/Type/Accessory/AccessoryUppercaseStringType.php @@ -225,6 +225,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 004c4b8cb8..af1ecf9d69 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -405,6 +405,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function getEnumCases(): array { return []; diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 60a4478a06..e73825bfa9 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -472,6 +472,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function getEnumCases(): array { return []; diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index d06699b90d..3a8aaf2a5a 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -469,6 +469,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index d624455997..62d26c7aee 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -447,6 +447,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 1782dafa77..b1f49fbe46 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -119,6 +119,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 6e2c98b88c..9e0e6665be 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -441,6 +441,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type ); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index 69bc2752f0..f1a0391bb3 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -534,6 +534,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return TypeCombinator::union($this, new CallableType()); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function getTemplateTypeMap(): TemplateTypeMap { return $this->templateTypeMap; diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 282b005c15..fab6643f01 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -117,6 +117,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isTrue(): TrinaryLogic { return TrinaryLogic::createFromBoolean($this->value === true); diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 6b482c62e6..2f33e4ddea 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -102,6 +102,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return TypeCombinator::union($this, $this->toFloat()); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function generalize(GeneralizePrecision $precision): Type { return new IntegerType(); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index 5df57fe795..3caa7ccc17 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -151,6 +151,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return TypeCombinator::union($this, new IntegerType()); + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Generic/TemplateTypeTrait.php b/src/Type/Generic/TemplateTypeTrait.php index a1f381e6f5..6dcc340e64 100644 --- a/src/Type/Generic/TemplateTypeTrait.php +++ b/src/Type/Generic/TemplateTypeTrait.php @@ -263,6 +263,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { if ( diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 62dafd4ade..bdbc8a5eea 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -106,6 +106,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return TypeCombinator::union($this, $this->toFloat()); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index d102f5fc31..e527741670 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -1292,6 +1292,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes)); } + public function toCoercedPropertyType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->toCoercedPropertyType()); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 46e004ae23..2f01e1f36e 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -257,6 +257,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type ); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 80a0bc2f0b..48e4b59fa1 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -636,6 +636,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isIterable(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index c762202f0b..f148473b50 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -429,6 +429,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function traverse(callable $cb): Type { return $this; diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 74fb77a130..c609ec32bd 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -193,6 +193,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return new ErrorType(); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 5c7730ee9f..f035f2b8f1 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -179,6 +179,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 8e295b9fc4..9ffc5177c3 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -905,6 +905,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function toBoolean(): BooleanType { if ( diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index 9f28095dd8..abfc73d533 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -95,6 +95,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index fdec15cc17..14095940be 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -745,6 +745,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this->getStaticObjectType()->toCoercedArgumentType($strictTypes); } + public function toCoercedPropertyType(): Type + { + return $this->getStaticObjectType()->toCoercedPropertyType(); + } + public function toBoolean(): BooleanType { return $this->getStaticObjectType()->toBoolean(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 953cb19054..7b9e260839 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -431,6 +431,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { return TemplateTypeMap::createEmpty(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 730869022f..cc837e1735 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -192,6 +192,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isNull(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/ArrayTypeTrait.php b/src/Type/Traits/ArrayTypeTrait.php index a019125c3f..300aec77d6 100644 --- a/src/Type/Traits/ArrayTypeTrait.php +++ b/src/Type/Traits/ArrayTypeTrait.php @@ -35,6 +35,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessible(): TrinaryLogic { return TrinaryLogic::createYes(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 4a5dd1e0b9..844ea13451 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -419,6 +419,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this->resolve()->toCoercedArgumentType($strictTypes); } + public function toCoercedPropertyType(): Type + { + return $this->resolve()->toCoercedPropertyType(); + } + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic { return $this->resolve()->isSmallerThan($otherType, $phpVersion); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 51a4922f43..abadb1ab82 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -324,4 +324,9 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this; } + public function toCoercedPropertyType(): Type + { + return $this; + } + } diff --git a/src/Type/Type.php b/src/Type/Type.php index b5de81b99c..9d9f426908 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -345,6 +345,13 @@ public function toArrayKey(): Type; */ public function toCoercedArgumentType(bool $strictTypes): self; + /** + * Widens this property type to include types that PHP will accept + * via implicit coercion during property assignment (e.g. float + * properties also accept int). + */ + public function toCoercedPropertyType(): self; + public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic; diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index c5c9f65415..bc43f1acf3 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -1066,6 +1066,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes)); } + public function toCoercedPropertyType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedPropertyType()); + } + public function inferTemplateTypes(Type $receivedType): TemplateTypeMap { $types = TemplateTypeMap::createEmpty(); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index ca864245e6..7d328b4b25 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -124,6 +124,11 @@ public function toCoercedArgumentType(bool $strictTypes): Type return new NullType(); } + public function toCoercedPropertyType(): Type + { + return $this; + } + public function isOffsetAccessLegal(): TrinaryLogic { return TrinaryLogic::createYes();