Skip to content

Commit 315b05e

Browse files
ondrejmirtesclaude
andcommitted
Add regression tests for issues fixed by unresolved template arguments
Each playground sample, verbatim under its own namespace, analyses clean with the feature on and reports the original false positive with the toggle off. Closes phpstan/phpstan#12704 Closes phpstan/phpstan#12576 Closes phpstan/phpstan#10419 Closes phpstan/phpstan#14647 Closes phpstan/phpstan#13431 Closes phpstan/phpstan#12601 Closes phpstan/phpstan#12490 Closes phpstan/phpstan#12420 Closes phpstan/phpstan#11835 Closes phpstan/phpstan#11435 Closes phpstan/phpstan#10290 Closes phpstan/phpstan#10289 Closes phpstan/phpstan#5741 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GNLiox4nj8c7YstGfsuZ39
1 parent da34b85 commit 315b05e

16 files changed

Lines changed: 686 additions & 0 deletions

‎tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,4 +493,26 @@ public function testBug13114(): void
493493
]);
494494
}
495495

496+
public function testBug10419(): void
497+
{
498+
$this->checkNullables = true;
499+
$this->checkExplicitMixed = true;
500+
$this->analyse([__DIR__ . '/data/bug-10419.php'], []);
501+
}
502+
503+
#[RequiresPhp('>= 8.2.0')]
504+
public function testBug10290(): void
505+
{
506+
$this->checkNullables = true;
507+
$this->checkExplicitMixed = true;
508+
$this->analyse([__DIR__ . '/data/bug-10290.php'], []);
509+
}
510+
511+
public function testBug5741(): void
512+
{
513+
$this->checkNullables = true;
514+
$this->checkExplicitMixed = false;
515+
$this->analyse([__DIR__ . '/data/bug-5741.php'], []);
516+
}
517+
496518
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug10290;
6+
7+
abstract class Result{
8+
/**
9+
* @template T
10+
* @param Ok<T> $ok
11+
* @return T
12+
*/
13+
public static function getOk(Ok $ok) { return $ok->data; }
14+
15+
/**
16+
* @template E
17+
* @param Err<E> $err
18+
* @return E
19+
*/
20+
public static function getErr(Err $err) { return $err->data; }
21+
}
22+
/** @template T */
23+
final readonly class Ok extends Result {
24+
/** @param T $data */
25+
public function __construct(protected mixed $data) {}
26+
}
27+
/** @template E */
28+
final readonly class Err extends Result {
29+
/** @param E $data */
30+
public function __construct(protected mixed $data) {}
31+
}
32+
33+
/**
34+
* @return Ok<non-empty-string>|Err<array<mixed>>
35+
*/
36+
function f(string $json): Result
37+
{
38+
$data = json_decode($json, true, JSON_THROW_ON_ERROR);
39+
assert(is_array($data));
40+
41+
if (isset($data['has_error']) && $data['has_error']) {
42+
\PHPStan\dumpType($data);
43+
return new Err($data);
44+
}
45+
46+
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
47+
if ($email === false) {
48+
\PHPStan\dumpType($data);
49+
return new Err($data);
50+
}
51+
52+
return new Ok($email);
53+
}
54+
55+
/**
56+
* @return Ok<true>|Err<string>
57+
*/
58+
function g(): Result
59+
{
60+
if (rand() === 1) {
61+
return new Ok(true);
62+
}
63+
64+
return new Err('error');
65+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug10419;
6+
7+
use DateTime;
8+
9+
/**
10+
* @template T
11+
*/
12+
class Foo {
13+
/**
14+
* @param T $value
15+
*/
16+
public function __construct(public $value) {}
17+
}
18+
19+
/**
20+
* @return Foo<array<string, array{
21+
* boolKey: bool,
22+
* naturalKey: non-negative-int,
23+
* }>>
24+
*/
25+
function fail(): Foo {
26+
return new Foo([
27+
(new DateTime)->format('Y-m-d') => [
28+
'boolKey' => false,
29+
'naturalKey' => 0,
30+
],
31+
]);
32+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Bug5741;
4+
5+
/**
6+
* @template T
7+
*/
8+
final class Result
9+
{
10+
/** @var T */
11+
public $value;
12+
13+
/**
14+
* @param T $value
15+
*/
16+
public function __construct($value)
17+
{
18+
$this->value = $value;
19+
}
20+
}
21+
22+
23+
/**
24+
* @return Result<int[]>
25+
*/
26+
function one()
27+
{
28+
$ints = [1, 2];
29+
30+
\PHPStan\dumpType($ints);
31+
return new Result($ints);
32+
}
33+
34+
35+
/**
36+
* @return Result<int[]>
37+
*/
38+
function two()
39+
{
40+
$result = new Result([]);
41+
$result->value = [1];
42+
$result->value[] = 2;
43+
44+
\PHPStan\dumpType($result->value);
45+
return $result;
46+
}
47+
48+
49+
/**
50+
* @return int[]
51+
*/
52+
function three()
53+
{
54+
$ints = [1, 2];
55+
\PHPStan\dumpType($ints);
56+
return $ints;
57+
}

‎tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,4 +1344,62 @@ public function testBug14893(): void
13441344
$this->analyse([__DIR__ . '/data/bug-14893.php'], []);
13451345
}
13461346

1347+
public function testBug12704(): void
1348+
{
1349+
$this->checkExplicitMixed = true;
1350+
$this->analyse([__DIR__ . '/data/bug-12704.php'], []);
1351+
}
1352+
1353+
public function testBug12576(): void
1354+
{
1355+
$this->checkExplicitMixed = true;
1356+
$this->analyse([__DIR__ . '/data/bug-12576.php'], []);
1357+
}
1358+
1359+
#[RequiresPhp('>= 8.1.0')]
1360+
public function testBug14647(): void
1361+
{
1362+
$this->checkExplicitMixed = true;
1363+
$this->analyse([__DIR__ . '/data/bug-14647.php'], []);
1364+
}
1365+
1366+
public function testBug12601(): void
1367+
{
1368+
$this->checkExplicitMixed = true;
1369+
$this->analyse([__DIR__ . '/data/bug-12601.php'], []);
1370+
}
1371+
1372+
public function testBug12490(): void
1373+
{
1374+
$this->analyse([__DIR__ . '/data/bug-12490.php'], []);
1375+
}
1376+
1377+
#[RequiresPhp('>= 8.1.0')]
1378+
public function testBug12420(): void
1379+
{
1380+
$this->checkExplicitMixed = true;
1381+
$this->analyse([__DIR__ . '/data/bug-12420.php'], [
1382+
[
1383+
'Method Bug12420\Test::testFailingArray() should return array<\'bar\'|\'foo\'> but returns array{\'foo\', \'bar\', \'wrong\'}.',
1384+
66,
1385+
],
1386+
]);
1387+
}
1388+
1389+
public function testBug11835(): void
1390+
{
1391+
$this->checkExplicitMixed = true;
1392+
$this->analyse([__DIR__ . '/data/bug-11835.php'], []);
1393+
}
1394+
1395+
public function testBug11435(): void
1396+
{
1397+
$this->analyse([__DIR__ . '/data/bug-11435.php'], []);
1398+
}
1399+
1400+
public function testBug10289(): void
1401+
{
1402+
$this->analyse([__DIR__ . '/data/bug-10289.php'], []);
1403+
}
1404+
13471405
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug10289;
4+
5+
use ArrayIterator;
6+
use IteratorAggregate;
7+
8+
/**
9+
* @implements IteratorAggregate<non-empty-string, non-empty-string>
10+
*/
11+
class X implements IteratorAggregate
12+
{
13+
/** @var array<non-empty-string, non-empty-string> */
14+
private array $data = ['x' => 'y'];
15+
16+
/** @return ArrayIterator<non-empty-string, non-empty-string> */
17+
public function getIterator(): ArrayIterator
18+
{
19+
return new ArrayIterator($this->data);
20+
}
21+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11435;
6+
7+
/**
8+
* @implements \IteratorAggregate<int, array{
9+
* routeName: string,
10+
* optional?: true,
11+
* }>
12+
*/
13+
class Example implements \IteratorAggregate
14+
{
15+
/**
16+
* @param list<array{
17+
* routeName: string,
18+
* optional?: true,
19+
* }> $elements
20+
*/
21+
public function __construct(
22+
private array $elements,
23+
) {
24+
}
25+
26+
public function getIterator(): \Traversable
27+
{
28+
return new \ArrayIterator($this->elements);
29+
}
30+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11835;
4+
5+
/**
6+
* @template TKey of array-key
7+
* @template TValue
8+
*/
9+
final class Collection
10+
{
11+
/** @return self<int, static> */
12+
public function chunk(int $size): self
13+
{
14+
return $this;
15+
}
16+
17+
/**
18+
* @template TMapValue
19+
*
20+
* @param callable(TValue, TKey): TMapValue $callback
21+
* @return self<TKey, TMapValue>
22+
*/
23+
public function map(callable $callback): self
24+
{
25+
return $this;
26+
}
27+
}
28+
29+
class DateHeader {}
30+
31+
class ProjectionCumulativeHeadersResolver
32+
{
33+
/**
34+
* @param Collection<int, DateHeader> $projectionMonthsHeaders
35+
* @return Collection<int, string>
36+
*/
37+
public function resolve(Collection $projectionMonthsHeaders): Collection
38+
{
39+
return $projectionMonthsHeaders->chunk(2)
40+
->map(fn ($_, $index) => $index * 2 + 2 . ' month');
41+
}
42+
}

0 commit comments

Comments
 (0)