diff --git a/UPGRADING.md b/UPGRADING.md index 56fa6b6d5e0..ad63118b236 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -64,7 +64,7 @@ - `Psalm\Type\Atomic\TTraitString` - `Psalm\Type\Atomic\TTrue` - `Psalm\Type\Atomic\TTypeAlias` - - `Psalm\Type\Atomic\TValueOfArray` + - `Psalm\Type\Atomic\TValueOf` - `Psalm\Type\Atomic\TVoid` - `Psalm\Type\Union` @@ -109,7 +109,7 @@ - `Psalm\Type\Atomic\TTemplateParam` - `Psalm\Type\Atomic\TTraitString` - `Psalm\Type\Atomic\TTypeAlias` - - `Psalm\Type\Atomic\TValueOfArray` + - `Psalm\Type\Atomic\TValueOf` - `Psalm\Type\Atomic\TVoid` - `Psalm\Type\Union` - While not a BC break per se, all classes / interfaces / traits / enums under @@ -156,7 +156,7 @@ - [BC] To remove a variable from the context, use `Context::remove()`. Calling `unset($context->vars_in_scope[$var_id])` can cause problems when using references. - [BC] `TKeyOfClassConstant` has been renamed to `TKeyOfArray`. -- [BC] `TValueOfClassConstant` has been renamed to `TValueOfArray`. +- [BC] `TValueOfClassConstant` has been renamed to `TValueOf`. - [BC] `TKeyOfTemplate` base class has been changed from `Scalar` to `Atomic`. - [BC] Class `Psalm\FileManipulation` became final - [BC] Class `Psalm\Context` became final @@ -742,7 +742,7 @@ - [BC] Class `Psalm\Type\Atomic\TLiteralInt` became final - [BC] Class `Psalm\Type\Atomic\TTrue` became final - [BC] Class `Psalm\Type\Atomic\TDependentGetClass` became final - - [BC] Class `Psalm\Type\Atomic\TValueOfArray` became final + - [BC] Class `Psalm\Type\Atomic\TValueOf` became final - [BC] Class `Psalm\Type\Atomic\TGenericObject` became final - [BC] Class `Psalm\Type\Atomic\TNonEmptyLowercaseString` became final - [BC] Class `Psalm\Type\Atomic\TEnumCase` became final diff --git a/docs/running_psalm/plugins/plugins_type_system.md b/docs/running_psalm/plugins/plugins_type_system.md index 777c4018ad5..ff185ee198e 100644 --- a/docs/running_psalm/plugins/plugins_type_system.md +++ b/docs/running_psalm/plugins/plugins_type_system.md @@ -51,13 +51,13 @@ The classes are as follows: `TKeyOfArray` - Represents an offset of an array (e.g. `key-of`). -`TValueOfArray` - Represents a value of an array (e.g. `value-of`). +`TValueOf` - Represents a value of an array or enum (e.g. `value-of`). `TTemplateIndexedAccess` - To be documented `TTemplateKeyOf` - Represents the type used when using TKeyOfArray when the type of the array is a template -`TTemplateValueOf` - Represents the type used when using TValueOfArray when the type of the array is a template +`TTemplateValueOf` - Represents the type used when using TValueOf when the type of the array or enum is a template `TPropertiesOf` - Represents properties and their types of a class as a keyed array (e.g. `properties-of`) diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index f2eed358f4b..d009b9fd5d8 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -36,7 +36,7 @@ use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateValueOf; -use Psalm\Type\Atomic\TValueOfArray; +use Psalm\Type\Atomic\TValueOf; use function array_merge; use function array_values; @@ -375,7 +375,7 @@ public static function isContainedBy( } if ($input_type_part instanceof TTemplateValueOf) { - $array_value_type = TValueOfArray::getArrayValueType($input_type_part->as); + $array_value_type = TValueOf::getValueType($input_type_part->as, $codebase); if ($array_value_type === null) { return false; } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 000dbf49b4c..4a855429faf 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -26,7 +26,7 @@ use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Atomic\TTemplatePropertiesOf; use Psalm\Type\Atomic\TTemplateValueOf; -use Psalm\Type\Atomic\TValueOfArray; +use Psalm\Type\Atomic\TValueOf; use Psalm\Type\Union; use UnexpectedValueException; @@ -351,9 +351,9 @@ private static function replaceTemplateKeyOfValueOf( } if ($atomic_type instanceof TTemplateValueOf - && TValueOfArray::isViableTemplateType($template_type) + && TValueOf::isViableTemplateType($template_type) ) { - return new TValueOfArray(clone $template_type); + return new TValueOf(clone $template_type); } return null; diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index effe35c3458..c849fc1ff2f 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -34,7 +34,7 @@ use Psalm\Type\Atomic\TPropertiesOf; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTypeAlias; -use Psalm\Type\Atomic\TValueOfArray; +use Psalm\Type\Atomic\TValueOf; use Psalm\Type\Atomic\TVoid; use Psalm\Type\Union; use ReflectionProperty; @@ -364,9 +364,9 @@ public static function expandAtomic( } if ($return_type instanceof TKeyOfArray - || $return_type instanceof TValueOfArray + || $return_type instanceof TValueOf ) { - return self::expandKeyOfValueOfArray( + return self::expandKeyOfValueOf( $codebase, $return_type, $self_class, @@ -965,11 +965,11 @@ function (?Union $property_type): bool { } /** - * @param TKeyOfArray|TValueOfArray $return_type + * @param TKeyOfArray|TValueOf $return_type * @param string|TNamedObject|TTemplateParam|null $static_class_type * @return non-empty-list */ - private static function expandKeyOfValueOfArray( + private static function expandKeyOfValueOf( Codebase $codebase, Atomic &$return_type, ?string $self_class, @@ -1029,8 +1029,8 @@ private static function expandKeyOfValueOfArray( && !TKeyOfArray::isViableTemplateType($constant_type) ) || ( - $return_type instanceof TValueOfArray - && !TValueOfArray::isViableTemplateType($constant_type) + $return_type instanceof TValueOf + && !TValueOf::isViableTemplateType($constant_type) ) ) { if ($throw_on_unresolvable_constant) { @@ -1052,7 +1052,7 @@ private static function expandKeyOfValueOfArray( if ($return_type instanceof TKeyOfArray) { $new_return_types = TKeyOfArray::getArrayKeyType(new Union($type_atomics)); } else { - $new_return_types = TValueOfArray::getArrayValueType(new Union($type_atomics)); + $new_return_types = TValueOf::getValueType(new Union($type_atomics), $codebase); } if ($new_return_types === null) { return [$return_type]; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 7b1fe60a8a6..00b61b9c31a 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -63,7 +63,7 @@ use Psalm\Type\Atomic\TTemplatePropertiesOf; use Psalm\Type\Atomic\TTemplateValueOf; use Psalm\Type\Atomic\TTypeAlias; -use Psalm\Type\Atomic\TValueOfArray; +use Psalm\Type\Atomic\TValueOf; use Psalm\Type\TypeNode; use Psalm\Type\Union; @@ -765,13 +765,13 @@ private static function getTypeFromGenericTree( ); } - if (!TValueOfArray::isViableTemplateType($generic_params[0])) { + if (!TValueOf::isViableTemplateType($generic_params[0])) { throw new TypeParseTreeException( 'Untemplated value-of param ' . $param_name . ' should be an array' ); } - return new TValueOfArray($generic_params[0]); + return new TValueOf($generic_params[0]); } if ($generic_type_value === 'int-mask') { @@ -842,7 +842,7 @@ private static function getTypeFromGenericTree( $param_type = $param_union_types[0]; if (!$param_type instanceof TClassConstant - && !$param_type instanceof TValueOfArray + && !$param_type instanceof TValueOf && !$param_type instanceof TKeyOfArray ) { throw new TypeParseTreeException( diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index 056cab2bcea..c7c2ca058ad 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -11,11 +11,11 @@ */ final class TIntMaskOf extends TInt { - /** @var TClassConstant|TKeyOfArray|TValueOfArray */ + /** @var TClassConstant|TKeyOfArray|TValueOf */ public $value; /** - * @param TClassConstant|TKeyOfArray|TValueOfArray $value + * @param TClassConstant|TKeyOfArray|TValueOf $value */ public function __construct(Atomic $value) { diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 68f1dacbce3..d00cb597484 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -9,7 +9,7 @@ use Psalm\Type\Union; /** - * Represents the type used when using TValueOfArray when the type of the array is a template + * Represents the type used when using TValueOf when the type of the array or enum is a template */ final class TTemplateValueOf extends Atomic { diff --git a/src/Psalm/Type/Atomic/TValueOfArray.php b/src/Psalm/Type/Atomic/TValueOf.php similarity index 53% rename from src/Psalm/Type/Atomic/TValueOfArray.php rename to src/Psalm/Type/Atomic/TValueOf.php index 59457115770..e2f925b9f94 100644 --- a/src/Psalm/Type/Atomic/TValueOfArray.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -2,16 +2,21 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Codebase\ConstantTypeResolver; +use Psalm\Storage\EnumCaseStorage; use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; +use function array_map; use function array_values; +use function assert; +use function count; /** - * Represents a value of an array. + * Represents a value of an array or enum. */ -final class TValueOfArray extends Atomic +final class TValueOf extends Atomic { /** @var Union */ public $type; @@ -56,6 +61,7 @@ public static function isViableTemplateType(Union $template_type): bool && !$type instanceof TKeyedArray && !$type instanceof TList && !$type instanceof TPropertiesOf + && !$type instanceof TNamedObject ) { return false; } @@ -63,39 +69,58 @@ public static function isViableTemplateType(Union $template_type): bool return true; } - public static function getArrayValueType( + public static function getValueType( Union $type, - bool $keep_template_params = false + Codebase $codebase, + bool $keep_template_params = false, ): ?Union { $value_types = []; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof TArray) { - $array_value_atomics = $atomic_type->type_params[1]; + $value_atomics = $atomic_type->type_params[1]; } elseif ($atomic_type instanceof TList) { - $array_value_atomics = $atomic_type->type_param; + $value_atomics = $atomic_type->type_param; } elseif ($atomic_type instanceof TKeyedArray) { - $array_value_atomics = $atomic_type->getGenericValueType(); + $value_atomics = $atomic_type->getGenericValueType(); } elseif ($atomic_type instanceof TTemplateParam) { if ($keep_template_params) { - $array_value_atomics = new Union([$atomic_type]); + $value_atomics = new Union([$atomic_type]); } else { - $array_value_atomics = static::getArrayValueType( + $value_atomics = static::getValueType( $atomic_type->as, + $codebase, $keep_template_params ); - if ($array_value_atomics === null) { + if ($value_atomics === null) { continue; } } + } elseif ($atomic_type instanceof TNamedObject + && $codebase->classlike_storage_provider->has($atomic_type->value) + ) { + $class_storage = $codebase->classlike_storage_provider->get($atomic_type->value); + $cases = $class_storage->enum_cases; + if (!$class_storage->is_enum + || $class_storage->enum_type === null + || count($cases) === 0 + ) { + // Invalid value-of, skip + continue; + } + + $value_atomics = new Union(array_map( + function (EnumCaseStorage $case): Atomic { + assert($case->value !== null); // Backed enum must have a value + return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); + }, + array_values($cases), + )); } else { continue; } - $value_types = array_merge( - $value_types, - array_values($array_value_atomics->getAtomicTypes()) - ); + $value_types = [...$value_types, ...array_values($value_atomics->getAtomicTypes())]; } if ($value_types === []) { diff --git a/tests/ValueOfArrayTest.php b/tests/ValueOfTest.php similarity index 70% rename from tests/ValueOfArrayTest.php rename to tests/ValueOfTest.php index 135996e75a1..a117c1c8503 100644 --- a/tests/ValueOfArrayTest.php +++ b/tests/ValueOfTest.php @@ -7,7 +7,7 @@ use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; -class ValueOfArrayTest extends TestCase +class ValueOfTest extends TestCase { use InvalidCodeAnalysisTestTrait; use ValidCodeAnalysisTestTrait; @@ -141,6 +141,74 @@ function returnPropertyOfA() { } ', ], + 'valueOfStringEnum' => [ + 'code' => ' $arg */ + function foobar(string $arg): void + { + /** @psalm-check-type-exact $arg = "foo"|"bar" */; + } + + /** @var Foo */ + $foo = Foo::Foo; + foobar($foo->value); + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'valueOfIntEnum' => [ + 'code' => ' $arg */ + function foobar(int $arg): void + { + /** @psalm-check-type-exact $arg = 2|3 */; + } + + /** @var Foo */ + $foo = Foo::Foo; + foobar($foo->value); + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'valueOfEnumUnion' => [ + 'code' => ' $arg */ + function foobar(int|string $arg): void + { + /** @psalm-check-type-exact $arg = 2|3|"foo"|"bar" */; + } + ', + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -212,6 +280,23 @@ function getValue() { ', 'error_message' => 'InvalidReturnStatement' ], + 'valueOfUnitEnum' => [ + 'code' => ' $arg */ + function foobar(string $arg): void {} + ', + // TODO turn this into an InvalidDocblock with a better error message. This is difficult because it + // has to happen after scanning has finished, otherwise the class might not have been scanned yet. + 'error_message' => 'MismatchingDocblockParamType', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } }