Skip to content

Commit 724c8ba

Browse files
committed
Bleeding edge - intersect array key type with int|string
1 parent 57e3cbf commit 724c8ba

7 files changed

Lines changed: 162 additions & 3 deletions

File tree

conf/config.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,8 @@ services:
404404

405405
-
406406
class: PHPStan\PhpDoc\TypeNodeResolver
407+
arguments:
408+
deepInspectTypes: %featureToggles.deepInspectTypes%
407409

408410
-
409411
class: PHPStan\PhpDoc\TypeNodeResolverExtensionRegistryProvider

src/PhpDoc/TypeNodeResolver.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,17 @@ class TypeNodeResolver
7373

7474
private Container $container;
7575

76+
private bool $deepInspectTypes;
77+
7678
public function __construct(
7779
TypeNodeResolverExtensionRegistryProvider $extensionRegistryProvider,
78-
Container $container
80+
Container $container,
81+
bool $deepInspectTypes = false
7982
)
8083
{
8184
$this->extensionRegistryProvider = $extensionRegistryProvider;
8285
$this->container = $container;
86+
$this->deepInspectTypes = $deepInspectTypes;
8387
}
8488

8589
/** @api */
@@ -417,7 +421,14 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
417421
if (count($genericTypes) === 1) { // array<ValueType>
418422
$arrayType = new ArrayType(new MixedType(true), $genericTypes[0]);
419423
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
420-
$arrayType = new ArrayType($genericTypes[0], $genericTypes[1]);
424+
$keyType = $genericTypes[0];
425+
if ($this->deepInspectTypes) {
426+
$keyType = TypeCombinator::intersect($keyType, new UnionType([
427+
new IntegerType(),
428+
new StringType(),
429+
]));
430+
}
431+
$arrayType = new ArrayType($keyType, $genericTypes[1]);
421432
} else {
422433
return new ErrorType();
423434
}

tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ public function testIsGenerator(): void
8686
public function testBug2568(): void
8787
{
8888
require_once __DIR__ . '/data/bug-2568.php';
89-
$this->analyse([__DIR__ . '/data/bug-2568.php'], []);
89+
$this->analyse([__DIR__ . '/data/bug-2568.php'], [
90+
[
91+
'Function Bug2568\my_array_keys() should return array<int, T> but returns array<int, (int&T)|(string&T)>.',
92+
12,
93+
],
94+
]);
9095
}
9196

9297
public function testBug2723(): void

tests/PHPStan/Rules/Functions/data/bug-2568.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,23 @@
1111
function my_array_keys($arr) {
1212
return array_keys($arr);
1313
}
14+
15+
/**
16+
* @template T of array-key
17+
*
18+
* @param array<T, mixed> $arr
19+
* @return array<int, T>
20+
*/
21+
function my_array_keys2($arr) {
22+
return array_keys($arr);
23+
}
24+
25+
/**
26+
* @template T of int|string
27+
*
28+
* @param array<T, mixed> $arr
29+
* @return array<int, T>
30+
*/
31+
function my_array_keys3($arr) {
32+
return array_keys($arr);
33+
}

tests/PHPStan/Rules/Methods/data/infer-array-key.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,79 @@ public function getIterator()
2222
}
2323

