Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow value-of to work with backed enums (fixes #7874). #8283

Merged
merged 3 commits into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions UPGRADING.md
Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/running_psalm/plugins/plugins_type_system.md
Expand Up @@ -51,13 +51,13 @@ The classes are as follows:

`TKeyOfArray` - Represents an offset of an array (e.g. `key-of<MyClass::CLASS_CONSTANT>`).

`TValueOfArray` - Represents a value of an array (e.g. `value-of<MyClass::CLASS_CONSTANT>`).
`TValueOf` - Represents a value of an array or enum (e.g. `value-of<MyClass::CLASS_CONSTANT>`).

`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<MyClass>`)

Expand Down
4 changes: 2 additions & 2 deletions src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
16 changes: 8 additions & 8 deletions src/Psalm/Internal/Type/TypeExpander.php
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Atomic>
*/
private static function expandKeyOfValueOfArray(
private static function expandKeyOfValueOf(
Codebase $codebase,
Atomic &$return_type,
?string $self_class,
Expand Down Expand Up @@ -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) {
Expand All @@ -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];
Expand Down
8 changes: 4 additions & 4 deletions src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/Psalm/Type/Atomic/TIntMaskOf.php
Expand Up @@ -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)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Psalm/Type/Atomic/TTemplateValueOf.php
Expand Up @@ -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
{
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -56,46 +61,66 @@ public static function isViableTemplateType(Union $template_type): bool
&& !$type instanceof TKeyedArray
&& !$type instanceof TList
&& !$type instanceof TPropertiesOf
&& !$type instanceof TNamedObject
) {
return false;
}
}
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 === []) {
Expand Down