Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
52 changes: 52 additions & 0 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_keys;
Expand Down Expand Up @@ -699,6 +700,57 @@ public function getOffsetValueType(Type $offsetType): Type

public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
{
if ($offsetType !== null) {
$scalarKeyTypes = $offsetType->toArrayKey()->getConstantStrings();
if (count($scalarKeyTypes) === 0) {
$integerRanges = TypeUtils::getIntegerRanges($offsetType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're calling getIntegerRanges on offsetType but previously you worked with

$offsetType->toArrayKey()

Shouldn't you work every where with $offsetType->toArrayKey() ?

that could have impact for things like int<0, 2>|'3' maybe ?

if (count($integerRanges) > 0) {
foreach ($integerRanges as $integerRange) {
$finiteTypes = $integerRange->getFiniteTypes();
if (count($finiteTypes) === 0) {
break;
}

foreach ($finiteTypes as $finiteType) {
$scalarKeyTypes[] = $finiteType;
}
}
}
}

// turn into tagged union for more precise results
if (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me why

  • We're working only with constantStrings (getConstantStrings)
  • We're adding int range only when there is no string

What about @param 1|2 $key ?
And what about @param '1'|2 $key ?

Also the $existingKeyType->getValue() === $scalarKeyType->getValue() check might miss some 1 === '1'.

count($scalarKeyTypes) >= 2
&& count($scalarKeyTypes) < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
) {
$hasNewKey = false;
foreach ($scalarKeyTypes as $scalarKeyType) {
$existingKeyFound = false;
foreach ($this->keyTypes as $existingKeyType) {
if ($existingKeyType->getValue() === $scalarKeyType->getValue()) {
$existingKeyFound = true;
break;
}
}
if (!$existingKeyFound) {
$hasNewKey = true;
break;
}
}

if ($hasNewKey) {
$arrayTypes = [];
foreach ($scalarKeyTypes as $scalarKeyType) {
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
$builder->setOffsetValueType($scalarKeyType, $valueType);
$arrayTypes[] = $builder->getArray();
}

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

$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
$builder->setOffsetValueType($offsetType, $valueType);

Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-9907.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace Bug9907;

use function PHPStan\Testing\assertType;

class HelloWorld
{
/**
* @param 'foo'|'bar' $key
*/
public function sayHello(string $key): void
{
$a = [];
$a['id'] = null;
$a[$key] = 'string';

assertType("array{id: null, bar: 'string'}|array{id: null, foo: 'string'}", $a);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/constant-array-type-set.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ 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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace SetConstantUnionOffsetOnConstantArray;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param array{foo: int} $a
*/
public function doFoo(array $a): void
{
$k = rand(0, 1) ? 'a' : 'b';
$a[$k] = 256;
assertType('array{foo: int, a: 256}|array{foo: int, b: 256}', $a);
}

/**
* @param array{foo: int} $a
* @param int<1,5> $intRange
*/
public function doBar(array $a, $intRange): void
{
$a[$intRange] = 256;
assertType('array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a);
}

/**
* @param array{foo: int} $a
* @param int<0, max> $intRange
*/
public function doInfiniteRange(array $a, $intRange): void
{
$a[$intRange] = 256;
assertType('non-empty-array<\'foo\'|int<0, max>, int>', $a);
}

/**
* @param array{foo: int} $a
* @param int<0, 5>|int<10, 15> $intRange
*/
public function doUnionOfRanges(array $a, $intRange): void
{
$a[$intRange] = 256;
assertType('array{foo: int, 0: 256}|array{foo: int, 10: 256}|array{foo: int, 11: 256}|array{foo: int, 12: 256}|array{foo: int, 13: 256}|array{foo: int, 14: 256}|array{foo: int, 15: 256}|array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}|array{foo: int, 5: 256}', $a);
}

/**
* @param array{foo: int} $a
* @param int<0, 3>|int<2, 4> $intRange
*/
public function doOverlappingRanges(array $a, $intRange): void
{
$a[$intRange] = 256;
assertType('array{foo: int, 0: 256}|array{foo: int, 1: 256}|array{foo: int, 2: 256}|array{foo: int, 3: 256}|array{foo: int, 4: 256}', $a);
}

/**
* @param array{0: 'a', 1: 'b'} $a
* @param int<0,1> $intRange
*/
public function doExistingKeys(array $a, $intRange): void
{
$a[$intRange] = 'c';
assertType("array{'a'|'c', 'b'|'c'}", $a);
}

}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3853,4 +3853,12 @@ public function testBug13805(): void
$this->analyse([__DIR__ . '/data/bug-13805.php'], []);
}

public function testBug7978(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-7978.php'], []);
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1310,4 +1310,9 @@ public function testBug9669(): void
$this->analyse([__DIR__ . '/data/bug-9669.php'], []);
}

public function testBug9907(): void
{
$this->analyse([__DIR__ . '/data/bug-9907.php'], []);
}

}
29 changes: 29 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-7978.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace Bug7978;

class Test {

const FIELD_SETS = [
'basic' => ['username', 'password'],
'headers' => ['app_id', 'app_key'],
];

/**
* @param array<string, string> $credentials
*/
public function acceptCredentials(array $credentials): void
{
}

public function doSomething(): void
{
foreach (self::FIELD_SETS as $type => $fields) {
$credentials = [];
foreach ($fields as $field) {
$credentials[$field] = 'fake';
}
$this->acceptCredentials($credentials);
}
}
}
34 changes: 34 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-9907.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace Bug9907Rule;

class Demo
{
/**
* @phpstan-param array{street: string, city: string} $address1
* @phpstan-param array{street: string, city: string} $address2
*
* @phpstan-return array{
* street?: array{change_to: string},
* city?: array{change_to: string},
* variation_count?: int<1, max>
* }
*/
public function diffAddresses(array $address1, array $address2): array
{
$addressDifference = array_diff_assoc($address1, $address2);
$differenceDetails = [];

foreach ($addressDifference as $name => $differenceValue) {
$differenceDetails[$name] = [
'change_to' => $differenceValue,
];
}

if (!empty(count($differenceDetails))) {
$differenceDetails['variation_count'] = count($differenceDetails);
}

return $differenceDetails;
}
}
Loading