From f09d18f61eaff433df14f8e20f11ddc7b85f077e Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 18 May 2026 09:29:06 +0000 Subject: [PATCH 1/6] Walk ancestors for stub method PHPDoc on built-in classes without their 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 --- .../Php/PhpClassReflectionExtension.php | 2 +- stubs/date.stub | 6 ----- tests/PHPStan/Analyser/nsrt/bug-14632.php | 26 +++++++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14632.php diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index a31e70433e3..608a9243de3 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -1317,7 +1317,7 @@ private function findMethodPhpDocIncludingAncestors( if ($resolved !== null) { return [$resolved, $declaringClass]; } - if (!$this->stubPhpDocProvider->isKnownClass($declaringClassName)) { + if (!$this->stubPhpDocProvider->isKnownClass($declaringClassName) && !$declaringClass->isBuiltin()) { return null; } diff --git a/stubs/date.stub b/stubs/date.stub index 8191057c642..3cb6da0d1de 100644 --- a/stubs/date.stub +++ b/stubs/date.stub @@ -41,12 +41,6 @@ interface DateTimeInterface { {} } -class DateTimeImmutable implements DateTimeInterface { -} - -class DateTime implements DateTimeInterface { -} - /** @return \DateInterval&object{days:int} */ function date_diff(\DateTimeInterface $baseObject, \DateTimeInterface $targetObject, bool $absolute = false): \DateInterval {} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14632.php b/tests/PHPStan/Analyser/nsrt/bug-14632.php new file mode 100644 index 00000000000..b7e31ae7704 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14632.php @@ -0,0 +1,26 @@ +diff($b); + assertType('DateInterval&object{days: int}', $interval); + assertType('int', $interval->days); +} + +function testDateTimeImmutable(DateTimeImmutable $a, DateTimeImmutable $b): void { + $interval = $a->diff($b); + assertType('DateInterval&object{days: int}', $interval); + assertType('int', $interval->days); +} + +function testDateTime(DateTime $a, DateTime $b): void { + $interval = $a->diff($b); + assertType('DateInterval&object{days: int}', $interval); + assertType('int', $interval->days); +} From eb432f040b2a4d4ee7f7c9839cf2cd53b001b51b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 09:34:15 +0000 Subject: [PATCH 2/6] Put cheaper isBuiltin() check before isKnownClass() lookup Co-Authored-By: Claude Opus 4.6 --- src/Reflection/Php/PhpClassReflectionExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 608a9243de3..ac476c71222 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -1317,7 +1317,7 @@ private function findMethodPhpDocIncludingAncestors( if ($resolved !== null) { return [$resolved, $declaringClass]; } - if (!$this->stubPhpDocProvider->isKnownClass($declaringClassName) && !$declaringClass->isBuiltin()) { + if (!$declaringClass->isBuiltin() && !$this->stubPhpDocProvider->isKnownClass($declaringClassName)) { return null; } From 04c5ee133c8fb800ab925efa005fd43329e76063 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 10:21:11 +0000 Subject: [PATCH 3/6] Skip ancestor stubs with template types for non-stub built-in classes 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 --- .../Php/PhpClassReflectionExtension.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index ac476c71222..20f6c7e5f17 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -53,6 +53,7 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; +use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_keys; @@ -1317,7 +1318,8 @@ private function findMethodPhpDocIncludingAncestors( if ($resolved !== null) { return [$resolved, $declaringClass]; } - if (!$declaringClass->isBuiltin() && !$this->stubPhpDocProvider->isKnownClass($declaringClassName)) { + $isKnownClass = $this->stubPhpDocProvider->isKnownClass($declaringClassName); + if (!$isKnownClass && !$declaringClass->isBuiltin()) { return null; } @@ -1335,10 +1337,28 @@ private function findMethodPhpDocIncludingAncestors( continue; } + if (!$isKnownClass && $this->stubPhpDocContainsTemplateTypes($resolved)) { + continue; + } + return [$resolved, $ancestor]; } return null; } + private function stubPhpDocContainsTemplateTypes(ResolvedPhpDocBlock $phpDoc): bool + { + $returnTag = $phpDoc->getReturnTag(); + if ($returnTag !== null && TypeUtils::containsTemplateType($returnTag->getType())) { + return true; + } + foreach ($phpDoc->getParamTags() as $paramTag) { + if (TypeUtils::containsTemplateType($paramTag->getType())) { + return true; + } + } + return false; + } + } From 86e71f64a23f3bcce5dbc719d4004b49273c9bc8 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 10:42:54 +0000 Subject: [PATCH 4/6] Skip generic ancestor interfaces instead of inspecting PHPDoc for template 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 --- .../Php/PhpClassReflectionExtension.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 20f6c7e5f17..c4ad8bfbfad 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -53,7 +53,6 @@ use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; -use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; use function array_key_exists; use function array_keys; @@ -1337,7 +1336,7 @@ private function findMethodPhpDocIncludingAncestors( continue; } - if (!$isKnownClass && $this->stubPhpDocContainsTemplateTypes($resolved)) { + if (!$isKnownClass && $ancestor->isGeneric()) { continue; } @@ -1347,18 +1346,4 @@ private function findMethodPhpDocIncludingAncestors( return null; } - private function stubPhpDocContainsTemplateTypes(ResolvedPhpDocBlock $phpDoc): bool - { - $returnTag = $phpDoc->getReturnTag(); - if ($returnTag !== null && TypeUtils::containsTemplateType($returnTag->getType())) { - return true; - } - foreach ($phpDoc->getParamTags() as $paramTag) { - if (TypeUtils::containsTemplateType($paramTag->getType())) { - return true; - } - } - return false; - } - } From f703d0234519391fec9a5aad8ded7804e699adf3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 18 May 2026 10:43:00 +0000 Subject: [PATCH 5/6] Add NSRT regression test for built-in classes with generic ancestors Ensures SplFileObject::key()/current(), MultipleIterator::key()/current() keep their signature map types (not mixed from Iterator), and Dom\NodeList::count()/WeakMap::count() inherit Countable's stub. Co-Authored-By: Claude Opus 4.6 --- .../nsrt/builtin-generic-ancestor-stubs.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php diff --git a/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php b/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php new file mode 100644 index 00000000000..c4fa6fff710 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php @@ -0,0 +1,28 @@ +key()); + assertType('array|string|false', $file->current()); +} + +function testMultipleIterator(MultipleIterator $it): void { + assertType('array', $it->key()); + assertType('array', $it->current()); +} + +/** @param WeakMap $map */ +function testWeakMap(WeakMap $map): void { + assertType('int<0, max>', $map->count()); +} + +function testDomNodeList(NodeList $list): void { + assertType('int<0, max>', $list->count()); +} From 3cda671840a024ef9c78e8dd4f9ff604ea1f62e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Mirtes?= Date: Mon, 18 May 2026 12:48:01 +0200 Subject: [PATCH 6/6] Apply suggestion from @ondrejmirtes --- tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php b/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php index c4fa6fff710..0de66fc8316 100644 --- a/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php +++ b/tests/PHPStan/Analyser/nsrt/builtin-generic-ancestor-stubs.php @@ -1,4 +1,4 @@ -= 8.4 namespace BuiltinGenericAncestorStubs;