1
0
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:
orklah 2023-04-02 18:51:25 +02:00 committed by GitHub
commit 87d0854a97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 474 additions and 125 deletions

View File

@ -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);
}
}

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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.',
);
}

View File

@ -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;
}

View File

@ -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',
],
];
}

View File

@ -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

View File

@ -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',
],
];
}

View File

@ -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');
}