Skip to content

Fix phpstan/phpstan#11776: Union type of not detected in getter#5357

Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-0vskegh
Open

Fix phpstan/phpstan#11776: Union type of not detected in getter#5357
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-0vskegh

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When a promoted constructor property has a PHPDoc type containing an intersection with a template type (e.g., (int|string)&TOperation where TOperation of scalar), accessing the property incorrectly reports a type mismatch with the getter's return type, even though both annotations use the same type expression.

The fix preserves narrowed template bounds during template type resolution.

Changes

  • Modified src/Type/Generic/TemplateTypeHelper.php: In resolveTemplateTypes(), when replacing a template type with a standin that is the same template (same name and scope) but with a wider bound, preserve the narrower bound from the original type. This maintains intersection-based narrowing like (int|string)&TOperation resolving to TOperation of int|string.
  • Added tests/PHPStan/Analyser/nsrt/bug-11776.php: Regression test with a generic class using a promoted constructor property with an intersection-narrowed template type.

Root cause

When PHPStan parses (int|string)&TOperation (where TOperation of scalar), TypeCombinator::intersect() correctly narrows the template's bound to int|string. However, when accessing the property, ResolvedPropertyReflection::getReadableType() calls TemplateTypeHelper::resolveTemplateTypes() which replaces the narrowed TOperation of int|string with the class's active template standin TOperation of scalar, losing the intersection narrowing.

The method return type was not affected because NodeScopeResolver::getPhpDocs() does not pass return types through resolveTemplateTypes(), preserving the narrowed bound.

The fix adds a check: when the standin is a template type with the same name and scope, and the original type has a strictly narrower bound (which is a subtype of the standin's bound), the narrower bound is preserved in the result.

Test

Added tests/PHPStan/Analyser/nsrt/bug-11776.php which reproduces the original issue: a readonly class ScalarableChoice<TOperation of scalar> with a promoted constructor parameter typed as class-string<EnumAsFilterInterface<(int|string)&TOperation>> and a getter returning the same type. The test verifies the property type correctly resolves to TOperation of int|string instead of TOperation of bool|float|int|string.

Fixes phpstan/phpstan#11776

…esolution

- When resolveTemplateTypes() replaces a template type with a standin
  of the same name and scope, preserve the narrower bound from PHPDoc
  intersections like (int|string)&TOperation
- New regression test in tests/PHPStan/Analyser/nsrt/bug-11776.php
- Root cause: ResolvedPropertyReflection called resolveTemplateTypes()
  which replaced TOperation of int|string with TOperation of scalar,
  losing the intersection narrowing from the PHPDoc type annotation
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.

1 participant