Skip to content

Fix #9146: TypeError dead catch when assigning mixed to int in property#4961

Closed
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-31autm4
Closed

Fix #9146: TypeError dead catch when assigning mixed to int in property#4961
phpstan-bot wants to merge 4 commits into2.1.xfrom
create-pull-request/patch-31autm4

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When assigning a mixed typed value to a natively typed property (e.g., int, string, float), PHPStan incorrectly reported the catch (TypeError) as dead code. At runtime, PHP throws TypeError when a value incompatible with the property's native type is assigned, so the catch block is valid.

Changes

  • Modified the TypeError throw point check in src/Analyser/NodeScopeResolver.php (line ~6369):
    • Changed from accepts($assignedExprType, true) to isSuperTypeOf($assignedNativeType) using the native type of the assigned expression
    • Added int-to-float widening: when the property type contains float, the check also accepts int (matching PHP's coercion rules)
  • Added regression test in tests/PHPStan/Rules/Exceptions/data/bug-9146.php with final classes to isolate the fix from property hook implicit throw points
  • Added test method testBug9146 in tests/PHPStan/Rules/Exceptions/CatchWithUnthrownExceptionRuleTest.php

Root cause

The previous check used $nativeProperty->getNativeType()->accepts($assignedExprType, true). The accepts() method delegates to MixedType::isAcceptedBy() when the assigned type is mixed, which unconditionally returns AcceptsResult::createYes() (because mixed is a supertype of the accepting type). This caused int->accepts(mixed, true) to return yes, preventing the TypeError throw point from being created.

The fix uses isSuperTypeOf() on the native type of the assigned expression instead, which correctly returns maybe for int->isSuperTypeOf(mixed), causing the throw point to be created. To preserve PHP's int-to-float coercion behavior (assigning int to a float property does not throw), the property type is widened to include int when it contains float.

Test

The regression test (bug-9146.php) covers three cases:

  • Assigning mixed to an int property (constructor)
  • Assigning mixed to a string property (method)
  • Assigning mixed to a float property (method)

All classes are final to ensure the test is not influenced by PHP 8.4+ property hook implicit throw points (which would mask the bug on non-final classes).

Fixes phpstan/phpstan#9146

phpstan-bot and others added 3 commits February 16, 2026 20:47
… 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 phpstan/phpstan#9146
Automated fix attempt 1 for CI failures.
Automated fix attempt 2 for CI failures.
@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

@phpstan-bot implement Type->toCoercedPropertyType similar to Type->toCoercedArgumentType instead type specific logic within NodeScopeResolver

@phpstan-bot
Copy link
Collaborator Author

phpstan-bot commented Feb 17, 2026

Claude finished @staabm's task in 14s —— View job


Implemented Type->toCoercedPropertyType()

Moved the int-to-float coercion logic from NodeScopeResolver into the Type system, following the same polymorphic pattern as toCoercedArgumentType().

  • Gather context: read existing toCoercedArgumentType() and current PR changes
  • Add toCoercedPropertyType() to the Type interface (src/Type/Type.php)
  • Implement on composite types (UnionType, IntersectionType, LateResolvable, Template, Static)
  • Implement FloatType::toCoercedPropertyType() → returns float|int (the only non-trivial implementation)
  • Implement on all remaining 23 Type classes (return $this)
  • Update NodeScopeResolver.php to use ->toCoercedPropertyType() instead of inline coercion logic
  • Push changes

What changed

37 files modified — The inline coercion logic in NodeScopeResolver (checking isFloat()->no() and manually building TypeCombinator::union($propertyNativeType, new IntegerType())) has been replaced with a single call:

// Before (inline logic in NodeScopeResolver)
$propertyNativeType = $nativeProperty->getNativeType();
$propertyNativeTypeForAccepts = !$propertyNativeType->isFloat()->no()
    ? TypeCombinator::union($propertyNativeType, new IntegerType())
    : $propertyNativeType;
if (!$propertyNativeTypeForAccepts->isSuperTypeOf($assignedExprType)->yes()) { ... }

// After (polymorphic dispatch via Type system)
if (!$nativeProperty->getNativeType()->toCoercedPropertyType()->isSuperTypeOf($assignedExprType)->yes()) { ... }

Key implementations:

  • FloatType::toCoercedPropertyType()TypeCombinator::union($this, new IntegerType()) — float properties accept int values via PHP's implicit coercion
  • UnionType::toCoercedPropertyType() → delegates to each member type
  • IntersectionType::toCoercedPropertyType() → delegates to each member type
  • All other types → return $this (no property assignment coercion)

Note: Tests could not be run locally due to sandbox restrictions. CI will verify correctness.
|

@ondrejmirtes
Copy link
Member

@staabm How different would these two methods be?

@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

How different would these two methods be?

I did not yet think thru all the types, but at least float accepts a int, not matter strict-types

@ondrejmirtes
Copy link
Member

But arguments are exactly the same

@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

at least the impl does not reflect that:

return $this;

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 <staabm@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes
Copy link
Member

In my opinion the logic would be in IntegerType, not FloatType.

@staabm staabm closed this Feb 17, 2026
@staabm
Copy link
Contributor

staabm commented Feb 17, 2026

superseded by #4981

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants