Skip to content

Fix phpstan/phpstan#13325: RegexIterator returns a generic type with unknown parameters#5403

Closed
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-pdpij5c
Closed

Fix phpstan/phpstan#13325: RegexIterator returns a generic type with unknown parameters#5403
phpstan-bot wants to merge 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-pdpij5c

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When constructing a RegexIterator, PHPStan couldn't fully resolve template types. The class has three templates: TKey, TValue, and TIterator of Traversable<TKey, TValue>. Since the constructor only accepts Iterator<TKey, TValue>, TIterator can't be directly inferred and falls back to its bound Traversable<TKey, TValue>. However, the TKey/TValue template types within that bound were not being further resolved, leaving them as raw template type placeholders.

Changes

  • Modified src/Analyser/Traverser/GenericTypeTemplateTraverser.php to call $traverse() on the resolved bound, so inner template types are recursively resolved
  • Added regression test in tests/PHPStan/Rules/Methods/data/bug-13325.php and tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Root cause

In GenericTypeTemplateTraverser::traverse(), when a template type couldn't be resolved from the template type map (mapped to ErrorType), it was replaced with its bound via $type->getDefault() ?? $type->getBound(). However, this return value was not passed through $traverse(), so any template types nested inside the bound (e.g., TKey and TValue inside Traversable<TKey, TValue>) were never resolved. The fix simply wraps the return in $traverse() to continue recursive resolution.

Test

Added a rule test reproducing the exact scenario from the issue: a method returning RegexIterator<mixed, mixed, Traversable<mixed, mixed>> that creates new RegexIterator($iterator, 'string'). Before the fix, this produced a false positive about mismatched return types.

Fixes phpstan/phpstan#13325

…ameters

- Fixed GenericTypeTemplateTraverser to continue traversing into bounds of unresolvable template types
- When a template type (like TIterator) can't be inferred from constructor args, its bound may still contain other template types (TKey, TValue) that need resolution
- Added regression test for phpstan/phpstan#13325
@VincentLanglet VincentLanglet self-assigned this Apr 5, 2026
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now gives

Method pocketmine\world\format\io\region\RegionWorldProvider::createRegionIterator() should return RegexIterator<mixed, string, FilesystemIterator> but returns RegexIterator<mixed, mixed, Traversable<mixed, mixed>>.  

with something like

/**
	 * @phpstan-return \RegexIterator<mixed, string, \FilesystemIterator>
	 */
	private function createRegionIterator() : \RegexIterator{
		return new \RegexIterator(
			new \FilesystemIterator('foo'),
			'bar',
			'baz'
		);
	}

PHPstan is not able to infer all TKey, TValue and TIterator types from new RegexIterator.

I think we should be able to write some code to be able to infer all three along with the change to @param TIterator $iterator but right now that's not the case.

…generic inference

Change @param from Iterator<TKey, TValue> to TIterator in RegexIterator's
constructor stub, matching the pattern already used by IteratorIterator.
This allows PHPStan to infer TIterator from the actual argument type
(e.g. FilesystemIterator) rather than falling back to the bound
Traversable<TKey, TValue>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's a summary of the changes:

Root cause: The RegexIterator constructor stub used @param Iterator<TKey, TValue> $iterator, which only let PHPStan infer TKey and TValue from the argument, but TIterator could never be inferred — it always fell back to the bound Traversable<TKey, TValue>.

Fix: Changed @param Iterator<TKey, TValue> $iterator to @param TIterator $iterator in stubs/iterable.stub:443. This matches the pattern already used by IteratorIterator (line 205). Now when passing e.g. FilesystemIterator, PHPStan correctly infers TIterator = FilesystemIterator, and TKey/TValue are inferred from the bound relationship.

Tests:

  • Updated tests/PHPStan/Rules/Methods/data/bug-13325.php with the corrected return type (Iterator instead of Traversable<mixed, mixed>) and added a FilesystemIterator test case
  • Added tests/PHPStan/Analyser/nsrt/bug-13325.php with assertType verification of inferred types

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants