Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/Type/ArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,57 @@ public function intersectKeyArray(Type $otherArraysType): Type
return $this;
}

$constantArrays = $otherArraysType->getConstantArrays();
if (count($constantArrays) > 0) {
// When the other operand is one or more array shapes with a known
// sealedness, the result is a (possibly unsealed) array shape too:
// it can only contain the keys present in those shapes, each
// optional because the general first array may or may not have it.
$allSealednessKnown = true;
foreach ($constantArrays as $constantArray) {
if ($constantArray->isUnsealed()->maybe()) {
$allSealednessKnown = false;
break;
}
}

if ($allSealednessKnown) {
$results = [];
foreach ($constantArrays as $constantArray) {
$results[] = $this->intersectConstantArrayShape($constantArray);
}

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

return $this->withTypes($otherArraysType->getIterableKeyType(), $this->getIterableValueType());
}

private function intersectConstantArrayShape(ConstantArrayType $constantArray): Type
{
$builder = ConstantArrayTypeBuilder::createEmpty();

$valueType = $this->getIterableValueType();
$keyType = $this->getIterableKeyType();
foreach ($constantArray->getKeyTypes() as $shapeKeyType) {
if (TypeCombinator::intersect($shapeKeyType, $keyType) instanceof NeverType) {
continue;
}
$builder->setOffsetValueType($shapeKeyType, $valueType, true);
}

$unsealed = $constantArray->getUnsealedTypes();
if ($constantArray->isUnsealed()->yes() && $unsealed !== null) {
$narrowedUnsealedKey = TypeCombinator::intersect($unsealed[0], $keyType);
if (!$narrowedUnsealedKey instanceof NeverType) {
$builder->makeUnsealed($narrowedUnsealedKey, $valueType);
}
}

return $builder->getArray();
}

public function popArray(): Type
{
return $this;
Expand Down
14 changes: 7 additions & 7 deletions tests/PHPStan/Analyser/nsrt/array-intersect-key.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function nonEmpty(array $arr, array $arr2): void
assertType('array<int, string>', array_intersect_key($arr, $arr2));
assertType('array<int, string>', array_intersect_key($arr2, $arr));
assertType('array{}', array_intersect_key($arr, []));
assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17]));
assertType('array{foo?: string}', array_intersect_key($arr, ['foo' => 17]));
}


Expand All @@ -34,7 +34,7 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void
assertType('array<int, string>', array_intersect_key($arr, $arr2));
assertType('array<int, string>', array_intersect_key($arr2, $arr));
assertType('array{}', array_intersect_key($arr, []));
assertType("array<'foo', string>", array_intersect_key($arr, ['foo' => 17]));
assertType('array{foo?: string}', array_intersect_key($arr, ['foo' => 17]));

/** @var array<int, string> $otherArrs */
assertType('array<int, string>', array_intersect_key($arr, $otherArrs));
Expand All @@ -43,20 +43,20 @@ public function normalArrays(array $arr, array $arr2, array $otherArrs): void
/** @var array<string, int> $otherArrs */
assertType('array<string, string>', array_intersect_key($arr, $otherArrs));
/** @var array<17, int> $otherArrs */
assertType('array<17, string>', array_intersect_key($arr, $otherArrs));
assertType('array{17?: string}', array_intersect_key($arr, $otherArrs));
/** @var array<null, int> $otherArrs */
assertType('array<\'\', string>', array_intersect_key($arr, $otherArrs));
assertType('array{\'\'?: string}', array_intersect_key($arr, $otherArrs));

if (array_key_exists(17, $arr2)) {
assertType('non-empty-array<17, string>&hasOffset(17)', array_intersect_key($arr2, [17 => 'bar']));
assertType('array{17: string}', array_intersect_key($arr2, [17 => 'bar']));
/** @var array<int, string> $otherArrs */
assertType('array<int, string>', array_intersect_key($arr2, $otherArrs));
/** @var array<string, int> $otherArrs */
assertType('array{}', array_intersect_key($arr2, $otherArrs));
}

if (array_key_exists(17, $arr2) && $arr2[17] === 'foo') {
assertType("non-empty-array<17, string>&hasOffsetValue(17, 'foo')", array_intersect_key($arr2, [17 => 'bar']));
assertType("array{17: 'foo'}", array_intersect_key($arr2, [17 => 'bar']));
/** @var array<int, string> $otherArrs */
assertType('array<int, string>', array_intersect_key($arr2, $otherArrs));
/** @var array<string, int> $otherArrs */
Expand All @@ -79,7 +79,7 @@ public function arrayUnpacking(array $arrs, array $arrs2): void
/** @param list<string> $arr */
public function list(array $arr, array $otherArrs): void
{
assertType('list<string>', array_intersect_key($arr, ['foo', 'bar']));
assertType('list{0?: string, 1?: string}', array_intersect_key($arr, ['foo', 'bar']));
/** @var array<int, string> $otherArrs */
assertType('array<int<0, max>, string>', array_intersect_key($arr, $otherArrs));
/** @var array<string, int> $otherArrs */
Expand Down
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14747.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Bug14747;

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

/** @return array<mixed> */
function get_options(): array {
return ['foo' => 'bar'];
}

function test(): void {
$options = get_options();

$o = array_intersect_key($options, ['a' => null, 'b' => null]);
assertType('array{a?: mixed, b?: mixed}', $o);
}

/**
* @param array<int|string, string> $arr
*/
function literalShape(array $arr): void {
assertType('array{foo?: string}', array_intersect_key($arr, ['foo' => 17]));
assertType('array{a?: string, b?: string}', array_intersect_key($arr, ['a' => 1, 'b' => 2]));
}

/**
* @param array<int, string> $arr
*/
function keysOutsideRange(array $arr): void {
// string keys cannot intersect int-keyed array
assertType('array{}', array_intersect_key($arr, ['foo' => 17, 'bar' => 18]));
}

/**
* @param array<int|string, string> $arr
*/
function unionOfShapes(array $arr, bool $b): void {
$other = $b ? ['a' => 1] : ['b' => 2, 'c' => 3];
assertType('array{a?: string}|array{b?: string, c?: string}', array_intersect_key($arr, $other));
}

/**
* @param array<int|string, string> $arr
* @param array{a: 1, ...<string, mixed>} $unsealed
*/
function unsealedShape(array $arr, array $unsealed): void {
assertType('array{a?: string, ...<string, string>}', array_intersect_key($arr, $unsealed));
}
Loading