Skip to content

Commit a96cdf2

Browse files
committed
array_intersect_key() for constant arrays
1 parent 771b860 commit a96cdf2

File tree

6 files changed

+118
-3
lines changed

6 files changed

+118
-3
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,11 @@ services:
10451045
tags:
10461046
- phpstan.broker.dynamicFunctionReturnTypeExtension
10471047

1048+
-
1049+
class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension
1050+
tags:
1051+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1052+
10481053
-
10491054
class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension
10501055
tags:

src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnT
2929
'array_udiff_uassoc' => 0,
3030
'array_udiff' => 0,
3131
'array_intersect_assoc' => 0,
32-
'array_intersect_key' => 0,
3332
'array_intersect_uassoc' => 0,
3433
'array_intersect_ukey' => 0,
3534
'array_intersect' => 0,
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\ArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
11+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\Type;
13+
use PHPStan\Type\TypeCombinator;
14+
use PHPStan\Type\TypeUtils;
15+
use function array_slice;
16+
use function count;
17+
18+
class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
19+
{
20+
21+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
22+
{
23+
return $functionReflection->getName() === 'array_intersect_key';
24+
}
25+
26+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
27+
{
28+
$args = $functionCall->getArgs();
29+
if (count($args) === 0) {
30+
return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType();
31+
}
32+
33+
$argTypes = [];
34+
foreach ($args as $arg) {
35+
$argType = $scope->getType($arg->value);
36+
if ($arg->unpack) {
37+
$argTypes[] = $argType->getIterableValueType();
38+
continue;
39+
}
40+
41+
$argTypes[] = $argType;
42+
}
43+
44+
$firstArray = $argTypes[0];
45+
$otherArrays = array_slice($argTypes, 1);
46+
if (count($otherArrays) === 0) {
47+
return $firstArray;
48+
}
49+
50+
$constantArrays = TypeUtils::getConstantArrays($firstArray);
51+
if (count($constantArrays) === 0) {
52+
return new ArrayType($firstArray->getIterableKeyType(), $firstArray->getIterableValueType());
53+
}
54+
55+
$otherArraysType = TypeCombinator::union(...$otherArrays);
56+
$results = [];
57+
foreach ($constantArrays as $constantArray) {
58+
$builder = ConstantArrayTypeBuilder::createEmpty();
59+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
60+
$valueType = $constantArray->getValueTypes()[$i];
61+
$has = $otherArraysType->hasOffsetValueType($keyType);
62+
if ($has->no()) {
63+
continue;
64+
}
65+
$builder->setOffsetValueType($keyType, $valueType, !$has->yes());
66+
}
67+
$results[] = $builder->getArray();
68+
}
69+
70+
return TypeCombinator::union(...$results);
71+
}
72+
73+
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4567,11 +4567,11 @@ public function dataArrayFunctions(): array
45674567
'array_intersect_assoc($integers, [])',
45684568
],
45694569
[
4570-
'array<0|1|2, 1|2|3>',
4570+
'array{}',
45714571
'array_intersect_key($integers, [])',
45724572
],
45734573
[
4574-
'array<int, int>',
4574+
'array{1|4, 2|5, 3|6}',
45754575
'array_intersect_key(...[$integers, [4, 5, 6]])',
45764576
],
45774577
[

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,7 @@ public function dataFileAsserts(): iterable
997997
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php');
998998
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php');
999999
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php');
1000+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php');
10001001
}
10011002

10021003
/**
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace ArrayIntersectKeyConstant;
4+
5+
use function array_intersect_key;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
11+
/**
12+
* @param array{name: string|null, description: string|null, author: string|null, type: string|null, homepage: string|null, require: array<int, string>, require-dev: array<int, string>, stability: string|null, license: string|null, repository: array<int, string>, autoload: string|null, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool, profile: bool, no-plugins: bool, no-scripts: bool, working-dir: string|null, no-cache: bool} $options
13+
* @return void
14+
*/
15+
public function doFoo(array $options): void
16+
{
17+
assertType('array{name: string|null, description: string|null, author: string|null, type: string|null, homepage: string|null, require: array<int, string>, require-dev: array<int, string>, stability: string|null, license: string|null, repository: array<int, string>, autoload: string|null, help: bool, quiet: bool, verbose: bool, version: bool, ansi: bool, no-interaction: bool, profile: bool, no-plugins: bool, no-scripts: bool, working-dir: string|null, no-cache: bool}', $options);
18+
19+
$allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload'];
20+
$options = array_filter(array_intersect_key($options, array_flip($allowlist)));
21+
assertType('array{name?: non-falsy-string, description?: non-falsy-string, author?: non-falsy-string, type?: non-falsy-string, homepage?: non-falsy-string, require?: non-empty-array<int, string>, require-dev?: non-empty-array<int, string>, stability?: non-falsy-string, license?: non-falsy-string, autoload?: non-falsy-string}', $options);
22+
}
23+
24+
public function doBar(): void
25+
{
26+
assertType('array{a: 1}', array_intersect_key(['a' => 1]));
27+
assertType('array{}', array_intersect_key(['a' => 1], []));
28+
29+
$a = ['a' => 1];
30+
if (rand(0, 1)) {
31+
$a['b'] = 2;
32+
}
33+
34+
assertType('array{a: 1, b?: 2}', array_intersect_key(['a' => 1, 'b' => 2], $a));
35+
}
36+
37+
}

0 commit comments

Comments
 (0)