Skip to content

Commit c703aa0

Browse files
committed
Fix phpstan/phpstan#14206: @var PDOStatement<int,string> incorrectly reported as unresolvable
- When TypeNodeResolver creates an intersection of a non-generic iterable class with user-provided type arguments (e.g. PDOStatement<int,string>), 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<mixed, array<int|string, mixed>> 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
1 parent f08de42 commit c703aa0

5 files changed

Lines changed: 39 additions & 8 deletions

File tree

.claude/worktrees/agent-a624340d

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 13a2f02edb0f763049973ae926dc10b6899a7cff

.claude/worktrees/agent-af2804bc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Subproject commit 1734058bef811c84970b19806bbd2daee38786ab

src/PhpDoc/TypeNodeResolver.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
use PHPStan\Type\IterableType;
8787
use PHPStan\Type\KeyOfType;
8888
use PHPStan\Type\MixedType;
89+
use PHPStan\Type\NeverType;
8990
use PHPStan\Type\NewObjectType;
9091
use PHPStan\Type\NonAcceptingNeverType;
9192
use PHPStan\Type\NonexistentParentClassType;
@@ -935,17 +936,23 @@ static function (string $variance): TemplateTypeVariance {
935936

936937
try {
937938
if (count($genericTypes) === 1) { // Foo<ValueType>
938-
return TypeCombinator::intersect(
939-
$mainType,
940-
new IterableType(new MixedType(true), $genericTypes[0]),
941-
);
939+
$iterableType = new IterableType(new MixedType(true), $genericTypes[0]);
940+
$result = TypeCombinator::intersect($mainType, $iterableType);
941+
if (!$result instanceof NeverType) {
942+
return $result;
943+
}
944+
945+
return new IntersectionType([$mainType, $iterableType]);
942946
}
943947

944948
if (count($genericTypes) === 2) { // Foo<KeyType, ValueType>
945-
return TypeCombinator::intersect(
946-
$mainType,
947-
new IterableType($genericTypes[0], $genericTypes[1]),
948-
);
949+
$iterableType = new IterableType($genericTypes[0], $genericTypes[1]);
950+
$result = TypeCombinator::intersect($mainType, $iterableType);
951+
if (!$result instanceof NeverType) {
952+
return $result;
953+
}
954+
955+
return new IntersectionType([$mainType, $iterableType]);
949956
}
950957
} finally {
951958
if ($mainTypeClassName !== null) {

tests/PHPStan/Rules/PhpDoc/InvalidPhpDocVarTagTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ public function testBug6348(): void
170170
$this->analyse([__DIR__ . '/data/bug-6348.php'], []);
171171
}
172172

173+
public function testBug14206(): void
174+
{
175+
$this->analyse([__DIR__ . '/data/bug-14206.php'], []);
176+
}
177+
173178
public function testBug9055(): void
174179
{
175180
$this->analyse([__DIR__ . '/data/bug-9055.php'], [
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14206;
4+
5+
use PDO;
6+
use PDOStatement;
7+
8+
class Test
9+
{
10+
public function test(PDO $db): void
11+
{
12+
/** @var PDOStatement<int,string> */
13+
$statement = $db->prepare('SELECT foo FROM bar');
14+
$statement->setFetchMode(PDO::FETCH_COLUMN, 0);
15+
$statement->execute();
16+
}
17+
}

0 commit comments

Comments
 (0)