Skip to content

Commit

Permalink
array_intersect_key() for constant arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 26, 2022
1 parent 771b860 commit a96cdf2
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 3 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension
tags:
Expand Down
1 change: 0 additions & 1 deletion src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class ArgumentBasedFunctionReturnTypeExtension implements DynamicFunctionReturnT
'array_udiff_uassoc' => 0,
'array_udiff' => 0,
'array_intersect_assoc' => 0,
'array_intersect_key' => 0,
'array_intersect_uassoc' => 0,
'array_intersect_ukey' => 0,
'array_intersect' => 0,
Expand Down
73 changes: 73 additions & 0 deletions src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function array_slice;
use function count;

class ArrayIntersectKeyFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_intersect_key';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$args = $functionCall->getArgs();
if (count($args) === 0) {
return ParametersAcceptorSelector::selectFromArgs($scope, $args, $functionReflection->getVariants())->getReturnType();
}

$argTypes = [];
foreach ($args as $arg) {
$argType = $scope->getType($arg->value);
if ($arg->unpack) {
$argTypes[] = $argType->getIterableValueType();
continue;
}

$argTypes[] = $argType;
}

$firstArray = $argTypes[0];
$otherArrays = array_slice($argTypes, 1);
if (count($otherArrays) === 0) {
return $firstArray;
}

$constantArrays = TypeUtils::getConstantArrays($firstArray);
if (count($constantArrays) === 0) {
return new ArrayType($firstArray->getIterableKeyType(), $firstArray->getIterableValueType());
}

$otherArraysType = TypeCombinator::union(...$otherArrays);
$results = [];
foreach ($constantArrays as $constantArray) {
$builder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
$valueType = $constantArray->getValueTypes()[$i];
$has = $otherArraysType->hasOffsetValueType($keyType);
if ($has->no()) {
continue;
}
$builder->setOffsetValueType($keyType, $valueType, !$has->yes());
}
$results[] = $builder->getArray();
}

return TypeCombinator::union(...$results);
}

}
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4567,11 +4567,11 @@ public function dataArrayFunctions(): array
'array_intersect_assoc($integers, [])',
],
[
'array<0|1|2, 1|2|3>',
'array{}',
'array_intersect_key($integers, [])',
],
[
'array<int, int>',
'array{1|4, 2|5, 3|6}',
'array_intersect_key(...[$integers, [4, 5, 6]])',
],
[
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5845.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-flip-constant.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-filter-constant.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-intersect-key-constant.php');
}

/**
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/array-intersect-key-constant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace ArrayIntersectKeyConstant;

use function array_intersect_key;
use function PHPStan\Testing\assertType;

class Foo
{

/**
* @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
* @return void
*/
public function doFoo(array $options): void
{
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);

$allowlist = ['name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license', 'autoload'];
$options = array_filter(array_intersect_key($options, array_flip($allowlist)));
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);
}

public function doBar(): void
{
assertType('array{a: 1}', array_intersect_key(['a' => 1]));
assertType('array{}', array_intersect_key(['a' => 1], []));

$a = ['a' => 1];
if (rand(0, 1)) {
$a['b'] = 2;
}

assertType('array{a: 1, b?: 2}', array_intersect_key(['a' => 1, 'b' => 2], $a));
}

}

0 comments on commit a96cdf2

Please sign in to comment.