diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index ebb588dc0fd..0a9ec6fbe44 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -575,31 +575,50 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni public function unsetOffset(Type $offsetType): Type { $offsetType = ArrayType::castToArrayKeyType($offsetType); - if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) { + + $results = []; + foreach (TypeUtils::flattenTypes($offsetType) as $flattenType) { + if (!$flattenType instanceof ConstantIntegerType && !$flattenType instanceof ConstantStringType) { + return new ArrayType($this->getKeyType(), $this->getItemType()); + } + + $hasKey = false; foreach ($this->keyTypes as $i => $keyType) { - if ($keyType->getValue() === $offsetType->getValue()) { - $keyTypes = $this->keyTypes; - unset($keyTypes[$i]); - $valueTypes = $this->valueTypes; - unset($valueTypes[$i]); - - $newKeyTypes = []; - $newValueTypes = []; - $newOptionalKeys = []; - - $k = 0; - foreach ($keyTypes as $j => $newKeyType) { - $newKeyTypes[] = $newKeyType; - $newValueTypes[] = $valueTypes[$j]; - if (in_array($j, $this->optionalKeys, true)) { - $newOptionalKeys[] = $k; - } - $k++; - } + if ($keyType->getValue() !== $flattenType->getValue()) { + continue; + } - return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys); + $hasKey = true; + $keyTypes = $this->keyTypes; + unset($keyTypes[$i]); + $valueTypes = $this->valueTypes; + unset($valueTypes[$i]); + + $newKeyTypes = []; + $newValueTypes = []; + $newOptionalKeys = []; + + $k = 0; + foreach ($keyTypes as $j => $newKeyType) { + $newKeyTypes[] = $newKeyType; + $newValueTypes[] = $valueTypes[$j]; + if (in_array($j, $this->optionalKeys, true)) { + $newOptionalKeys[] = $k; + } + $k++; } + + $results[] = new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys); + break; + } + if ($hasKey) { + continue; } + + $results[] = $this; + } + if ($results !== []) { + return TypeCombinator::union(...$results); } return new ArrayType($this->getKeyType(), $this->getItemType()); diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 9f6a8338786..c38c527412e 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -793,7 +793,7 @@ public function testBug7215(): void public function testBug7094(): void { $errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php'); - $this->assertCount(7, $errors); + $this->assertCount(6, $errors); $this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() contains unresolvable type.', $errors[0]->getMessage()); $this->assertSame(74, $errors[0]->getLine()); @@ -808,8 +808,6 @@ public function testBug7094(): void $this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array given.', $errors[5]->getMessage()); $this->assertSame(29, $errors[5]->getLine()); - $this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, array<\'bar\'|\'baz\'|\'foo\', 5|6|7|bool|string> given.', $errors[6]->getMessage()); - $this->assertSame(49, $errors[6]->getLine()); } public function testOffsetAccess(): void diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index fe64bcfebd3..ef18e501a6b 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -935,6 +935,9 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/this-subtractable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expression-inference.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1519.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-1.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-3.php'); } /** diff --git a/tests/PHPStan/Analyser/data/bug-7621-1.php b/tests/PHPStan/Analyser/data/bug-7621-1.php new file mode 100644 index 00000000000..733e68323af --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7621-1.php @@ -0,0 +1,148 @@ + [ + self::GROUP_PUBLIC_CONSTANTS, + self::GROUP_PROTECTED_CONSTANTS, + self::GROUP_PRIVATE_CONSTANTS, + ], + self::GROUP_SHORTCUT_STATIC_PROPERTIES => [ + self::GROUP_PUBLIC_STATIC_PROPERTIES, + self::GROUP_PROTECTED_STATIC_PROPERTIES, + self::GROUP_PRIVATE_STATIC_PROPERTIES, + ], + self::GROUP_SHORTCUT_PROPERTIES => [ + self::GROUP_SHORTCUT_STATIC_PROPERTIES, + self::GROUP_PUBLIC_PROPERTIES, + self::GROUP_PROTECTED_PROPERTIES, + self::GROUP_PRIVATE_PROPERTIES, + ], + self::GROUP_SHORTCUT_PUBLIC_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PUBLIC_METHODS, + ], + self::GROUP_SHORTCUT_PROTECTED_METHODS => [ + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PROTECTED_METHODS, + ], + self::GROUP_SHORTCUT_PRIVATE_METHODS => [ + self::GROUP_PRIVATE_STATIC_METHODS, + self::GROUP_PRIVATE_METHODS, + ], + self::GROUP_SHORTCUT_FINAL_METHODS => [ + self::GROUP_PUBLIC_FINAL_METHODS, + self::GROUP_PROTECTED_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + ], + self::GROUP_SHORTCUT_ABSTRACT_METHODS => [ + self::GROUP_PUBLIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + ], + self::GROUP_SHORTCUT_STATIC_METHODS => [ + self::GROUP_STATIC_CONSTRUCTORS, + self::GROUP_PUBLIC_STATIC_FINAL_METHODS, + self::GROUP_PROTECTED_STATIC_FINAL_METHODS, + self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS, + self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS, + self::GROUP_PUBLIC_STATIC_METHODS, + self::GROUP_PROTECTED_STATIC_METHODS, + self::GROUP_PRIVATE_STATIC_METHODS, + ], + self::GROUP_SHORTCUT_METHODS => [ + self::GROUP_SHORTCUT_FINAL_METHODS, + self::GROUP_SHORTCUT_ABSTRACT_METHODS, + self::GROUP_SHORTCUT_STATIC_METHODS, + self::GROUP_CONSTRUCTOR, + self::GROUP_DESTRUCTOR, + self::GROUP_PUBLIC_METHODS, + self::GROUP_PROTECTED_METHODS, + self::GROUP_PRIVATE_METHODS, + self::GROUP_MAGIC_METHODS, + ], + ]; + + /** + * @param array $supportedGroups + * @return array + */ + public function unpackShortcut(string $shortcut, array $supportedGroups): array + { + $groups = []; + + foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) { + if (in_array($groupOrShortcut, $supportedGroups, true)) { + $groups[] = $groupOrShortcut; + assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } elseif ( + !array_key_exists($groupOrShortcut, self::SHORTCUTS) + ) { + // Nothing + assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } else { + $groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups)); + assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]); + } + } + + return $groups; + } + +} + diff --git a/tests/PHPStan/Analyser/data/bug-7621-2.php b/tests/PHPStan/Analyser/data/bug-7621-2.php new file mode 100644 index 00000000000..be70ec2cf50 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7621-2.php @@ -0,0 +1,23 @@ + ['foo', 'bar'] ]; + + public function foo(): void + { + assertType('array{\'foo\', \'bar\'}', self::FOO['foo']); + $keys = [0, 1, 2]; + foreach ($keys as $key) { + if (array_key_exists($key, self::FOO['foo'])) { + assertType('array{\'foo\', \'bar\'}', self::FOO['foo']); + } else { + assertType('array{0?: \'foo\', 1?: \'bar\'}&non-empty-array', self::FOO['foo']); + } + } + } +} diff --git a/tests/PHPStan/Analyser/data/bug-7621-3.php b/tests/PHPStan/Analyser/data/bug-7621-3.php new file mode 100644 index 00000000000..5016de05f3f --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-7621-3.php @@ -0,0 +1,20 @@ + ['foo', 'bar'] ]; + + + /** @param 'foo'|'bar' $key */ + public function foo(string $key): void + { + if (!array_key_exists($key, self::FOO)) { + assertType('array{}|array{foo: array{\'foo\', \'bar\'}}', self::FOO); + assertType('array{\'foo\', \'bar\'}', self::FOO['foo']); + } + } +}