Skip to content

Commit e99f997

Browse files
committed
Late binding of enum cases
Resolves a number of long-standing bugs ('Failed to infer case value ...') Fixes #10374 Fixes #10560 Fixes #10643 Fixes #8978
1 parent 59df6a7 commit e99f997

File tree

10 files changed

+124
-34
lines changed

10 files changed

+124
-34
lines changed

src/Psalm/Internal/Analyzer/ClassAnalyzer.php

+9-8
Original file line numberDiff line numberDiff line change
@@ -2486,25 +2486,26 @@ private function checkEnum(): void
24862486

24872487
$seen_values = [];
24882488
foreach ($storage->enum_cases as $case_storage) {
2489-
if ($case_storage->value !== null && $storage->enum_type === null) {
2489+
$case_value = $case_storage->getValue($this->getCodebase()->classlikes);
2490+
if ($case_value !== null && $storage->enum_type === null) {
24902491
IssueBuffer::maybeAdd(
24912492
new InvalidEnumCaseValue(
24922493
'Case of a non-backed enum should not have a value',
24932494
$case_storage->stmt_location,
24942495
$storage->name,
24952496
),
24962497
);
2497-
} elseif ($case_storage->value === null && $storage->enum_type !== null) {
2498+
} elseif ($case_value === null && $storage->enum_type !== null) {
24982499
IssueBuffer::maybeAdd(
24992500
new InvalidEnumCaseValue(
25002501
'Case of a backed enum should have a value',
25012502
$case_storage->stmt_location,
25022503
$storage->name,
25032504
),
25042505
);
2505-
} elseif ($case_storage->value !== null) {
2506-
if ((is_int($case_storage->value) && $storage->enum_type === 'string')
2507-
|| (is_string($case_storage->value) && $storage->enum_type === 'int')
2506+
} elseif ($case_value !== null) {
2507+
if ((is_int($case_value) && $storage->enum_type === 'string')
2508+
|| (is_string($case_value) && $storage->enum_type === 'int')
25082509
) {
25092510
IssueBuffer::maybeAdd(
25102511
new InvalidEnumCaseValue(
@@ -2516,8 +2517,8 @@ private function checkEnum(): void
25162517
}
25172518
}
25182519

2519-
if ($case_storage->value !== null) {
2520-
if (in_array($case_storage->value, $seen_values, true)) {
2520+
if ($case_value !== null) {
2521+
if (in_array($case_value, $seen_values, true)) {
25212522
IssueBuffer::maybeAdd(
25222523
new DuplicateEnumCaseValue(
25232524
'Enum case values should be unique',
@@ -2526,7 +2527,7 @@ private function checkEnum(): void
25262527
),
25272528
);
25282529
} else {
2529-
$seen_values[] = $case_storage->value;
2530+
$seen_values[] = $case_value;
25302531
}
25312532
}
25322533
}

src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -1035,10 +1035,11 @@ private static function handleEnumValue(
10351035
$case_values = [];
10361036

10371037
foreach ($enum_cases as $enum_case) {
1038-
if (is_string($enum_case->value)) {
1039-
$case_values[] = Type::getAtomicStringFromLiteral($enum_case->value);
1040-
} elseif (is_int($enum_case->value)) {
1041-
$case_values[] = new TLiteralInt($enum_case->value);
1038+
$case_value = $enum_case->getValue($statements_analyzer->getCodebase()->classlikes);
1039+
if (is_string($case_value)) {
1040+
$case_values[] = Type::getAtomicStringFromLiteral($case_value);
1041+
} elseif (is_int($case_value)) {
1042+
$case_values[] = new TLiteralInt($case_value);
10421043
} else {
10431044
// this should never happen
10441045
$case_values[] = new TMixed();

src/Psalm/Internal/Codebase/ConstantTypeResolver.php

+7
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,13 @@ public static function resolve(
344344
return Type::getString($value)->getSingleAtomic();
345345
} elseif (is_int($value)) {
346346
return Type::getInt(false, $value)->getSingleAtomic();
347+
} elseif ($value instanceof UnresolvedConstantComponent) {
348+
return self::resolve(
349+
$classlikes,
350+
$value,
351+
$statements_analyzer,
352+
$visited_constant_ids + [$c_id => true],
353+
);
347354
}
348355
} elseif ($c instanceof EnumNameFetch) {
349356
return Type::getString($c->case)->getSingleAtomic();

src/Psalm/Internal/Codebase/Methods.php

+5-3
Original file line numberDiff line numberDiff line change
@@ -628,11 +628,13 @@ public function getMethodReturnType(
628628
) {
629629
$types = [];
630630
foreach ($original_class_storage->enum_cases as $case_name => $case_storage) {
631+
$case_value = $case_storage->getValue($this->classlikes);
632+
631633
if (UnionTypeComparator::isContainedBy(
632634
$source_analyzer->getCodebase(),
633-
is_int($case_storage->value) ?
634-
Type::getInt(false, $case_storage->value) :
635-
Type::getString($case_storage->value),
635+
is_int($case_value) ?
636+
Type::getInt(false, $case_value) :
637+
Type::getString($case_value),
636638
$first_arg_type,
637639
)) {
638640
$types[] = new TEnumCase($original_fq_class_name, $case_name);

src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Psalm\Internal\Provider\NodeDataProvider;
3333
use Psalm\Internal\Scanner\ClassLikeDocblockComment;
3434
use Psalm\Internal\Scanner\FileScanner;
35+
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
3536
use Psalm\Internal\Type\TypeAlias;
3637
use Psalm\Internal\Type\TypeAlias\ClassTypeAlias;
3738
use Psalm\Internal\Type\TypeAlias\InlineTypeAlias;
@@ -65,7 +66,6 @@
6566
use Psalm\Type\Atomic\TString;
6667
use Psalm\Type\Atomic\TTemplateParam;
6768
use Psalm\Type\Union;
68-
use RuntimeException;
6969
use UnexpectedValueException;
7070

7171
use function array_merge;
@@ -752,6 +752,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool
752752
$values_types[] = Type::getAtomicStringFromLiteral($enumCaseStorage->value);
753753
} elseif (is_int($enumCaseStorage->value)) {
754754
$values_types[] = new Type\Atomic\TLiteralInt($enumCaseStorage->value);
755+
} elseif ($enumCaseStorage->value instanceof UnresolvedConstantComponent) {
756+
// backed enum with a type yet unknown
757+
$values_types[] = new Type\Atomic\TMixed;
755758
}
756759
}
757760
}
@@ -1462,7 +1465,12 @@ private function visitEnumDeclaration(
14621465
);
14631466
}
14641467
} else {
1465-
throw new RuntimeException('Failed to infer case value for ' . $stmt->name->name);
1468+
$enum_value = ExpressionResolver::getUnresolvedClassConstExpr(
1469+
$stmt->expr,
1470+
$this->aliases,
1471+
$fq_classlike_name,
1472+
$storage->parent_class,
1473+
);
14661474
}
14671475
}
14681476

src/Psalm/Internal/Provider/ReturnTypeProvider/GetObjectVarsReturnTypeProvider.php

+5-4
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ public static function getGetObjectVarsReturnType(
6363
return new TKeyedArray($properties);
6464
}
6565
$enum_case_storage = $enum_classlike_storage->enum_cases[$object_type->case_name];
66-
if (is_int($enum_case_storage->value)) {
67-
$properties['value'] = new Union([new Atomic\TLiteralInt($enum_case_storage->value)]);
68-
} elseif (is_string($enum_case_storage->value)) {
69-
$properties['value'] = new Union([Type::getAtomicStringFromLiteral($enum_case_storage->value)]);
66+
$case_value = $enum_case_storage->getValue($statements_source->getCodebase()->classlikes);
67+
if (is_int($case_value)) {
68+
$properties['value'] = new Union([new Atomic\TLiteralInt($case_value)]);
69+
} elseif (is_string($case_value)) {
70+
$properties['value'] = new Union([Type::getAtomicStringFromLiteral($case_value)]);
7071
}
7172
return new TKeyedArray($properties);
7273
}

src/Psalm/Internal/Type/SimpleAssertionReconciler.php

+7-4
Original file line numberDiff line numberDiff line change
@@ -2971,11 +2971,12 @@ private static function reconcileValueOf(
29712971
// For value-of<MyBackedEnum>, the assertion is meant to return *ANY* value of *ANY* enum case
29722972
if ($enum_case_to_assert === null) {
29732973
foreach ($class_storage->enum_cases as $enum_case) {
2974+
$enum_value = $enum_case->getValue($codebase->classlikes);
29742975
assert(
2975-
$enum_case->value !== null,
2976+
$enum_value !== null,
29762977
'Verified enum type above, value can not contain `null` anymore.',
29772978
);
2978-
$reconciled_types[] = Type::getLiteral($enum_case->value);
2979+
$reconciled_types[] = Type::getLiteral($enum_value);
29792980
}
29802981

29812982
continue;
@@ -2986,8 +2987,10 @@ private static function reconcileValueOf(
29862987
return null;
29872988
}
29882989

2989-
assert($enum_case->value !== null, 'Verified enum type above, value can not contain `null` anymore.');
2990-
$reconciled_types[] = Type::getLiteral($enum_case->value);
2990+
$enum_value = $enum_case->getValue($codebase->classlikes);
2991+
2992+
assert($enum_value !== null, 'Verified enum type above, value can not contain `null` anymore.');
2993+
$reconciled_types[] = Type::getLiteral($enum_value);
29912994
}
29922995

29932996
if ($reconciled_types === []) {

src/Psalm/Storage/EnumCaseStorage.php

+31-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@
33
namespace Psalm\Storage;
44

55
use Psalm\CodeLocation;
6+
use Psalm\Internal\Codebase\ClassLikes;
7+
use Psalm\Internal\Codebase\ConstantTypeResolver;
8+
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
9+
use Psalm\Type\Atomic\TLiteralInt;
10+
use Psalm\Type\Atomic\TLiteralString;
11+
use UnexpectedValueException;
612

713
final class EnumCaseStorage
814
{
915
use UnserializeMemoryUsageSuppressionTrait;
1016

1117
/**
12-
* @var int|string|null
18+
* @var int|string|null|UnresolvedConstantComponent
1319
*/
1420
public $value;
1521

@@ -22,7 +28,7 @@ final class EnumCaseStorage
2228
public $deprecated = false;
2329

2430
/**
25-
* @param int|string|null $value
31+
* @param int|string|null|UnresolvedConstantComponent $value
2632
*/
2733
public function __construct(
2834
$value,
@@ -31,4 +37,27 @@ public function __construct(
3137
$this->value = $value;
3238
$this->stmt_location = $location;
3339
}
40+
41+
/** @return int|string|null */
42+
public function getValue(ClassLikes $classlikes)
43+
{
44+
$case_value = $this->value;
45+
46+
if ($case_value instanceof UnresolvedConstantComponent) {
47+
$case_value = ConstantTypeResolver::resolve(
48+
$classlikes,
49+
$case_value,
50+
);
51+
52+
if ($case_value instanceof TLiteralString) {
53+
$case_value = $case_value->value;
54+
} elseif ($case_value instanceof TLiteralInt) {
55+
$case_value = $case_value->value;
56+
} else {
57+
throw new UnexpectedValueException('Failed to infer case value');
58+
}
59+
}
60+
61+
return $case_value;
62+
}
3463
}

src/Psalm/Type/Atomic/TValueOf.php

+11-7
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,24 @@ public function __construct(Union $type, bool $from_docblock = false)
3232
/**
3333
* @param non-empty-array<string,EnumCaseStorage> $cases
3434
*/
35-
private static function getValueTypeForNamedObject(array $cases, TNamedObject $atomic_type): Union
36-
{
35+
private static function getValueTypeForNamedObject(
36+
array $cases,
37+
TNamedObject $atomic_type,
38+
Codebase $codebase,
39+
): Union {
3740
if ($atomic_type instanceof TEnumCase) {
3841
assert(isset($cases[$atomic_type->case_name]), 'Should\'ve been verified in TValueOf#getValueType');
39-
$value = $cases[$atomic_type->case_name]->value;
42+
$value = $cases[$atomic_type->case_name]->getValue($codebase->classlikes);
4043
assert($value !== null, 'Backed enum must have a value.');
4144
return new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($value)]);
4245
}
4346

4447
return new Union(array_map(
45-
static function (EnumCaseStorage $case): Atomic {
46-
assert($case->value !== null);
48+
static function (EnumCaseStorage $case) use ($codebase): Atomic {
49+
$case_value = $case->getValue($codebase->classlikes);
50+
assert($case_value !== null);
4751
// Backed enum must have a value
48-
return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value);
52+
return ConstantTypeResolver::getLiteralTypeFromScalarValue($case_value);
4953
},
5054
array_values($cases),
5155
));
@@ -141,7 +145,7 @@ public static function getValueType(
141145
continue;
142146
}
143147

144-
$value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type);
148+
$value_atomics = self::getValueTypeForNamedObject($cases, $atomic_type, $codebase);
145149
} else {
146150
continue;
147151
}

tests/EnumTest.php

+34
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,40 @@ enum Bar: int
679679
'ignored_issues' => [],
680680
'php_version' => '8.1',
681681
],
682+
'enumWithCasesReferencingClassConstantsWhereClassIsDefinedAfterTheEnum' => [
683+
'code' => <<<'PHP'
684+
<?php
685+
enum Bar: string {
686+
case FOO = Foo::FOO;
687+
}
688+
class Foo {
689+
const FOO = "foo";
690+
}
691+
$a = Bar::FOO->value;
692+
PHP,
693+
'assertions' => [
694+
'$a===' => "'foo'",
695+
],
696+
'ignored_issues' => [],
697+
'php_version' => '8.1',
698+
],
699+
'enumWithCasesReferencingAnotherEnumCase' => [
700+
'code' => <<<'PHP'
701+
<?php
702+
enum Bar: string {
703+
case BAR = Foo::FOO->value;
704+
}
705+
enum Foo: string {
706+
case FOO = "foo";
707+
}
708+
$a = Bar::BAR->value;
709+
PHP,
710+
'assertions' => [
711+
'$a===' => "'foo'",
712+
],
713+
'ignored_issues' => [],
714+
'php_version' => '8.1',
715+
],
682716
];
683717
}
684718

0 commit comments

Comments
 (0)