2424
}
25+
26+
/**
27+
* @implements \IteratorAggregate<int, \stdClass>
28+
*/
29+
class Bar implements \IteratorAggregate
30+
{
31+
32+
/** @var array<int, \stdClass> */
33+
private $items;
34+
35+
public function getIterator()
36+
{
37+
$it = new \ArrayIterator($this->items);
38+
assertType('int', $it->key());
39+
40+
return $it;
41+
}
42+
43+
}
44+
45+
/**
46+
* @implements \IteratorAggregate<string, \stdClass>
47+
*/
48+
class Baz implements \IteratorAggregate
49+
{
50+
51+
/** @var array<string, \stdClass> */
52+
private $items;
53+
54+
public function getIterator()
55+
{
56+
$it = new \ArrayIterator($this->items);
57+
assertType('string', $it->key());
58+
59+
return $it;
60+
}
61+
62+
}
63+
64+
/**
65+
* @implements \IteratorAggregate<int, \stdClass>
66+
*/
67+
class Lorem implements \IteratorAggregate
68+
{
69+
70+
/** @var array<\stdClass> */
71+
private $items;
72+
73+
public function getIterator()
74+
{
75+
$it = new \ArrayIterator($this->items);
76+
assertType('(int|string)', $it->key());
77+
78+
return $it;
79+
}
80+
81+
}
82+
83+
/**
84+
* @implements \IteratorAggregate<int|string, \stdClass>
85+
*/
86+
class Ipsum implements \IteratorAggregate
87+
{
88+
89+
/** @var array<int|string, \stdClass> */
90+
private $items;
91+
92+
public function getIterator()
93+
{
94+
$it = new \ArrayIterator($this->items);
95+
assertType('int|string', $it->key());
96+
97+
return $it;
98+
}
99+
100+
}

tests/PHPStan/Rules/PhpDoc/IncompatiblePhpDocTypeRuleTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ public function testBug3753(): void
162162
'PHPDoc tag @param for parameter $foo contains unresolvable type.',
163163
20,
164164
],
165+
[
166+
'PHPDoc tag @param for parameter $bars contains unresolvable type.',
167+
28,
168+
],
165169
]);
166170
}
167171

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
use PHPStan\Type\Constant\ConstantStringType;
1616
use PHPStan\Type\Generic\GenericClassStringType;
1717
use PHPStan\Type\Generic\GenericObjectType;
18+
use PHPStan\Type\Generic\TemplateBenevolentUnionType;
1819
use PHPStan\Type\Generic\TemplateObjectType;
1920
use PHPStan\Type\Generic\TemplateObjectWithoutClassType;
2021
use PHPStan\Type\Generic\TemplateType;
2122
use PHPStan\Type\Generic\TemplateTypeFactory;
2223
use PHPStan\Type\Generic\TemplateTypeScope;
2324
use PHPStan\Type\Generic\TemplateTypeVariance;
25+
use PHPStan\Type\Generic\TemplateUnionType;
2426

2527
class TypeCombinatorTest extends \PHPStan\Testing\TestCase
2628
{
@@ -2945,6 +2947,45 @@ public function dataIntersect(): array
29452947
NeverType::class,
29462948
'*NEVER*',
29472949
],
2950+
[
2951+
[
2952+
TemplateTypeFactory::create(
2953+
TemplateTypeScope::createWithFunction('my_array_keys'),
2954+
'T',
2955+
new BenevolentUnionType([new IntegerType(), new StringType()]),
2956+
TemplateTypeVariance::createInvariant(),
2957+
),
2958+
new UnionType([new IntegerType(), new StringType()]),
2959+
],
2960+
TemplateBenevolentUnionType::class,
2961+
'T of (int|string) (function my_array_keys(), parameter)',
2962+
],
2963+
[
2964+
[
2965+
TemplateTypeFactory::create(
2966+
TemplateTypeScope::createWithFunction('my_array_keys'),
2967+
'T',
2968+
new BenevolentUnionType([new IntegerType(), new StringType()]),
2969+
TemplateTypeVariance::createInvariant(),
2970+
),
2971+
new BenevolentUnionType([new IntegerType(), new StringType()]),
2972+
],
2973+
TemplateBenevolentUnionType::class,
2974+
'T of (int|string) (function my_array_keys(), parameter)',
2975+
],
2976+
[
2977+
[
2978+
TemplateTypeFactory::create(
2979+
TemplateTypeScope::createWithFunction('my_array_keys'),
2980+
'T',
2981+
new UnionType([new IntegerType(), new StringType()]),
2982+
TemplateTypeVariance::createInvariant(),
2983+
),
2984+
new UnionType([new IntegerType(), new StringType()]),
2985+
],
2986+
UnionType::class,
2987+
'T of int|string (function my_array_keys(), parameter)',
2988+
],
29482989
];
29492990
}
29502991

0 commit comments

Comments
 (0)