mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Merge pull request #9586 from boesing/feature/value-of-enum-assertions
Introduce `value-of` with backed enum cases in assertions
This commit is contained in:
commit
87d0854a97
@ -9,25 +9,23 @@ use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ClassConstantByWildcardResolver
|
||||
{
|
||||
private StorageByPatternResolver $resolver;
|
||||
private Codebase $codebase;
|
||||
|
||||
public function __construct(Codebase $codebase)
|
||||
{
|
||||
$this->resolver = new StorageByPatternResolver();
|
||||
$this->codebase = $codebase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Atomic>|null
|
||||
* @return non-empty-array<array-key,Atomic>|null
|
||||
*/
|
||||
public function resolve(string $class_name, string $constant_pattern): ?array
|
||||
{
|
||||
@ -35,24 +33,27 @@ final class ClassConstantByWildcardResolver
|
||||
return null;
|
||||
}
|
||||
|
||||
$constant_regex_pattern = sprintf('#^%s$#', str_replace('*', '.*', $constant_pattern));
|
||||
$classlike_storage = $this->codebase->classlike_storage_provider->get($class_name);
|
||||
|
||||
$class_like_storage = $this->codebase->classlike_storage_provider->get($class_name);
|
||||
$matched_class_constant_types = [];
|
||||
|
||||
foreach ($class_like_storage->constants as $constant => $class_constant_storage) {
|
||||
if (preg_match($constant_regex_pattern, $constant) === 0) {
|
||||
continue;
|
||||
}
|
||||
$constants = $this->resolver->resolveConstants(
|
||||
$classlike_storage,
|
||||
$constant_pattern,
|
||||
);
|
||||
|
||||
$types = [];
|
||||
foreach ($constants as $class_constant_storage) {
|
||||
if (! $class_constant_storage->type) {
|
||||
$matched_class_constant_types[] = [new TMixed()];
|
||||
$types[] = [new TMixed()];
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_class_constant_types[] = $class_constant_storage->type->getAtomicTypes();
|
||||
$types[] = $class_constant_storage->type->getAtomicTypes();
|
||||
}
|
||||
|
||||
return array_values(array_merge([], ...$matched_class_constant_types));
|
||||
if ($types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_merge([], ...$types);
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ use ReflectionProperty;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function count;
|
||||
@ -1603,8 +1604,7 @@ class ClassLikes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE
|
||||
* $visibility
|
||||
* @param ReflectionProperty::IS_PUBLIC|ReflectionProperty::IS_PROTECTED|ReflectionProperty::IS_PRIVATE $visibility
|
||||
*/
|
||||
public function getClassConstantType(
|
||||
string $class_name,
|
||||
@ -1612,7 +1612,8 @@ class ClassLikes
|
||||
int $visibility,
|
||||
?StatementsAnalyzer $statements_analyzer = null,
|
||||
array $visited_constant_ids = [],
|
||||
bool $late_static_binding = false
|
||||
bool $late_static_binding = false,
|
||||
bool $in_value_of_context = false
|
||||
): ?Union {
|
||||
$class_name = strtolower($class_name);
|
||||
|
||||
@ -1622,41 +1623,42 @@ class ClassLikes
|
||||
|
||||
$storage = $this->classlike_storage_provider->get($class_name);
|
||||
|
||||
if (isset($storage->constants[$constant_name])) {
|
||||
$constant_storage = $storage->constants[$constant_name];
|
||||
$enum_types = null;
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
) {
|
||||
return null;
|
||||
if ($storage->is_enum) {
|
||||
$enum_types = $this->getEnumType(
|
||||
$storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
if ($in_value_of_context) {
|
||||
return $enum_types;
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PROTECTED
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($constant_storage->unresolved_node) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
|
||||
$this,
|
||||
$constant_storage->unresolved_node,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
)]);
|
||||
if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->type = $constant_storage->inferred_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $late_static_binding ? $constant_storage->type : ($constant_storage->inferred_type ?? null);
|
||||
} elseif (isset($storage->enum_cases[$constant_name])) {
|
||||
return new Union([new TEnumCase($storage->name, $constant_name)]);
|
||||
}
|
||||
return null;
|
||||
|
||||
$constant_types = $this->getConstantType(
|
||||
$storage,
|
||||
$constant_name,
|
||||
$visibility,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
$late_static_binding,
|
||||
);
|
||||
|
||||
$types = [];
|
||||
if ($enum_types !== null) {
|
||||
$types = array_merge($types, $enum_types->getAtomicTypes());
|
||||
}
|
||||
|
||||
if ($constant_types !== null) {
|
||||
$types = array_merge($types, $constant_types->getAtomicTypes());
|
||||
}
|
||||
|
||||
if ($types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Union($types);
|
||||
}
|
||||
|
||||
private function checkMethodReferences(ClassLikeStorage $classlike_storage, Methods $methods): void
|
||||
@ -2366,4 +2368,113 @@ class ClassLikes
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function getConstantType(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $constant_name,
|
||||
int $visibility,
|
||||
?StatementsAnalyzer $statements_analyzer,
|
||||
array $visited_constant_ids,
|
||||
bool $late_static_binding
|
||||
): ?Union {
|
||||
$constant_resolver = new StorageByPatternResolver();
|
||||
$resolved_constants = $constant_resolver->resolveConstants(
|
||||
$class_like_storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
$filtered_constants_by_visibility = array_filter(
|
||||
$resolved_constants,
|
||||
fn(ClassConstantStorage $resolved_constant) => $this->filterConstantNameByVisibility(
|
||||
$resolved_constant,
|
||||
$visibility,
|
||||
)
|
||||
);
|
||||
|
||||
if ($filtered_constants_by_visibility === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$new_atomic_types = [];
|
||||
|
||||
foreach ($filtered_constants_by_visibility as $filtered_constant_name => $constant_storage) {
|
||||
if (!isset($class_like_storage->constants[$filtered_constant_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($constant_storage->unresolved_node) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->inferred_type = new Union([ConstantTypeResolver::resolve(
|
||||
$this,
|
||||
$constant_storage->unresolved_node,
|
||||
$statements_analyzer,
|
||||
$visited_constant_ids,
|
||||
)]);
|
||||
|
||||
if ($constant_storage->type === null || !$constant_storage->type->from_docblock) {
|
||||
/** @psalm-suppress InaccessibleProperty Lazy resolution */
|
||||
$constant_storage->type = $constant_storage->inferred_type;
|
||||
}
|
||||
}
|
||||
|
||||
$constant_type = $late_static_binding
|
||||
? $constant_storage->type
|
||||
: ($constant_storage->inferred_type ?? null);
|
||||
|
||||
if ($constant_type === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_atomic_types[] = $constant_type->getAtomicTypes();
|
||||
}
|
||||
|
||||
if ($new_atomic_types === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Union(array_merge([], ...$new_atomic_types));
|
||||
}
|
||||
|
||||
private function getEnumType(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $constant_name
|
||||
): ?Union {
|
||||
$constant_resolver = new StorageByPatternResolver();
|
||||
$resolved_enums = $constant_resolver->resolveEnums(
|
||||
$class_like_storage,
|
||||
$constant_name,
|
||||
);
|
||||
|
||||
if ($resolved_enums === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$types = [];
|
||||
foreach (array_keys($resolved_enums) as $enum_case_name) {
|
||||
$types[$enum_case_name] = new TEnumCase($class_like_storage->name, $enum_case_name);
|
||||
}
|
||||
|
||||
return new Union($types);
|
||||
}
|
||||
|
||||
private function filterConstantNameByVisibility(
|
||||
ClassConstantStorage $constant_storage,
|
||||
int $visibility
|
||||
): bool {
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PROTECTED
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
&& $constant_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PROTECTED
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
87
src/Psalm/Internal/Codebase/StorageByPatternResolver.php
Normal file
87
src/Psalm/Internal/Codebase/StorageByPatternResolver.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Psalm\Internal\Codebase;
|
||||
|
||||
use Psalm\Storage\ClassConstantStorage;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\EnumCaseStorage;
|
||||
|
||||
use function preg_match;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class StorageByPatternResolver
|
||||
{
|
||||
public const RESOLVE_CONSTANTS = 1;
|
||||
public const RESOLVE_ENUMS = 2;
|
||||
|
||||
/**
|
||||
* @return array<string,ClassConstantStorage>
|
||||
*/
|
||||
public function resolveConstants(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $pattern
|
||||
): array {
|
||||
$constants = $class_like_storage->constants;
|
||||
|
||||
if (strpos($pattern, '*') === false) {
|
||||
if (isset($constants[$pattern])) {
|
||||
return [$pattern => $constants[$pattern]];
|
||||
}
|
||||
|
||||
return [];
|
||||
} elseif ($pattern === '*') {
|
||||
return $constants;
|
||||
}
|
||||
|
||||
$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
|
||||
$matched_constants = [];
|
||||
|
||||
foreach ($constants as $constant => $class_constant_storage) {
|
||||
if (preg_match($regex_pattern, $constant) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_constants[$constant] = $class_constant_storage;
|
||||
}
|
||||
|
||||
return $matched_constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,EnumCaseStorage>
|
||||
*/
|
||||
public function resolveEnums(
|
||||
ClassLikeStorage $class_like_storage,
|
||||
string $pattern
|
||||
): array {
|
||||
$enum_cases = $class_like_storage->enum_cases;
|
||||
if (strpos($pattern, '*') === false) {
|
||||
if (isset($enum_cases[$pattern])) {
|
||||
return [$pattern => $enum_cases[$pattern]];
|
||||
}
|
||||
|
||||
return [];
|
||||
} elseif ($pattern === '*') {
|
||||
return $enum_cases;
|
||||
}
|
||||
|
||||
$regex_pattern = sprintf('#^%s$#', str_replace('*', '.*?', $pattern));
|
||||
$matched_enums = [];
|
||||
foreach ($enum_cases as $enum_case_name => $enum_case_storage) {
|
||||
if (preg_match($regex_pattern, $enum_case_name) === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matched_enums[$enum_case_name] = $enum_case_storage;
|
||||
}
|
||||
|
||||
return $matched_enums;
|
||||
}
|
||||
}
|
@ -425,7 +425,9 @@ class ClassLikeNodeScanner
|
||||
try {
|
||||
$type_string = CommentAnalyzer::splitDocLine($type_string)[0];
|
||||
} catch (DocblockParseException $e) {
|
||||
throw new DocblockParseException($type_string . ' is not a valid type: '.$e->getMessage());
|
||||
throw new DocblockParseException(
|
||||
$type_string . ' is not a valid type: ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
$type_string = CommentAnalyzer::sanitizeDocblockType($type_string);
|
||||
try {
|
||||
|
@ -4,6 +4,7 @@ namespace Psalm\Internal\Type\Comparator;
|
||||
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\Scalar;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
@ -17,10 +18,12 @@ use Psalm\Type\Atomic\TConditional;
|
||||
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\TList;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
@ -36,12 +39,14 @@ use Psalm\Type\Atomic\TTemplateKeyOf;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTemplateValueOf;
|
||||
use Psalm\Type\Atomic\TValueOf;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function get_class;
|
||||
use function is_int;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
@ -82,6 +87,32 @@ class AtomicTypeComparator
|
||||
);
|
||||
}
|
||||
|
||||
if ($input_type_part instanceof TValueOf) {
|
||||
if ($container_type_part instanceof TValueOf) {
|
||||
return UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_type_part->type,
|
||||
$container_type_part->type,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
} elseif ($container_type_part instanceof Scalar) {
|
||||
return UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_type_part->type,
|
||||
new Union([$container_type_part]),
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TMixed
|
||||
|| ($container_type_part instanceof TTemplateParam
|
||||
&& $container_type_part->as->isMixed()
|
||||
@ -302,7 +333,7 @@ class AtomicTypeComparator
|
||||
$atomic_comparison_result->type_coerced = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($container_type_part instanceof TEnumCase
|
||||
@ -604,6 +635,40 @@ class AtomicTypeComparator
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -71,11 +71,13 @@ use Psalm\Type\Atomic\TScalar;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTrue;
|
||||
use Psalm\Type\Atomic\TValueOf;
|
||||
use Psalm\Type\Reconciler;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
@ -527,6 +529,10 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
}
|
||||
}
|
||||
|
||||
if ($assertion_type instanceof TValueOf) {
|
||||
return $assertion_type->type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -2894,19 +2900,23 @@ class SimpleAssertionReconciler extends Reconciler
|
||||
int &$failed_reconciliation
|
||||
): Union {
|
||||
$class_name = $class_constant_expression->fq_classlike_name;
|
||||
$constant_pattern = $class_constant_expression->const_name;
|
||||
|
||||
$resolver = new ClassConstantByWildcardResolver($codebase);
|
||||
$matched_class_constant_types = $resolver->resolve($class_name, $constant_pattern);
|
||||
if ($matched_class_constant_types === null) {
|
||||
if (!$codebase->classlike_storage_provider->has($class_name)) {
|
||||
return $existing_type;
|
||||
}
|
||||
|
||||
if ($matched_class_constant_types === []) {
|
||||
$constant_pattern = $class_constant_expression->const_name;
|
||||
|
||||
$resolver = new ClassConstantByWildcardResolver($codebase);
|
||||
$matched_class_constant_types = $resolver->resolve(
|
||||
$class_name,
|
||||
$constant_pattern,
|
||||
);
|
||||
|
||||
if ($matched_class_constant_types === null) {
|
||||
$failed_reconciliation = Reconciler::RECONCILIATION_EMPTY;
|
||||
return Type::getNever();
|
||||
}
|
||||
|
||||
return TypeCombiner::combine($matched_class_constant_types, $codebase);
|
||||
return TypeCombiner::combine(array_values($matched_class_constant_types), $codebase);
|
||||
}
|
||||
}
|
||||
|
@ -6,9 +6,6 @@ use Psalm\Codebase;
|
||||
use Psalm\Exception\CircularReferenceException;
|
||||
use Psalm\Exception\UnresolvableConstantException;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer;
|
||||
use Psalm\Internal\Type\SimpleAssertionReconciler;
|
||||
use Psalm\Internal\Type\SimpleNegatedAssertionReconciler;
|
||||
use Psalm\Internal\Type\TypeParser;
|
||||
use Psalm\Storage\Assertion\IsType;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
@ -18,6 +15,7 @@ use Psalm\Type\Atomic\TClassConstant;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TConditional;
|
||||
use Psalm\Type\Atomic\TEnumCase;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TIntMask;
|
||||
@ -41,7 +39,6 @@ use Psalm\Type\Union;
|
||||
use ReflectionProperty;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
@ -49,9 +46,7 @@ use function count;
|
||||
use function get_class;
|
||||
use function is_string;
|
||||
use function reset;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -136,6 +131,10 @@ class TypeExpander
|
||||
bool $expand_templates = false,
|
||||
bool $throw_on_unresolvable_constant = false
|
||||
): array {
|
||||
if ($return_type instanceof TEnumCase) {
|
||||
return [$return_type];
|
||||
}
|
||||
|
||||
if ($return_type instanceof TNamedObject
|
||||
|| $return_type instanceof TTemplateParam
|
||||
) {
|
||||
@ -250,52 +249,18 @@ class TypeExpander
|
||||
return [new TLiteralClassString($return_type->fq_classlike_name)];
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name);
|
||||
|
||||
if (strpos($return_type->const_name, '*') !== false) {
|
||||
$matching_constants = [
|
||||
...array_keys($class_storage->constants),
|
||||
...array_keys($class_storage->enum_cases),
|
||||
];
|
||||
|
||||
$const_name_part = substr($return_type->const_name, 0, -1);
|
||||
|
||||
if ($const_name_part) {
|
||||
$matching_constants = array_filter(
|
||||
$matching_constants,
|
||||
static fn($constant_name): bool => $constant_name !== $const_name_part
|
||||
&& strpos($constant_name, $const_name_part) === 0
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$matching_constants = [$return_type->const_name];
|
||||
try {
|
||||
$class_constant = $codebase->classlikes->getClassConstantType(
|
||||
$return_type->fq_classlike_name,
|
||||
$return_type->const_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
$class_constant = null;
|
||||
}
|
||||
|
||||
$matching_constant_types = [];
|
||||
|
||||
foreach ($matching_constants as $matching_constant) {
|
||||
try {
|
||||
$class_constant = $codebase->classlikes->getClassConstantType(
|
||||
$return_type->fq_classlike_name,
|
||||
$matching_constant,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
$class_constant = null;
|
||||
}
|
||||
|
||||
if ($class_constant) {
|
||||
if ($class_constant->isSingle()) {
|
||||
$matching_constant_types = array_merge(
|
||||
array_values($class_constant->getAtomicTypes()),
|
||||
$matching_constant_types,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matching_constant_types) {
|
||||
return $matching_constant_types;
|
||||
if ($class_constant) {
|
||||
return array_values($class_constant->getAtomicTypes());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1092,7 +1057,7 @@ class TypeExpander
|
||||
}
|
||||
|
||||
if ($throw_on_unresolvable_constant
|
||||
&& !$codebase->classOrInterfaceExists($type_param->fq_classlike_name)
|
||||
&& !$codebase->classOrInterfaceOrEnumExists($type_param->fq_classlike_name)
|
||||
) {
|
||||
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
|
||||
}
|
||||
@ -1102,6 +1067,10 @@ class TypeExpander
|
||||
$type_param->fq_classlike_name,
|
||||
$type_param->const_name,
|
||||
ReflectionProperty::IS_PRIVATE,
|
||||
null,
|
||||
[],
|
||||
false,
|
||||
$return_type instanceof TValueOf,
|
||||
);
|
||||
} catch (CircularReferenceException $e) {
|
||||
return [$return_type];
|
||||
@ -1138,9 +1107,11 @@ class TypeExpander
|
||||
} else {
|
||||
$new_return_types = TValueOf::getValueType(new Union($type_atomics), $codebase);
|
||||
}
|
||||
|
||||
if ($new_return_types === null) {
|
||||
return [$return_type];
|
||||
}
|
||||
|
||||
return array_values($new_return_types->getAtomicTypes());
|
||||
}
|
||||
}
|
||||
|
@ -779,7 +779,7 @@ class TypeParser
|
||||
if ($template_param->getIntersectionTypes()) {
|
||||
throw new TypeParseTreeException(
|
||||
$generic_type_value . '<' . $param_name . '> must be a TTemplateParam'
|
||||
. ' with no intersection types.',
|
||||
. ' with no intersection types.',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ use Psalm\Type\Union;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* Represents a value of an array or enum.
|
||||
@ -30,6 +29,27 @@ final class TValueOf extends Atomic
|
||||
parent::__construct($from_docblock);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-array<string,EnumCaseStorage> $cases
|
||||
*/
|
||||
private static function getValueTypeForNamedObject(array $cases, TNamedObject $atomic_type): Union
|
||||
{
|
||||
if ($atomic_type instanceof TEnumCase) {
|
||||
assert(isset($cases[$atomic_type->case_name]), 'Should\'ve been verified in TValueOf#getValueType');
|
||||
$value = $cases[$atomic_type->case_name]->value;
|
||||
assert($value !== null, 'Backed enum must have a value.');
|
||||
return new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]);
|
||||
}
|
||||
|
||||
return 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),
|
||||
));
|
||||
}
|
||||
|
||||
public function getKey(bool $include_extra = true): string
|
||||
{
|
||||
return 'value-of<' . $this->type . '>';
|
||||
@ -107,19 +127,14 @@ final class TValueOf extends Atomic
|
||||
$cases = $class_storage->enum_cases;
|
||||
if (!$class_storage->is_enum
|
||||
|| $class_storage->enum_type === null
|
||||
|| count($cases) === 0
|
||||
|| $cases === []
|
||||
|| ($atomic_type instanceof TEnumCase && !isset($cases[$atomic_type->case_name]))
|
||||
) {
|
||||
// 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),
|
||||
));
|
||||
$value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
@ -2146,6 +2146,57 @@ class AssertAnnotationTest extends TestCase
|
||||
'$o->foo===' => 'array{a: 1}',
|
||||
],
|
||||
],
|
||||
'assertionOfBackedEnumValuesWithValueOf' => [
|
||||
'code' => '<?php
|
||||
enum StringEnum: string
|
||||
{
|
||||
case FOO = "foo";
|
||||
case BAR = "bar";
|
||||
case BAZ = "baz";
|
||||
}
|
||||
|
||||
enum IntEnum: int
|
||||
{
|
||||
case FOO = 1;
|
||||
case BAR = 2;
|
||||
case BAZ = 3;
|
||||
}
|
||||
|
||||
/** @psalm-assert value-of<StringEnum::BAR|StringEnum::FOO> $foo */
|
||||
function assertSomeString(string $foo): void
|
||||
{}
|
||||
|
||||
/** @psalm-assert value-of<IntEnum::BAR|IntEnum::FOO> $foo */
|
||||
function assertSomeInt(int $foo): void
|
||||
{}
|
||||
|
||||
/** @param "foo"|"bar" $foo */
|
||||
function takesSomeStringFromEnum(string $foo): StringEnum
|
||||
{
|
||||
return StringEnum::from($foo);
|
||||
}
|
||||
|
||||
/** @param 1|2 $foo */
|
||||
function takesSomeIntFromEnum(int $foo): IntEnum
|
||||
{
|
||||
return IntEnum::from($foo);
|
||||
}
|
||||
|
||||
/** @var non-empty-string $string */
|
||||
$string = null;
|
||||
/** @var positive-int $int */
|
||||
$int = null;
|
||||
|
||||
assertSomeString($string);
|
||||
takesSomeStringFromEnum($string);
|
||||
|
||||
assertSomeInt($int);
|
||||
takesSomeIntFromEnum($int);
|
||||
',
|
||||
'assertions' => [],
|
||||
'ignored_issues' => [],
|
||||
'php_version' => '8.1',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1806,7 +1806,7 @@ class ConstantTest extends TestCase
|
||||
function foo(int $s): string {
|
||||
return [1 => "a", 2 => "b"][$s];
|
||||
}',
|
||||
'error_message' => "offset value of '1|0",
|
||||
'error_message' => "offset value of '0|1",
|
||||
],
|
||||
'constantWithMissingClass' => [
|
||||
'code' => '<?php
|
||||
|
@ -542,6 +542,38 @@ class EnumTest extends TestCase
|
||||
'ignored_issues' => [],
|
||||
'php_version' => '8.1',
|
||||
],
|
||||
'valueOfBackedEnum' => [
|
||||
'code' => <<<'PHP'
|
||||
<?php
|
||||
enum StringEnum: string {
|
||||
case FOO = 'foo';
|
||||
case BAR = 'bar';
|
||||
}
|
||||
|
||||
enum IntEnum: int {
|
||||
case FOO = 1;
|
||||
case BAR = 2;
|
||||
}
|
||||
|
||||
/** @var value-of<StringEnum::FOO> $string */
|
||||
$string = '';
|
||||
/** @var value-of<StringEnum::*> $anyString */
|
||||
$anyString = '';
|
||||
|
||||
/** @var value-of<IntEnum::FOO> $int */
|
||||
$int = 0;
|
||||
/** @var value-of<IntEnum::*> $anyInt */
|
||||
$anyInt = 0;
|
||||
PHP,
|
||||
'assertions' => [
|
||||
'$string===' => '\'foo\'',
|
||||
'$anyString===' => '\'bar\'|\'foo\'',
|
||||
'$int===' => '1',
|
||||
'$anyInt===' => '1|2',
|
||||
],
|
||||
'ignored_issues' => [],
|
||||
'php_version' => '8.1',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,8 @@ use Psalm\Internal\Codebase\ClassConstantByWildcardResolver;
|
||||
use Psalm\Tests\TestCase;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
|
||||
use function reset;
|
||||
|
||||
final class ClassConstantByWildcardResolverTest extends TestCase
|
||||
{
|
||||
private ClassConstantByWildcardResolver $resolver;
|
||||
@ -33,7 +35,8 @@ final class ClassConstantByWildcardResolverTest extends TestCase
|
||||
}
|
||||
',
|
||||
);
|
||||
$this->project_analyzer->getCodebase()->scanFiles();
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$codebase->scanFiles();
|
||||
$resolved = $this->resolver->resolve('ReconciliationTest\\Foo', '*');
|
||||
self::assertNotEmpty($resolved);
|
||||
foreach ($resolved as $type) {
|
||||
@ -57,7 +60,8 @@ final class ClassConstantByWildcardResolverTest extends TestCase
|
||||
}
|
||||
',
|
||||
);
|
||||
$this->project_analyzer->getCodebase()->scanFiles();
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
$codebase->scanFiles();
|
||||
$resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'BA*');
|
||||
self::assertNotEmpty($resolved);
|
||||
foreach ($resolved as $type) {
|
||||
@ -68,7 +72,7 @@ final class ClassConstantByWildcardResolverTest extends TestCase
|
||||
$resolved = $this->resolver->resolve('ReconciliationTest\\Foo', 'QOO');
|
||||
self::assertNotNull($resolved);
|
||||
self::assertCount(1, $resolved);
|
||||
$type = $resolved[0];
|
||||
$type = reset($resolved);
|
||||
self::assertInstanceOf(TLiteralString::class, $type);
|
||||
self::assertTrue($type->value === 'qoo');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user