From c703aa0f2480860062da694a37918df9d8c75976 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 4 Mar 2026 07:48:49 +0000 Subject: [PATCH 1/3] Fix phpstan/phpstan#14206: @var PDOStatement incorrectly reported as unresolvable - When TypeNodeResolver creates an intersection of a non-generic iterable class with user-provided type arguments (e.g. PDOStatement), TypeCombinator::intersect may collapse the result to NeverType if the iterable types are incompatible - This was triggered by the more precise getIterator() return type in the PDOStatement stub (Iterator> instead of plain Iterator) - The fix preserves the IntersectionType directly when the intersection would otherwise collapse to NeverType, maintaining @var override semantics - New regression test in tests/PHPStan/Rules/PhpDoc/data/bug-14206.php --- .claude/worktrees/agent-a624340d | 1 + .claude/worktrees/agent-af2804bc | 1 + src/PhpDoc/TypeNodeResolver.php | 23 ++++++++++++------- .../InvalidPhpDocVarTagTypeRuleTest.php | 5 ++++ tests/PHPStan/Rules/PhpDoc/data/bug-14206.php | 17 ++++++++++++++ 5 files changed, 39 insertions(+), 8 deletions(-) create mode 160000 .claude/worktrees/agent-a624340d create mode 160000 .claude/worktrees/agent-af2804bc create mode 100644 tests/PHPStan/Rules/PhpDoc/data/bug-14206.php diff --git a/.claude/worktrees/agent-a624340d b/.claude/worktrees/agent-a624340d new file mode 160000 index 0000000000..13a2f02edb --- /dev/null +++ b/.claude/worktrees/agent-a624340d @@ -0,0 +1 @@ +Subproject commit 13a2f02edb0f763049973ae926dc10b6899a7cff diff --git a/.claude/worktrees/agent-af2804bc b/.claude/worktrees/agent-af2804bc new file mode 160000 index 0000000000..1734058bef --- /dev/null +++ b/.claude/worktrees/agent-af2804bc @@ -0,0 +1 @@ +Subproject commit 1734058bef811c84970b19806bbd2daee38786ab diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 8af74bf926..3cbc9321c1 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -86,6 +86,7 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; +use PHPStan\Type\NeverType; use PHPStan\Type\NewObjectType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; @@ -935,17 +936,23 @@ static function (string $variance): TemplateTypeVariance { try { if (count($genericTypes) === 1) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType(new MixedType(true), $genericTypes[0]), - ); + $iterableType = new IterableType(new MixedType(true), $genericTypes[0]); + $result = TypeCombinator::intersect($mainType, $iterableType); + if (!$result instanceof NeverType) { + return $result; + } + + return new IntersectionType([$mainType, $iterableType]); } if (count($genericTypes) === 2) { // Foo - return TypeCombinator::intersect( - $mainType, - new IterableType($genericTypes[0], $genericTypes[1]), - ); + $iterableType = new IterableType($genericTypes[0], $genericTypes[1]); + $result = TypeCombinator::intersect($mainType, $iterableType); + if (!$result instanceof NeverType) { + return $result; + } + + return new IntersectionType([$mainType, $iterableType]); } } finally { if ($mainTypeClassName !== null) { diff --git a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php index 0c911449d2..a4bef3c6fe 100644 --- a/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php +++ b/tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php @@ -170,6 +170,11 @@ public function testBug6348(): void $this->analyse([__DIR__ . '/data/bug-6348.php'], []); } + public function testBug14206(): void + { + $this->analyse([__DIR__ . '/data/bug-14206.php'], []); + } + public function testBug9055(): void { $this->analyse([__DIR__ . '/data/bug-9055.php'], [ diff --git a/tests/PHPStan/Rules/PhpDoc/data/bug-14206.php b/tests/PHPStan/Rules/PhpDoc/data/bug-14206.php new file mode 100644 index 0000000000..8f112666a1 --- /dev/null +++ b/tests/PHPStan/Rules/PhpDoc/data/bug-14206.php @@ -0,0 +1,17 @@ + */ + $statement = $db->prepare('SELECT foo FROM bar'); + $statement->setFetchMode(PDO::FETCH_COLUMN, 0); + $statement->execute(); + } +} From f52997185f31cc7f01bea483683fcd3c45f6d626 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 4 Mar 2026 19:31:06 +0100 Subject: [PATCH 2/3] Another strategy --- .claude/worktrees/agent-a624340d | 1 - .claude/worktrees/agent-af2804bc | 1 - src/PhpDoc/TypeNodeResolver.php | 23 ++++++++--------------- stubs/PDOStatement.stub | 6 +++--- 4 files changed, 11 insertions(+), 20 deletions(-) delete mode 160000 .claude/worktrees/agent-a624340d delete mode 160000 .claude/worktrees/agent-af2804bc diff --git a/.claude/worktrees/agent-a624340d b/.claude/worktrees/agent-a624340d deleted file mode 160000 index 13a2f02edb..0000000000 --- a/.claude/worktrees/agent-a624340d +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 13a2f02edb0f763049973ae926dc10b6899a7cff diff --git a/.claude/worktrees/agent-af2804bc b/.claude/worktrees/agent-af2804bc deleted file mode 160000 index 1734058bef..0000000000 --- a/.claude/worktrees/agent-af2804bc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1734058bef811c84970b19806bbd2daee38786ab diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 3cbc9321c1..8af74bf926 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -86,7 +86,6 @@ use PHPStan\Type\IterableType; use PHPStan\Type\KeyOfType; use PHPStan\Type\MixedType; -use PHPStan\Type\NeverType; use PHPStan\Type\NewObjectType; use PHPStan\Type\NonAcceptingNeverType; use PHPStan\Type\NonexistentParentClassType; @@ -936,23 +935,17 @@ static function (string $variance): TemplateTypeVariance { try { if (count($genericTypes) === 1) { // Foo - $iterableType = new IterableType(new MixedType(true), $genericTypes[0]); - $result = TypeCombinator::intersect($mainType, $iterableType); - if (!$result instanceof NeverType) { - return $result; - } - - return new IntersectionType([$mainType, $iterableType]); + return TypeCombinator::intersect( + $mainType, + new IterableType(new MixedType(true), $genericTypes[0]), + ); } if (count($genericTypes) === 2) { // Foo - $iterableType = new IterableType($genericTypes[0], $genericTypes[1]); - $result = TypeCombinator::intersect($mainType, $iterableType); - if (!$result instanceof NeverType) { - return $result; - } - - return new IntersectionType([$mainType, $iterableType]); + return TypeCombinator::intersect( + $mainType, + new IterableType($genericTypes[0], $genericTypes[1]), + ); } } finally { if ($mainTypeClassName !== null) { diff --git a/stubs/PDOStatement.stub b/stubs/PDOStatement.stub index 1b462cdc4b..9f8b3f5ac3 100644 --- a/stubs/PDOStatement.stub +++ b/stubs/PDOStatement.stub @@ -1,8 +1,8 @@ > - * @implements IteratorAggregate> + * @implements Traversable + * @implements IteratorAggregate * @link https://php.net/manual/en/class.pdostatement.php */ class PDOStatement implements Traversable, IteratorAggregate @@ -21,7 +21,7 @@ class PDOStatement implements Traversable, IteratorAggregate public function getColumnMeta(int $column) {} /** - * @return Iterator> + * @return Iterator */ public function getIterator() {} } From af725e7b9578c80cf12560bdd94e8362d9dd827f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 4 Mar 2026 19:38:29 +0100 Subject: [PATCH 3/3] Update test --- tests/PHPStan/Analyser/nsrt/bug-8886.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-8886.php b/tests/PHPStan/Analyser/nsrt/bug-8886.php index 146cbb590b..995e4fde12 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-8886.php +++ b/tests/PHPStan/Analyser/nsrt/bug-8886.php @@ -9,5 +9,5 @@ function testPDOStatementGetIterator(): void { $pdo = new PDO('sqlite::memory:'); $stmt = $pdo->query('SELECT 1'); - assertType('Iterator>', $stmt->getIterator()); + assertType('Iterator', $stmt->getIterator()); }