diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27..114545fc75 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4092,13 +4092,25 @@ private function generalizeType(Type $a, Type $b, int $depth): Type ) { $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) { + $aValueType = $constantArraysA->getOffsetValueType($keyType); + $bValueType = $constantArraysB->getOffsetValueType($keyType); + + $canPreserve = $aValueType->isInteger()->no() + && $bValueType->isInteger()->no() + && $aValueType->isArray()->no() + && $bValueType->isArray()->no(); + + if ($canPreserve && $aValueType->isSuperTypeOf($bValueType)->yes()) { + $generalizedValue = $aValueType; + } elseif ($canPreserve && $bValueType->isSuperTypeOf($aValueType)->yes()) { + $generalizedValue = $bValueType; + } else { + $generalizedValue = $this->generalizeType($aValueType, $bValueType, $depth + 1); + } + $resultArrayBuilder->setOffsetValueType( $keyType, - $this->generalizeType( - $constantArraysA->getOffsetValueType($keyType), - $constantArraysB->getOffsetValueType($keyType), - $depth + 1, - ), + $generalizedValue, !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(), ); } diff --git a/tests/PHPStan/Analyser/nsrt/array-keys-branches.php b/tests/PHPStan/Analyser/nsrt/array-keys-branches.php index 61170869b7..b4b262f858 100644 --- a/tests/PHPStan/Analyser/nsrt/array-keys-branches.php +++ b/tests/PHPStan/Analyser/nsrt/array-keys-branches.php @@ -58,7 +58,7 @@ function (array $generalArray) { assertType('mixed~null', $generalArray['key']); assertType('array{0: \'foo\', 1: \'bar\', 2?: \'baz\'}', $arrayAppendedInIf); assertType('non-empty-list<\'bar\'|\'baz\'|\'foo\'>', $arrayAppendedInForeach); - assertType('non-empty-array, literal-string&lowercase-string&non-falsy-string>', $anotherArrayAppendedInForeach); + assertType("non-empty-array, 'bar'|'baz'|'foo'>", $anotherArrayAppendedInForeach); assertType('\'str\'', $array['n']); assertType('int<0, max>', $incremented); assertType('0|1', $setFromZeroToOne); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12653.php b/tests/PHPStan/Analyser/nsrt/bug-12653.php new file mode 100644 index 0000000000..33f81139ee --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12653.php @@ -0,0 +1,42 @@ + + */ + public function main() + { + $list = [ + 'a' => Reproduction::TYPE_XXX, + 'b' => Reproduction::TYPE_YYY, + 'c' => Reproduction::TYPE_ZZZ, + 'd' => Reproduction::TYPE_XXX, + ]; + + $keys = ['a', 'b', 'c', 'd']; + $found = false; + foreach ($keys as $key) { + if ($list[$key] === Reproduction::TYPE_XXX) { + // The first matched key is kept and subsequent matched keys are rewritten. + if (!$found) { + $found = true; + } else { + $list[$key] = Reproduction::TYPE_ZZZ; + } + } + } + + assertType("array{a: 'xxx'|'zzz', b: 'yyy'|'zzz', c: 'zzz', d: 'xxx'|'zzz'}", $list); + + return $list; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-12653b.php b/tests/PHPStan/Analyser/nsrt/bug-12653b.php new file mode 100644 index 0000000000..79496e3860 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-12653b.php @@ -0,0 +1,111 @@ + + */ + public function whileLoop(): array + { + $list = [ + 'a' => self::TYPE_XXX, + 'b' => self::TYPE_YYY, + 'c' => self::TYPE_ZZZ, + 'd' => self::TYPE_XXX, + ]; + + $keys = ['a', 'b', 'c', 'd']; + $found = false; + $i = 0; + while ($i < count($keys)) { + $key = $keys[$i]; + if ($list[$key] === self::TYPE_XXX) { + if (!$found) { + $found = true; + } else { + $list[$key] = self::TYPE_ZZZ; + } + } + $i++; + } + assertType("array{a: 'xxx'|'zzz', b: 'yyy'|'zzz', c: 'zzz', d: 'xxx'|'zzz'}", $list); + + return $list; + } +} + +class ForLoopTest +{ + const TYPE_XXX = 'xxx'; + const TYPE_YYY = 'yyy'; + const TYPE_ZZZ = 'zzz'; + + /** + * @return array<'a'|'b'|'c'|'d', self::TYPE_*> + */ + public function forLoop(): array + { + $list = [ + 'a' => self::TYPE_XXX, + 'b' => self::TYPE_YYY, + 'c' => self::TYPE_ZZZ, + 'd' => self::TYPE_XXX, + ]; + + $keys = ['a', 'b', 'c', 'd']; + $found = false; + for ($i = 0; $i < count($keys); $i++) { + $key = $keys[$i]; + if ($list[$key] === self::TYPE_XXX) { + if (!$found) { + $found = true; + } else { + $list[$key] = self::TYPE_ZZZ; + } + } + } + assertType("array{a: 'xxx'|'zzz', b: 'yyy'|'zzz', c: 'zzz', d: 'xxx'|'zzz'}", $list); + + return $list; + } +} + +class FloatConstantArrayTest +{ + const RATE_LOW = 0.5; + const RATE_MED = 1.0; + const RATE_HIGH = 1.5; + + /** + * @param list<'x'|'y'|'z'> $keys + */ + public function floatConstantsInArray(array $keys): void + { + $rates = [ + 'x' => self::RATE_LOW, + 'y' => self::RATE_MED, + 'z' => self::RATE_HIGH, + ]; + + $found = false; + foreach ($keys as $key) { + if ($rates[$key] === self::RATE_LOW) { + if (!$found) { + $found = true; + } else { + $rates[$key] = self::RATE_HIGH; + } + } + } + + assertType("array{x: 0.5|1.5, y: 1.0|1.5, z: 1.5}", $rates); + } +}