From 28cf00cc01f0ea3276a0bf58dea4ed524da5de29 Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Sat, 16 May 2026 18:15:18 +0900 Subject: [PATCH 1/6] Add `@implements` to SPL iterator stubs --- phpstan-baseline.neon | 6 +++ stubs/iterable.stub | 49 +++++++++++++------ tests/PHPStan/Analyser/nsrt/bug-8435.php | 26 ++++++++++ .../Rules/Classes/InstantiationRuleTest.php | 2 +- 4 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-8435.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 256cd87c441..a4f48fcb9e3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -114,6 +114,12 @@ parameters: count: 2 path: src/Analyser/TypeSpecifier.php + - + rawMessage: 'Cannot call method getPathname() on SplFileInfo|string.' + identifier: method.nonObject + count: 1 + path: src/Cache/FileCacheStorage.php + - rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().' identifier: generics.variance diff --git a/stubs/iterable.stub b/stubs/iterable.stub index baf5ca90837..4d180a38cab 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -159,22 +159,10 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Countable } /** - * @template T of \RecursiveIterator|\IteratorAggregate - * @mixin T + * @implements SeekableIterator */ -class RecursiveIteratorIterator +class DirectoryIterator extends SplFileInfo implements SeekableIterator { - /** - * @param T $iterator - */ - public function __construct( - $iterator, - int $mode = RecursiveIteratorIterator::LEAVES_ONLY, - int $flags = 0 - ) - { - - } } @@ -207,6 +195,31 @@ class IteratorIterator implements OuterIterator { public function __construct(Traversable $iterator) {} } +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator of \RecursiveIterator|\IteratorAggregate + * + * @implements OuterIterator + * + * @mixin TIterator + */ +class RecursiveIteratorIterator implements OuterIterator +{ + /** + * @param TIterator $iterator + */ + public function __construct( + $iterator, + int $mode = RecursiveIteratorIterator::LEAVES_ONLY, + int $flags = 0 + ) + { + + } + +} + /** * @template-covariant TKey * @template-covariant TValue @@ -289,6 +302,14 @@ class RecursiveArrayIterator extends ArrayIterator implements RecursiveIterator public function uksort($cmp_function) { } } +/** + * @implements RecursiveIterator + */ +class RecursiveDirectoryIterator extends FilesystemIterator implements RecursiveIterator +{ + +} + /** * @template TKey * @template TValue diff --git a/tests/PHPStan/Analyser/nsrt/bug-8435.php b/tests/PHPStan/Analyser/nsrt/bug-8435.php new file mode 100644 index 00000000000..af4c03cfe56 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-8435.php @@ -0,0 +1,26 @@ +analyse([__DIR__ . '/data/bug-3425.php'], [ [ - 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects T of IteratorAggregate|RecursiveIterator, Generator given.', + 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects TIterator of IteratorAggregate|RecursiveIterator, Generator given.', 5, ], ]); From 6a76e67785ad70b0efc49a7894c1b0d69a560b07 Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Mon, 25 May 2026 19:41:19 +0900 Subject: [PATCH 2/6] Use `key-of`/`value-of` to maintain backward compatibility --- stubs/iterable.stub | 6 ++---- tests/PHPStan/Analyser/nsrt/bug-8435.php | 10 ++++++++++ tests/PHPStan/Rules/Classes/InstantiationRuleTest.php | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/stubs/iterable.stub b/stubs/iterable.stub index 4d180a38cab..490d7a1cd00 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -196,11 +196,9 @@ class IteratorIterator implements OuterIterator { } /** - * @template-covariant TKey - * @template-covariant TValue - * @template TIterator of \RecursiveIterator|\IteratorAggregate + * @template TIterator of \RecursiveIterator|\IteratorAggregate * - * @implements OuterIterator + * @implements OuterIterator, value-of> * * @mixin TIterator */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-8435.php b/tests/PHPStan/Analyser/nsrt/bug-8435.php index af4c03cfe56..ecbcf7cdd85 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8435.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8435.php @@ -23,4 +23,14 @@ public function sayHello2(string $path): void assertType('SplFileInfo|string', $fileinfo); } } + + /** + * @param RecursiveIteratorIterator $iterator + */ + public function test(RecursiveIteratorIterator $iterator): void + { + foreach ($iterator as $fileinfo) { + assertType('SplFileInfo|string', $fileinfo); + } + } } diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index d61f96bcbe5..3c4617ac082 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -351,7 +351,7 @@ public function testBug3425(): void { $this->analyse([__DIR__ . '/data/bug-3425.php'], [ [ - 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects TIterator of IteratorAggregate|RecursiveIterator, Generator given.', + 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects TIterator of IteratorAggregate|RecursiveIterator, Generator given.', 5, ], ]); From 85ee9b30afc6123d5a24ddac68c6176b3b4d197f Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Mon, 25 May 2026 20:53:32 +0900 Subject: [PATCH 3/6] Restore the old template var name --- stubs/iterable.stub | 8 ++++---- tests/PHPStan/Rules/Classes/InstantiationRuleTest.php | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/stubs/iterable.stub b/stubs/iterable.stub index 490d7a1cd00..94125deaf13 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -196,16 +196,16 @@ class IteratorIterator implements OuterIterator { } /** - * @template TIterator of \RecursiveIterator|\IteratorAggregate + * @template T of \RecursiveIterator|\IteratorAggregate * - * @implements OuterIterator, value-of> + * @implements OuterIterator, value-of> * - * @mixin TIterator + * @mixin T */ class RecursiveIteratorIterator implements OuterIterator { /** - * @param TIterator $iterator + * @param T $iterator */ public function __construct( $iterator, diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 3c4617ac082..a3538302b56 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -351,7 +351,7 @@ public function testBug3425(): void { $this->analyse([__DIR__ . '/data/bug-3425.php'], [ [ - 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects TIterator of IteratorAggregate|RecursiveIterator, Generator given.', + 'Parameter #1 $iterator of class RecursiveIteratorIterator constructor expects T of IteratorAggregate|RecursiveIterator, Generator given.', 5, ], ]); From dab3e582578e15e9ebbbfd94902e0733f309b15b Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Wed, 27 May 2026 11:37:47 +0900 Subject: [PATCH 4/6] Make value of DirectoryIterator `mixed` `DirectoryIterator::current()` itself always returns `DirectoryIterator` but `FilesystemIterator`, a child class of `DirectoryIterator`, gives `string|SplFileIterator` which is not a subtype of `DirectoryIterator`. The declared return type of `DirectoryIterator::current()` is actually `mixed`. So the stub should be `mixed` too in order not to violate LSP. https://www.php.net/directoryiterator.current --- stubs/iterable.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/iterable.stub b/stubs/iterable.stub index 94125deaf13..fe5263bdee8 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -159,7 +159,7 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Countable } /** - * @implements SeekableIterator + * @implements SeekableIterator */ class DirectoryIterator extends SplFileInfo implements SeekableIterator { From a5d98fdc43e91dfad59952e1c73782596d2cf2a2 Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Wed, 27 May 2026 15:57:51 +0900 Subject: [PATCH 5/6] Keep `FilesystemIterator::current` typed as `SplFileInfo|string` --- stubs/iterable.stub | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/stubs/iterable.stub b/stubs/iterable.stub index fe5263bdee8..c2d9684a9fd 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -159,13 +159,22 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Countable } /** - * @implements SeekableIterator + * @template TValue = mixed + * @implements SeekableIterator */ class DirectoryIterator extends SplFileInfo implements SeekableIterator { } +/** + * @extends DirectoryIterator + */ +class FilesystemIterator extends DirectoryIterator +{ + +} + /** * @template-covariant TKey * @template-covariant TValue From 93455b3a7b62fc2335bf49b01b941ff9a093195b Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Sun, 31 May 2026 16:34:55 +0900 Subject: [PATCH 6/6] assert a variable is `SplFileInfo` instead of adding baseline --- phpstan-baseline.neon | 6 ------ src/Cache/FileCacheStorage.php | 3 +++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a4f48fcb9e3..256cd87c441 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -114,12 +114,6 @@ parameters: count: 2 path: src/Analyser/TypeSpecifier.php - - - rawMessage: 'Cannot call method getPathname() on SplFileInfo|string.' - identifier: method.nonObject - count: 1 - path: src/Cache/FileCacheStorage.php - - rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().' identifier: generics.variance diff --git a/src/Cache/FileCacheStorage.php b/src/Cache/FileCacheStorage.php index 6929806f694..5b47324e0ef 100644 --- a/src/Cache/FileCacheStorage.php +++ b/src/Cache/FileCacheStorage.php @@ -13,7 +13,9 @@ use PHPStan\ShouldNotHappenException; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use SplFileInfo; use function array_keys; +use function assert; use function closedir; use function dirname; use function error_get_last; @@ -147,6 +149,7 @@ public function clearUnusedFiles(): void $beginNew = "getPathname(); $contents = FileReader::read($path);