Walk ancestors for stub method PHPDoc on built-in classes without their own stub entries#5695
Conversation
…ir own stub entries - Remove empty `DateTimeImmutable` and `DateTime` stubs from `stubs/date.stub` that existed only to make `DateTimeInterface::diff()` stub PHPDoc apply to them - Change the guard in `PhpClassReflectionExtension::findMethodPhpDocIncludingAncestors()` to also walk ancestors when the declaring class is a built-in (internal) PHP class, not just when it's a known stub class - The previous `isKnownClass` guard prevented ancestor lookup entirely for classes not registered in stub files, even when their interfaces/parents had stub PHPDocs - User-defined classes are still excluded from ancestor stub lookup to prevent interface stubs from incorrectly overriding user-provided PHPDocs
| return [$resolved, $declaringClass]; | ||
| } | ||
| if (!$this->stubPhpDocProvider->isKnownClass($declaringClassName)) { | ||
| if (!$this->stubPhpDocProvider->isKnownClass($declaringClassName) && !$declaringClass->isBuiltin()) { |
There was a problem hiding this comment.
the cheaper !$declaringClass->isBuiltin() check should be done first
There was a problem hiding this comment.
Tests also already completed successfully. Everything is done — the review comment has been addressed, tests and PHPStan pass, and the fix is pushed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ondrejmirtes
left a comment
There was a problem hiding this comment.
Check the diff in https://github.com/phpstan/phpstan-src/actions/runs/26025371328/job/76497518910. Some results are getting worse a lot of +mixed in the diff. Why is that? Should we improve some stubs? Is this a breaking change?
The isBuiltin() guard allowed all built-in classes to walk ancestors for stub PHPDocs. This caused classes like SplFileObject to pick up Iterator::key()'s @return TKey which resolves to mixed, overriding more specific signature map types (e.g. int). Now the ancestor walk skips stubs whose param/return types contain template types when the declaring class is not in the stub system. This preserves the DateTimeInterface::diff() fix (concrete types) while avoiding regressions for Iterator/ArrayAccess/CachingIterator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Here's a summary of the fix: Root cause of the regressions: The The fix: In the ancestor walk loop, when the declaring class is not a known stub class, skip ancestor stubs whose
One remaining behavioral change: All local tests pass ( |
ondrejmirtes
left a comment
There was a problem hiding this comment.
I don't love this fix. It's too computationally expensive and also imprecise (for other tags than @param + @return).
What about skipping this if the parent class/implemented interface in question is generic? Because this only applies to class-level generics, not method-level generics.
Also add a NSRT regression test for cases covered in your previous commit 04c5ee1
…plate types Replace the computationally expensive TypeUtils::containsTemplateType() check on resolved PHPDoc with a simple $ancestor->isGeneric() check. This correctly skips generic interfaces (Iterator, ArrayAccess, etc.) while allowing non-generic ones (DateTimeInterface, Countable) through, and handles all PHPDoc tags uniformly rather than only @param/@return. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensures SplFileObject::key()/current(), MultipleIterator::key()/current() keep their signature map types (not mixed from Iterator<TKey,TValue>), and Dom\NodeList::count()/WeakMap::count() inherit Countable's stub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's a summary of the changes: Code change (
Regression test (
All 12,099 tests pass and |
Summary
Built-in PHP classes that implement interfaces with stub PHPDocs (like
DateTimeImmutableimplementingDateTimeInterface) were not inheriting the interface's stub method PHPDocs unless they had their own (empty) stub entry. This required maintaining unnecessary empty stubs. The fix makes the ancestor stub PHPDoc lookup work for built-in classes even without their own stub entries.Changes
DateTimeImmutableandDateTimeclass stubs fromstubs/date.stub— these existed solely to makeDateTimeInterface::diff()return type (DateInterval&object{days:int}) apply to implementing classesPhpClassReflectionExtension::findMethodPhpDocIncludingAncestors()(line 1320) from!isKnownClass(declaringClassName)to!isKnownClass(declaringClassName) && !declaringClass->isBuiltin(), so built-in classes also walk their ancestors for stub PHPDocstests/PHPStan/Analyser/nsrt/bug-14632.phpasserting thatDateTimeImmutable::diff()andDateTime::diff()returnDateInterval&object{days: int}Analogous cases probed
Countable::count()→WeakMap::count(): Already handled by the signature map (resources/functionMap.php), so this fix doesn't change behavior thereisBuiltin()guard prevents this)Root cause
PhpClassReflectionExtension::findMethodPhpDocIncludingAncestors()had a guard that returnednullimmediately when the declaring class was not a "known" class in the stub system (i.e., not registered via a stub file). This prevented the ancestor-walking loop from ever running, which meant interface stub PHPDocs (likeDateTimeInterface::diff()) were never found for implementing classes (likeDateTimeImmutable) unless those classes had their own (empty) stub entry.The fix adds
!$declaringClass->isBuiltin()to the guard, so built-in PHP classes likeDateTimeImmutablewill walk their ancestors and find the interface stub PHPDoc, while user-defined classes continue to skip the ancestor loop (preserving their own PHPDocs).Test
tests/PHPStan/Analyser/nsrt/bug-14632.php: Asserts thatDateTimeInterface::diff(),DateTimeImmutable::diff(), andDateTime::diff()all returnDateInterval&object{days: int}(withdaysnarrowed tointinstead ofint|false)DateTimeImmutable::diff()andDateTime::diff()return plainDateIntervalwithdays: int|falsemake tests), PHPStan self-analysis (make phpstan), and coding standards (make cs-fix) all passFixes phpstan/phpstan#14632