Fix ReflectionGetAttributesMethodReturnTypeExtension for UnionTypes#5463
Fix ReflectionGetAttributesMethodReturnTypeExtension for UnionTypes#5463phpstan-bot wants to merge 9 commits intophpstan:2.1.xfrom
Conversation
…tensions on union types - In `MethodCallReturnTypeHelper::methodCallReturnType()`, the per-class-name loop passed the union's combined `MethodReflection` to `isMethodSupported()` and `getTypeFromMethodCall()`/`getTypeFromStaticMethodCall()`. Extensions that check `$methodReflection->getDeclaringClass()->getName()` (e.g. `ReflectionGetAttributesMethodReturnTypeExtension`) only matched the declaring class of `UnionTypeMethodReflection::$methods[0]`, causing extensions for other union members to be skipped. - Now create a per-class `ObjectType` and get the method reflection from it for each iteration, so each extension sees the correct declaring class. - Also applies to the static method call branch in the same loop.
VincentLanglet
left a comment
There was a problem hiding this comment.
This fix risk to transform GenericObjectType or TemplateObjectType into simple ObjectType. Would it make sens to iterate on the union type ?
… from class names This preserves GenericObjectType and TemplateObjectType information that was lost when creating `new ObjectType($className)` from just the class name string. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what I changed to address the reviewer's concern: Problem: The previous fix used Fix: Instead of iterating over
Both |
VincentLanglet
left a comment
There was a problem hiding this comment.
Try to not touch MethodCallReturnTypeHelper and remove the $methodReflection->getDeclaringClass()->getName() check inside the ReflectionGetAttributesMethodReturnTypeExtension instead.
…xtension instead of modifying MethodCallReturnTypeHelper Revert changes to MethodCallReturnTypeHelper and instead remove the $methodReflection->getDeclaringClass()->getName() check from the extension's isMethodSupported(). The getClass() method + extension registry already ensure each extension instance is only invoked for the correct class hierarchy, making the declaring class check redundant. The declaring class check was also the root cause of the union type bug: UnionTypeMethodReflection::getDeclaringClass() returns only the first member's declaring class, so extensions for other union members were not matched. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's a summary of the changes: What changed:
Why this works: The declaring class check was redundant because |
|
I processed this review but have nothing to report. |
src/Reflection/ClassReflection.php
Outdated
| $attributeClass = $this->reflectionProvider->getClass(Attribute::class); | ||
| $arguments = []; | ||
| foreach ($nativeAttributes[0]->getArgumentsExpressions() as $i => $expression) { | ||
| foreach ($nativeAttributes[0]->getArgumentsExpressions() as $i => $expression) { // @phpstan-ignore method.notFound (method exists on BetterReflection's ReflectionAttribute adapter) |
There was a problem hiding this comment.
Rather than ignoring the method.notFOund error
Use
@var array<ReflectionAttribute>
above the $nativeAttributes var definition
There was a problem hiding this comment.
Done. Here's a summary of what I changed and why:
The reviewer's suggestion (@var array<ReflectionAttribute>) doesn't work because removing the declaring class check entirely causes the extension to fire during self-analysis. When it fires, it overrides BetterReflection's return type (list<BetterReflection\ReflectionAttribute>) with the native type (list<ReflectionAttribute<Attribute>>). The @var then fails PHPStan's varTag.type rule because BetterReflection\ReflectionAttribute (which is CoreReflectionAttribute<object>) is not a subtype of ReflectionAttribute<Attribute>.
Root cause investigation: Through debug logging, I discovered:
- During self-analysis:
getDeclaringClass()->getName()returnsPHPStan\BetterReflection\Reflection\Adapter\ReflectionClass(adapter overridesgetAttributes()) - For user code: declaring class is native
ReflectionClass - For union types:
UnionTypeMethodReflectionreturns the first member's declaring class (always native)
The fix: Replace !str_contains($methodReflection->getDeclaringClass()->getName(), '\\') — this check:
- Passes for native reflection classes (user code)
- Passes for union types (first member is native)
- Fails for BetterReflection adapters (namespaced), preserving their accurate return types
This removes both the @phpstan-ignore from ClassReflection.php and the fragile === $this->className check that caused the union type bug.
…declaring class check entirely The previous approach (removing the declaring class check entirely) caused the extension to fire during self-analysis where BetterReflection adapters override getAttributes(). This overrode BetterReflection's accurate return type with the native ReflectionAttribute type, which lacks getArgumentsExpressions(). Instead of checking declaring class name === className (which fails for union types because UnionTypeMethodReflection returns only the first member's declaring class), check that the declaring class is a native PHP class (no namespace separator). This correctly: - Fires for user code (declaring class is native ReflectionClass etc.) - Fires for union types (declaring class is first member's native class) - Skips BetterReflection adapters during self-analysis (namespaced classes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Rather than a check
!str_contains($methodReflection->getDeclaringClass()->getName(), '\\');
what about improving getTypeFromMethodCall for BetterReflection classes.
This will be a
GenericObjectType(BetterReflectionAttribute
…str_contains hack Instead of filtering out BetterReflection adapter classes with a str_contains check in isMethodSupported(), resolve the correct ReflectionAttribute class name from the method's declared return type in getTypeFromMethodCall(). For native reflection classes, the return type is ReflectionAttribute[] so the resolved class is ReflectionAttribute. For BetterReflection adapters, the return type includes their ReflectionAttribute adapter, so the extension correctly returns GenericObjectType with that class, preserving methods like getArgumentsExpressions(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what I changed to address the reviewer's feedback: Problem: The previous approach used Fix:
This approach uses |
| foreach ($returnType->getIterableValueType()->getObjectClassNames() as $className) { | ||
| if ($nativeReflectionAttributeType->isSuperTypeOf(new ObjectType($className))->yes()) { | ||
| return $className; | ||
| } | ||
| } |
There was a problem hiding this comment.
This would creates an issue with a method which returns something like array<Attribute1|Attribute2> since you're matching the first one only.
There was a problem hiding this comment.
Both make tests and make phpstan pass cleanly.
Summary of the change:
In resolveReflectionAttributeClassName, instead of returning the first class name from the method's return type that is a subtype of ReflectionAttribute, the code now collects all matching class names and only uses the result when there is exactly one unambiguous match. When there are zero or multiple matches (e.g., a return type like array<Attribute1|Attribute2> where both extend ReflectionAttribute), it falls back to the native ReflectionAttribute::class.
This handles the existing cases correctly:
- Native reflection: return type value is
ReflectionAttribute→ one match → uses it - BetterReflection adapters: return type value is
BRAdapter\ReflectionAttribute|FakeReflectionAttribute→ one match (only the adapter extends nativeReflectionAttribute) → uses it - Hypothetical
Attribute1|Attribute2: two matches → falls back toReflectionAttribute::class(the common base)
Instead of returning the first matching class name from the method's return type, collect all matches and only use the result when there is exactly one unambiguous match. When there are zero or multiple matching ReflectionAttribute subclasses, fall back to the native ReflectionAttribute class. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
When there is multiple classes, it should be an union of all the
new GenericObjectType($classes, [$classType])
…ubclasses match When resolveReflectionAttributeClassNames finds multiple classes that extend ReflectionAttribute, create a union of GenericObjectType for each instead of falling back to the base ReflectionAttribute class. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what I changed to address the reviewer's feedback: Change: When For example, if a method returns The single-match case still works the same — Both |
VincentLanglet
left a comment
There was a problem hiding this comment.
What if resolveReflectionAttributeClassNames is renamed resolveReflectionAttribute and return directly the GenericObjectType or the UnionType to avoid a useless TypeCombinator::union call.
…buteType and return Type directly Instead of returning class name strings that get wrapped in GenericObjectType by the caller, the helper method now accepts the classType parameter and returns the final GenericObjectType or UnionType directly. This avoids the unnecessary TypeCombinator::union call in the single-match case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. The change renames |
VincentLanglet
left a comment
There was a problem hiding this comment.
FOUND 1 ERROR AFFECTING 1 LINE
60 | ERROR | [x] Use early exit to reduce code nesting.
| | (SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed)
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. The fix uses early exit ( |
|
WDYT @janedbal ? |
Fixes phpstan/phpstan#14466