Skip to content

Commit

Permalink
ConstantArrayTypeBuilder - allow setting union offset and keep array …
Browse files Browse the repository at this point in the history
…shape
  • Loading branch information
ondrejmirtes committed Nov 13, 2023
1 parent d808819 commit 1517bf8
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 42 deletions.
5 changes: 0 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -826,11 +826,6 @@ parameters:
count: 2
path: src/Type/Constant/ConstantArrayTypeBuilder.php

-
message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType\\|PHPStan\\\\Type\\\\Constant\\\\ConstantStringType but it's error\\-prone and dangerous\\.$#"
count: 1
path: src/Type/Constant/ConstantArrayTypeBuilder.php

-
message: "#^PHPDoc tag @var assumes the expression with type PHPStan\\\\Type\\\\Type is always PHPStan\\\\Type\\\\Constant\\\\ConstantIntegerType\\|PHPStan\\\\Type\\\\Constant\\\\ConstantStringType but it's error\\-prone and dangerous\\.$#"
count: 1
Expand Down
39 changes: 12 additions & 27 deletions src/Type/Constant/ConstantArrayTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,40 +118,25 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
}
if (
count($scalarTypes) > 0
&& count($scalarTypes) < self::ARRAY_COUNT_LIMIT
&& count($this->innards) === 1
&& (count($scalarTypes) * count($this->innards)) < self::ARRAY_COUNT_LIMIT
) {
$innard = $this->innards[0];
$match = true;
$valueTypes = $innard->valueTypes;
$newInnards = [];
foreach ($scalarTypes as $scalarType) {
$scalarOffsetType = $scalarType->toArrayKey();
if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
throw new ShouldNotHappenException();
}
$offsetMatch = false;

/** @var ConstantIntegerType|ConstantStringType $keyType */
foreach ($innard->keyTypes as $i => $keyType) {
if ($keyType->getValue() !== $scalarOffsetType->getValue()) {
continue;
foreach ($this->innards as $innard) {
$scalarOffsetType = $scalarType->toArrayKey();
if (!$scalarOffsetType instanceof ConstantIntegerType && !$scalarOffsetType instanceof ConstantStringType) {
throw new ShouldNotHappenException();
}

$valueTypes[$i] = TypeCombinator::union($valueTypes[$i], $valueType);
$offsetMatch = true;
}

if ($offsetMatch) {
continue;
$newInnard = $innard->duplicate();
$newInnard->setValueTypeForConstantOffset($scalarOffsetType, $valueType, $optional);
$newInnards[] = $newInnard;
}

$match = false;
}

if ($match) {
$innard->valueTypes = $valueTypes;
return;
}
$this->innards = $newInnards;

return;
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Type/Constant/ConstantArrayTypeBuilderInnards.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public function __construct(
{
}

public function duplicate(): self
{
return new self(
$this->keyTypes,
$this->valueTypes,
$this->nextAutoIndexes,
$this->optionalKeys,
$this->isList,
);
}

public function setValueTypeForNewOffset(Type $valueType, bool $optional): void
{
$newAutoIndexes = $optional ? $this->nextAutoIndexes : [];
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 @@ -822,6 +822,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6108.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1516.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6138.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-set-multi-type.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6174.php');
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5749.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5675.php');
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/data/array-fill-keys.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ function withObjectKey() : array
function withUnionKeys(): void
{
$arr1 = ['foo', rand(0, 1) ? 'bar1' : 'bar2', 'baz'];
assertType("non-empty-array<'bar1'|'bar2'|'baz'|'foo', 'b'>", array_fill_keys($arr1, 'b'));
assertType("array{foo: 'b', bar1: 'b', baz: 'b'}|array{foo: 'b', bar2: 'b', baz: 'b'}", array_fill_keys($arr1, 'b'));

$arr2 = ['foo'];
if (rand(0, 1)) {
$arr2[] = 'bar';
}
$arr2[] = 'baz';
assertType("non-empty-array<'bar'|'baz'|'foo', 'b'>", array_fill_keys($arr2, 'b'));
assertType("array{foo: 'b', bar?: 'b', baz?: 'b'}", array_fill_keys($arr2, 'b'));
}

function withOptionalKeys(): void
Expand Down
41 changes: 41 additions & 0 deletions tests/PHPStan/Analyser/data/constant-array-set-multi-type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace ConstantArrayTypeSetMultiType;

use function PHPStan\Testing\assertType;

class Foo
{

public function doFoo(): void
{
$a = [];
$b = 'foo';
if (rand(0, 1)) {
$b = 'bar';
} elseif (rand(0, 1)) {
$b = 'baz';
} elseif (rand(0, 1)) {
$b = 'lorem';
}

$a[$b] = 'test';
assertType("array{bar: 'test'}|array{baz: 'test'}|array{foo: 'test'}|array{lorem: 'test'}", $a);
}

public function doFoo2(): void
{
$b = 'foo';
if (rand(0, 1)) {
$b = 'bar';
} elseif (rand(0, 1)) {
$b = 'baz';
} elseif (rand(0, 1)) {
$b = 'lorem';
}

$c = [$b => 'test'];
assertType("array{bar: 'test'}|array{baz: 'test'}|array{foo: 'test'}|array{lorem: 'test'}", $c);
}

}
16 changes: 8 additions & 8 deletions tests/PHPStan/Analyser/data/constant-array-type-set.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ public function doFoo(int $i)
/** @var 0|1|2 $offset */
$offset = doFoo();
$c[$offset] = true;
assertType('array{bool, bool, bool}', $c);
assertType('array{false, false, true}|array{false, true, false}|array{true, false, false}', $c);

$d = [false, false, false];
/** @var int<0, 2> $offset2 */
$offset2 = doFoo();
$d[$offset2] = true;
assertType('array{bool, bool, bool}', $d);
assertType('array{false, false, true}|array{false, true, false}|array{true, false, false}', $d);

$e = [false, false, false];
/** @var 0|1|2|3 $offset3 */
$offset3 = doFoo();
$e[$offset3] = true;
assertType('non-empty-array<0|1|2|3, bool>', $e);
assertType('array{false, false, false, true}|array{false, false, true}|array{false, true, false}|array{true, false, false}', $e);

$f = [false, false, false];
/** @var 0|1 $offset4 */
$offset4 = doFoo();
$f[$offset4] = true;
assertType('array{bool, bool, false}', $f);
assertType('array{false, true, false}|array{true, false, false}', $f);
}

/**
Expand All @@ -50,7 +50,7 @@ public function doBar(int $offset): void
{
$a = [false, false, false];
$a[$offset] = true;
assertType('array{bool, bool, false}', $a);
assertType('array{false, true, false}|array{true, false, false}', $a);
}

/**
Expand All @@ -61,7 +61,7 @@ public function doBar2(int $offset): void
{
$a = [false, false, false, false, false];
$a[$offset] = true;
assertType('array{bool, bool, false, bool, bool}', $a);
assertType('array{false, false, false, false, true}|array{false, false, false, true, false}|array{false, true, false, false, false}|array{true, false, false, false, false}', $a);
}

/**
Expand Down Expand Up @@ -94,14 +94,14 @@ public function doBar5(int $offset): void
{
$a = [false, false, false];
$a[$offset] = true;
assertType('non-empty-array<int<0, 4>, bool>', $a);
assertType('array{0: false, 1: false, 2: false, 4: true}|array{false, false, false, true}|array{false, false, true}|array{false, true, false}|array{true, false, false}', $a);
}

public function doBar6(bool $offset): void
{
$a = [false, false, false];
$a[$offset] = true;
assertType('array{bool, bool, false}', $a);
assertType('array{false, true, false}|array{true, false, false}', $a);
}

/**
Expand Down

0 comments on commit 1517bf8

Please sign in to comment.