Repository navigation
Collect template argument constraints through object shape properties in TemplateArgumentObserver - #6817
Open
phpstan-bot wants to merge 1 commit into
Open
Collect template argument constraints through object shape properties in TemplateArgumentObserver#6817phpstan-bot wants to merge 1 commit into
TemplateArgumentObserver#6817phpstan-bot wants to merge 1 commit into
Conversation
… in `TemplateArgumentObserver`
- TemplateArgumentObserver::observeSend() now matches a declared `object{...}`
shape property by property against the actual object's public non-static
properties (new observeObjectShapeSend()). Before, a declared shape names no
class, so `(object) ['x' => new Collection([...])]` returned or passed as
`object{x: Collection<int, string>}` never constrained the inner `new`
site, and its invariant TKey stayed at the exact `0|1|...` literal.
- A declared intersection `object{...}&Foo` sends to each shape member too.
- Ported the same logic to the native mirror in
turbo-ext/src/TemplateArgumentObserver.cpp and regenerated its declarations
header.
- Covers every place that sends to a declared type: return, typed property
assignment, call argument and `yield` (Generator value type), plus nested
shapes, nullable unions and optional shape properties.
- Probed and left alone: closures returned as `Closure(): object{...}` do not
constrain their body's `new` sites even with plain generic or array shape
return types. That limitation is separate from object shapes.
ondrejmirtes
force-pushed
the
create-pull-request/patch-sub5gw1
branch
from
October 27, 2026 18:45
99453bb to
b23037a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a function body's
new Collection(...)sits inside an object shape ((object) ['x' => new Collection($items)]), the declared type it flows into (@return Collection<int, object{x: Collection<int, object{t: string}>}>) did not widen the inner collection's invariant template argument. The exact inferred key type (0|1|2|3) stayed, and PHPStan reported areturn.typeerror. The same code with an array shape (['x' => new Collection(...)]) already worked.This PR makes the template-argument observer follow object shape properties the way it already follows array shape offsets and generic class arguments.
Changes
src/Analyser/Generics/TemplateArgumentObserver.phpobserveSend(): when the declared type is anObjectShapeType, delegate to the newobserveObjectShapeSend(). That method sends each declared property type against the readable type of the actual object's matching public, non-static property.object{...}&\stdClass), send to each object shape member as well, then continue with the existing class-based matching.turbo-ext/src/TemplateArgumentObserver.cpp: the same logic in the native mirror.turbo-ext/src/generated/TemplateArgumentObserver.his regenerated for the new private method.phpstan-baseline.neon: baseline entries for the two deliberateinstanceof ObjectShapeTypechecks and the oneinstanceof IntersectionTypecheck. TheTypeinterface has no way to list an object shape's properties.Analogous flows checked. All of them were broken before and are fixed now, each with a test:
|null,&\stdClassintersection, optional shape properties insidelist<>)yieldagainst theGeneratorvalue typeProbed and not changed: a closure returned as
Closure(): object{...}still does not constrain its body'snewsites. This also happens withClosure(): Collection<int, string>andClosure(): array{...}, so it is a separate limitation unrelated to object shapes.Root cause
observeSend()collects constraints for unresolved template arguments by walking the declared and actual types together: union members, generic class arguments (throughgetObjectClassReflections()), and iterable key/value types. An object shape fits none of these. The actualobject{x: Collection<…>}&stdClasshas one class reflection (stdClass), but the declaredobject{x: Collection<int, string>}has none, so the walk returned early. Thenew Collection(...)site inside was never constrained and kept its exact inferredTKey.Test
tests/PHPStan/Rules/Methods/data/bug-15417.php(ReturnTypeRuleTest::testBug15417, PHP >= 8.2): the reproducer from the issue, verbatim.tests/PHPStan/Rules/Methods/data/bug-15417-shapes.php(ReturnTypeRuleTest::testBug15417Shapes): nested, nullable, intersection and optional-property return cases, plus a case that is still reported when the declared argument really is incompatible (Collection<string, string>).tests/PHPStan/Rules/Methods/data/bug-15417-argument.php(CallStaticMethodsRuleTest::testBug15417): call argument.tests/PHPStan/Rules/Properties/data/bug-15417.php(TypesAssignedToPropertiesRuleTest::testBug15417): property assignment.tests/PHPStan/Rules/Generators/data/bug-15417.php(YieldTypeRuleTest::testBug15417):yield.All of these fail without the fix and pass with it. The full suite passes both with and without the turbo extension loaded, and the turbo smoke test and method parity check pass.
Fixes phpstan/phpstan#15417
🤖 Generated with Claude Code