From 300e39061f788a365a98beb64d023f325516df71 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Fri, 13 Mar 2026 23:42:47 +0100 Subject: [PATCH 1/2] Add TDate template parameter to DatePeriod stubs The DatePeriod stub only had a `Start` template for constructor overload resolution, with `@implements IteratorAggregate` hardcoded. This caused TooManyTemplateParams when codebases targeting both PHPStan and Psalm used `DatePeriod` in PHPDocs. Add a `TDate of DateTimeInterface` template parameter to all three DatePeriod stubs (PHP <8.0, 8.0-8.1, 8.2+) and use `@psalm-this-out` on the constructor to bind TDate based on the Start parameter. This aligns with PHPStorm stubs and preserves concrete DateTimeInterface subtypes through iteration. Fixes #11727 --- stubs/CoreImmutableClasses.phpstub | 5 ++++- stubs/Php80.phpstub | 7 +++++-- stubs/Php82.phpstub | 7 +++++-- tests/CoreStubsTest.php | 21 +++++++++++++++++---- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index b4213ad76bd..5399c22cef2 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -113,7 +113,8 @@ class DateTimeZone * @psalm-immutable * * @template-covariant Start of string|DateTimeInterface - * @implements Traversable + * @template-covariant TDate of DateTimeInterface + * @implements Traversable */ class DatePeriod implements Traversable { @@ -123,6 +124,8 @@ class DatePeriod implements Traversable * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval * @param (Start is string ? never : DateTimeInterface|positive-int) $end * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options + * + * @psalm-this-out self */ public function __construct($start, $interval = 0, $end = 1, $options = 0) {} } diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 0e1404b74ae..3116085b82f 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -212,7 +212,8 @@ class UnhandledMatchError extends Error {} * @psalm-immutable * * @template-covariant Start of string|DateTimeInterface - * @implements IteratorAggregate + * @template-covariant TDate of DateTimeInterface + * @implements IteratorAggregate */ class DatePeriod implements IteratorAggregate { @@ -222,10 +223,12 @@ class DatePeriod implements IteratorAggregate * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval * @param (Start is string ? never : (DateTimeInterface|positive-int)) $end * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options + * + * @psalm-this-out self */ public function __construct($start, $interval = 0, $end = 1, $options = 0) {} - /** @psalm-return (Start is string ? Iterator : Iterator) */ + /** @psalm-return Iterator */ public function getIterator(): Iterator {} } diff --git a/stubs/Php82.phpstub b/stubs/Php82.phpstub index a2b9ee46150..40f7688542c 100644 --- a/stubs/Php82.phpstub +++ b/stubs/Php82.phpstub @@ -15,7 +15,8 @@ namespace { * @psalm-immutable * * @template-covariant Start of string|DateTimeInterface - * @implements IteratorAggregate + * @template-covariant TDate of DateTimeInterface + * @implements IteratorAggregate */ class DatePeriod implements IteratorAggregate { @@ -27,10 +28,12 @@ namespace { * @param (Start is string ? int-mask : DateInterval) $interval * @param (Start is string ? never : (DateTimeInterface|positive-int)) $end * @param (Start is string ? never : int-mask) $options + * + * @psalm-this-out self */ public function __construct($start, $interval = 0, $end = 1, $options = 0) {} - /** @psalm-return (Start is string ? Iterator : Iterator) */ + /** @psalm-return Iterator */ public function getIterator(): Iterator {} } diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 03c68263b49..41a9b6735c5 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -49,8 +49,8 @@ public function providerValidCodeParse(): iterable echo $dt->format("Y-m-d"); }', 'assertions' => [ - '$period' => 'DatePeriod', - '$dt' => 'DateTimeInterface|null', + '$period' => 'DatePeriod', + '$dt' => 'DateTimeImmutable|null', ], 'ignored_issues' => [], 'php_version' => '7.3', @@ -68,7 +68,7 @@ public function providerValidCodeParse(): iterable echo $dt->format("Y-m-d"); }', 'assertions' => [ - '$period' => 'DatePeriod', + '$period' => 'DatePeriod', '$dt' => 'DateTimeImmutable|null', ], 'ignored_issues' => [], @@ -83,7 +83,7 @@ public function providerValidCodeParse(): iterable echo $dt->format("Y-m-d"); }', 'assertions' => [ - '$period' => 'DatePeriod', + '$period' => 'DatePeriod', '$dt' => 'DateTime|null', ], 'ignored_issues' => [], @@ -107,6 +107,19 @@ public function providerValidCodeParse(): iterable 'ignored_issues' => ['RedundantCondition'], 'php_version' => '8.0', ]; + yield 'DatePeriod with TDate template param does not trigger TooManyTemplateParams' => [ + 'code' => ' $period */ + function foo(DatePeriod $period): void { + foreach ($period as $dt) { + echo $dt->format("Y-m-d"); + } + }', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ]; yield 'sprintf yields a non-empty-string for non-empty-string value' => [ 'code' => ' Date: Fri, 13 Mar 2026 23:45:55 +0100 Subject: [PATCH 2/2] Add additional test coverage for DatePeriod TDate template - Test DateTime start binds TDate correctly (PHP 8.2) - Test getIterator() return type for DateTimeImmutable start - Test getIterator() return type for ISO string start --- tests/CoreStubsTest.php | 45 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 41a9b6735c5..1f7a2972281 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -120,6 +120,51 @@ function foo(DatePeriod $period): void { 'ignored_issues' => [], 'php_version' => '8.2', ]; + yield 'DatePeriod with DateTime start binds TDate to DateTime' => [ + 'code' => 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTime|null', + ], + 'ignored_issues' => [], + 'php_version' => '8.2', + ]; + yield 'DatePeriod getIterator returns correctly typed Iterator' => [ + 'code' => 'getIterator();', + 'assertions' => [ + '$iterator' => 'Iterator', + ], + 'ignored_issues' => [], + 'php_version' => '8.2', + ]; + yield 'DatePeriod getIterator with ISO string returns Iterator of DateTime' => [ + 'code' => 'getIterator();', + 'assertions' => [ + '$iterator' => 'Iterator', + ], + 'ignored_issues' => [], + 'php_version' => '8.2', + ]; yield 'sprintf yields a non-empty-string for non-empty-string value' => [ 'code' => '