From a96cdf2bf5cf2b145a5c0fbbe007776e8d7bd7b8 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 26 Aug 2022 14:03:41 +0200 Subject: [PATCH] array_intersect_key() for constant arrays --- conf/config.neon | 5 ++ ...gumentBasedFunctionReturnTypeExtension.php | 1 - ...ntersectKeyFunctionReturnTypeExtension.php | 73 +++++++++++++++++++ .../Analyser/LegacyNodeScopeResolverTest.php | 4 +- .../Analyser/NodeScopeResolverTest.php | 1 + .../data/array-intersect-key-constant.php | 37 ++++++++++ 6 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/data/array-intersect-key-constant.php diff --git a/conf/config.neon b/conf/config.neon index acb5e0dc91..55b330e82e 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1045,6 +1045,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayIntersectKeyFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php index f5381f9099..00a4cabed3 100644 --- a/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php @@ -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, diff --git a/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..81843f80a2 --- /dev/null +++ b/src/Type/Php/ArrayIntersectKeyFunctionReturnTypeExtension.php @@ -0,0 +1,73 @@ +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); + } + +} diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 3b23506d5c..218bbe6fe8 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -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', + 'array{1|4, 2|5, 3|6}', 'array_intersect_key(...[$integers, [4, 5, 6]])', ], [ diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index cc6df70d2e..60464fa311 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -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'); } /** diff --git a/tests/PHPStan/Analyser/data/array-intersect-key-constant.php b/tests/PHPStan/Analyser/data/array-intersect-key-constant.php new file mode 100644 index 0000000000..b51bae631c --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-intersect-key-constant.php @@ -0,0 +1,37 @@ +, require-dev: array, stability: string|null, license: string|null, repository: array, 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, require-dev: array, stability: string|null, license: string|null, repository: array, 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, require-dev?: non-empty-array, 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)); + } + +}