diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 6cff7ef3af..0593049044 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -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; @@ -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); + 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 ( + 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); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9907.php b/tests/PHPStan/Analyser/nsrt/bug-9907.php new file mode 100644 index 0000000000..0985776490 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9907.php @@ -0,0 +1,20 @@ +, 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 diff --git a/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php new file mode 100644 index 0000000000..66dace8c80 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php @@ -0,0 +1,70 @@ + $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); + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 0ab14fd52a..2bb2c5c2e6 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -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'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 84e81f0f33..9b3bb88db0 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -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'], []); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/bug-7978.php b/tests/PHPStan/Rules/Methods/data/bug-7978.php new file mode 100644 index 0000000000..4b3f951835 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-7978.php @@ -0,0 +1,29 @@ + ['username', 'password'], + 'headers' => ['app_id', 'app_key'], + ]; + + /** + * @param array $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); + } + } +} diff --git a/tests/PHPStan/Rules/Methods/data/bug-9907.php b/tests/PHPStan/Rules/Methods/data/bug-9907.php new file mode 100644 index 0000000000..39086dbd70 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/bug-9907.php @@ -0,0 +1,34 @@ + + * } + */ + 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; + } +}