diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index bbd5ab3457c..8c77bba5218 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12987,7 +12987,7 @@ 'strpbrk' => ['string|false', 'string'=>'string', 'characters'=>'string'], 'strpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strptime' => ['array|false', 'timestamp'=>'string', 'format'=>'string'], -'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], +'strrchr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], 'strrev' => ['string', 'string'=>'string'], 'strripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 75266c16cf0..8a4a76077b8 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -113,6 +113,10 @@ 'old' => ['?bool', 'text'=>'string'], 'new' => ['bool', 'text'=>'string'], ], + 'strrchr' => [ + 'old' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], + 'new' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], + ], ], 'removed' => [ diff --git a/docs/annotating_code/type_syntax/array_types.md b/docs/annotating_code/type_syntax/array_types.md index 2846a9a220d..1021ab1fcab 100644 --- a/docs/annotating_code/type_syntax/array_types.md +++ b/docs/annotating_code/type_syntax/array_types.md @@ -14,7 +14,7 @@ $a = [1, 2, 3, 4, 5]; ```php 'hello', 5 => 'goodbye']; -$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC'] +$b = ['a' => 'AA', 'b' => 'BB', 'c' => 'CC']; ``` Makeshift [Structs](https://en.wikipedia.org/wiki/Struct_(C_programming_language)): diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 6466d91994e..c79bf10c005 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -4,7 +4,6 @@ use Psalm\Codebase; use Psalm\Internal\MethodIdentifier; -use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; @@ -18,11 +17,9 @@ use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TEnumCase; use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyOf; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; @@ -45,7 +42,6 @@ use function assert; use function count; use function get_class; -use function is_int; use function strtolower; /** @@ -630,40 +626,6 @@ public static function isContainedBy( } } - if ($input_type_part instanceof TEnumCase - && $codebase->classlike_storage_provider->has($input_type_part->value) - ) { - if ($container_type_part instanceof TString || $container_type_part instanceof TInt) { - $input_type_classlike_storage = $codebase->classlike_storage_provider->get($input_type_part->value); - if ($input_type_classlike_storage->enum_type === null - || !isset($input_type_classlike_storage->enum_cases[$input_type_part->case_name]) - ) { - // Not a backed enum or non-existent enum case - return false; - } - - $input_type_enum_case_storage = $input_type_classlike_storage->enum_cases[$input_type_part->case_name]; - assert( - $input_type_enum_case_storage->value !== null, - 'Backed enums cannot have values without a value.', - ); - - if (is_int($input_type_enum_case_storage->value)) { - return self::isContainedBy( - $codebase, - new TLiteralInt($input_type_enum_case_storage->value), - $container_type_part, - ); - } - - return self::isContainedBy( - $codebase, - Type::getAtomicStringFromLiteral($input_type_enum_case_storage->value), - $container_type_part, - ); - } - } - if ($container_type_part instanceof TString || $container_type_part instanceof TScalar) { if ($input_type_part instanceof TNamedObject) { // check whether the object has a __toString method diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index b547e8e9bf7..13c4da53919 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -82,6 +82,7 @@ use function count; use function explode; use function get_class; +use function in_array; use function is_int; use function min; use function strlen; @@ -530,7 +531,11 @@ public static function reconcile( } if ($assertion_type instanceof TValueOf) { - return $assertion_type->type; + return self::reconcileValueOf( + $codebase, + $assertion_type, + $failed_reconciliation, + ); } return null; @@ -2931,6 +2936,71 @@ private static function reconcileClassConstant( return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase); } + /** + * @param Reconciler::RECONCILIATION_* $failed_reconciliation + */ + private static function reconcileValueOf( + Codebase $codebase, + TValueOf $assertion_type, + int &$failed_reconciliation + ): ?Union { + $reconciled_types = []; + + // For now, only enums are supported here + foreach ($assertion_type->type->getAtomicTypes() as $atomic_type) { + $enum_case_to_assert = null; + if ($atomic_type instanceof TClassConstant) { + $class_name = $atomic_type->fq_classlike_name; + $enum_case_to_assert = $atomic_type->const_name; + } elseif ($atomic_type instanceof TNamedObject) { + $class_name = $atomic_type->value; + } else { + return null; + } + + if (!$codebase->classOrInterfaceOrEnumExists($class_name)) { + return null; + } + + $class_storage = $codebase->classlike_storage_provider->get($class_name); + if (!$class_storage->is_enum) { + return null; + } + + if (!in_array($class_storage->enum_type, ['string', 'int'], true)) { + return null; + } + + // For value-of, the assertion is meant to return *ANY* value of *ANY* enum case + if ($enum_case_to_assert === null) { + foreach ($class_storage->enum_cases as $enum_case) { + assert( + $enum_case->value !== null, + 'Verified enum type above, value can not contain `null` anymore.', + ); + $reconciled_types[] = Type::getLiteral($enum_case->value); + } + + continue; + } + + $enum_case = $class_storage->enum_cases[$atomic_type->const_name] ?? null; + if ($enum_case === null) { + return null; + } + + assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.'); + $reconciled_types[] = Type::getLiteral($enum_case->value); + } + + if ($reconciled_types === []) { + $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; + return Type::getNever(); + } + + return TypeCombiner::combine($reconciled_types, $codebase, false, false); + } + /** * @psalm-assert-if-true TCallableObject|TObjectWithProperties|TNamedObject $type */ diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index fbc3e7e783c..6903c94094a 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -58,6 +58,7 @@ use function explode; use function get_class; use function implode; +use function is_int; use function preg_quote; use function preg_replace; use function stripos; @@ -258,6 +259,19 @@ public static function getNumericString(): Union return new Union([$type]); } + /** + * @param int|string $value + * @return TLiteralString|TLiteralInt + */ + public static function getLiteral($value): Atomic + { + if (is_int($value)) { + return new TLiteralInt($value); + } + + return TLiteralString::make($value); + } + public static function getString(?string $value = null): Union { return new Union([$value === null ? new TString() : self::getAtomicStringFromLiteral($value)]); diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index fba322b7d98..16cdf6d5244 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -2194,6 +2194,10 @@ function assertSomeString(string $foo): void function assertSomeInt(int $foo): void {} + /** @psalm-assert value-of $foo */ + function assertAnyEnumValue(string|int $foo): void + {} + /** @param "foo"|"bar" $foo */ function takesSomeStringFromEnum(string $foo): StringEnum { @@ -2216,8 +2220,14 @@ function takesSomeIntFromEnum(int $foo): IntEnum assertSomeInt($int); takesSomeIntFromEnum($int); + + /** @var string|int $potentialEnumValue */ + $potentialEnumValue = null; + assertAnyEnumValue($potentialEnumValue); ', - 'assertions' => [], + 'assertions' => [ + '$potentialEnumValue===' => "'bar'|'baz'|'foo'|1|2|3", + ], 'ignored_issues' => [], 'php_version' => '8.1', ], diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 1ac88b1c233..77df3b8d659 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -1015,6 +1015,21 @@ function withA(WithState $_): void {} 'ignored_issues' => [], 'php_version' => '8.1', ], + 'backedEnumDoesNotPassNativeType' => [ + 'code' => ' 'InvalidArgument', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }