Skip to content
Closed
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
90 changes: 90 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 All @@ -61,6 +62,7 @@
use function count;
use function implode;
use function in_array;
use function is_int;
use function is_string;
use function min;
use function pow;
Expand Down Expand Up @@ -699,6 +701,37 @@ public function getOffsetValueType(Type $offsetType): Type

public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
{
if ($offsetType !== null) {
$scalarKeyTypes = $this->resolveFiniteScalarKeyTypes($offsetType);
if ($scalarKeyTypes !== null) {
$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 All @@ -713,6 +746,63 @@ public function setExistingOffsetValueType(Type $offsetType, Type $valueType): T
return $builder->getArray();
}

/**
* @return list<ConstantIntegerType|ConstantStringType>|null
*/
private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array
{
$offsetType = $offsetType->toArrayKey();

// Handle unions of constant string types (e.g. 'a'|'b')
$constantStrings = $offsetType->getConstantStrings();
if (count($constantStrings) >= 2 && count($constantStrings) <= self::CHUNK_FINITE_TYPES_LIMIT) {
$result = [];
foreach ($constantStrings as $constantString) {
$scalarValues = $constantString->getConstantScalarValues();
if (count($scalarValues) !== 1) {
return null;
}
if (is_int($scalarValues[0])) {
$result[] = new ConstantIntegerType($scalarValues[0]);
} elseif (is_string($scalarValues[0])) {
$result[] = new ConstantStringType($scalarValues[0]);
} else {
return null;
}
}
return $result;
}

// Handle integer range types (e.g. int<1,5>)
$integerRanges = TypeUtils::getIntegerRanges($offsetType);
if (count($integerRanges) > 0) {
$finiteScalarTypes = [];
$seen = [];
foreach ($integerRanges as $integerRange) {
$finiteTypes = $integerRange->getFiniteTypes();
if ($finiteTypes === []) {
return null;
}

foreach ($finiteTypes as $finiteType) {
if (isset($seen[$finiteType->getValue()])) {
continue;
}
$seen[$finiteType->getValue()] = true;
$finiteScalarTypes[] = $finiteType;
}
}

if (count($finiteScalarTypes) < 2 || count($finiteScalarTypes) > self::CHUNK_FINITE_TYPES_LIMIT) {
return null;
}

return $finiteScalarTypes;
}

return null;
}
Comment on lines +749 to +804
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/**
* @return list<ConstantIntegerType|ConstantStringType>|null
*/
private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array
{
$offsetType = $offsetType->toArrayKey();
// Handle unions of constant string types (e.g. 'a'|'b')
$constantStrings = $offsetType->getConstantStrings();
if (count($constantStrings) >= 2 && count($constantStrings) <= self::CHUNK_FINITE_TYPES_LIMIT) {
$result = [];
foreach ($constantStrings as $constantString) {
$scalarValues = $constantString->getConstantScalarValues();
if (count($scalarValues) !== 1) {
return null;
}
if (is_int($scalarValues[0])) {
$result[] = new ConstantIntegerType($scalarValues[0]);
} elseif (is_string($scalarValues[0])) {
$result[] = new ConstantStringType($scalarValues[0]);
} else {
return null;
}
}
return $result;
}
// Handle integer range types (e.g. int<1,5>)
$integerRanges = TypeUtils::getIntegerRanges($offsetType);
if (count($integerRanges) > 0) {
$finiteScalarTypes = [];
$seen = [];
foreach ($integerRanges as $integerRange) {
$finiteTypes = $integerRange->getFiniteTypes();
if ($finiteTypes === []) {
return null;
}
foreach ($finiteTypes as $finiteType) {
if (isset($seen[$finiteType->getValue()])) {
continue;
}
$seen[$finiteType->getValue()] = true;
$finiteScalarTypes[] = $finiteType;
}
}
if (count($finiteScalarTypes) < 2 || count($finiteScalarTypes) > self::CHUNK_FINITE_TYPES_LIMIT) {
return null;
}
return $finiteScalarTypes;
}
return null;
}
/** @return array<ConstantIntegerType|ConstantStringType>|null */
private function resolveFiniteScalarKeyTypes(Type $offsetType): ?array
{
$result = [];
$offsetType = $offsetType->toArrayKey();
$constantStrings = $offsetType->getConstantStrings();
if (count($constantStrings) > 0) {
foreach ($constantStrings as $constantString) {
$scalarValues = $constantString->getConstantScalarValues();
if (count($scalarValues) !== 1) {
return null;
}
if (is_int($scalarValues[0])) {
$result[] = new ConstantIntegerType($scalarValues[0]);
} elseif (is_string($scalarValues[0])) {
$result[] = new ConstantStringType($scalarValues[0]);
} else {
return null;
}
}
} else {
$integerRanges = TypeUtils::getIntegerRanges($offsetType);
foreach ($integerRanges as $integerRange) {
$finiteTypes = $integerRange->getFiniteTypes();
if ($finiteTypes === []) {
return null;
}
foreach ($finiteTypes as $finiteType) {
$result[$finiteType->getValue()] = $finiteType;
}
}
}
if (count($result) >= 2 && count($result) <= self::CHUNK_FINITE_TYPES_LIMIT) {
return $result;
}
return null;
}


public function unsetOffset(Type $offsetType): Type
{
$offsetType = $offsetType->toArrayKey();
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

namespace Bug13000;

use function PHPStan\Testing\assertType;

function (): void {
$r = [];
foreach (['a' => '1', 'b' => '2'] as $key => $val) {
$r[$key] = $val;
}
assertType("array{a?: '1'|'2', b?: '1'|'2'}", $r);
};
32 changes: 32 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13759.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug13759;

use function PHPStan\Testing\assertType;

class Test
{
public function scenario(): void
{
$ints = [];
foreach (['a', 'b'] as $key) {
$ints[$key] = 1;
}
$ints['c'] = 1;

assertType("array{a?: 1, b?: 1, c: 1}", $ints);

foreach (['a'] as $key) {
$ints[$key] = $this->intToSomething($ints[$key]);
}

assertType("array{a: float|string, b?: 1, c: 1}", $ints);
}

/**
* @return string|float
*/
protected function intToSomething(int $int): string|float {
return mt_rand(1, 2) ? (string)$int : (float)$int;
}
}
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-2294.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php declare(strict_types = 1);

namespace Bug2294;

use function PHPStan\Testing\assertType;

function (): void {
$entries = ['A' => null, 'B' => null];

$entries2 = [];
foreach($entries as $key => $value) {
$entries2[$key] = ['a' => 1, 'b' => 2];
}
assertType("array{A?: array{a: 1, b: 2}, B?: array{a: 1, b: 2}}", $entries2);
};
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('non-empty-array<\'foo\'|int<0, 5>|int<10, 15>, int>', $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);
Copy link
Contributor

Choose a reason for hiding this comment

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

this bug should be covered via CallMethodsRuleTest instead


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