Skip to content

Stop the recursion when an enum case value reads itself - #6731

Merged
staabm merged 2 commits into
phpstan:2.3.xfrom
SanderMuller:enum-case-self-reference
Oct 16, 2026
Merged

staabm merged 2 commits into
phpstan:2.3.xfrom
SanderMuller:enum-case-self-reference

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

A backed enum case can read its own value, for example case A = self::A->value;. ClassReflection::getEnumCase() then resolved the same value expression again and again. With the Turbo extension, the worker stopped on the call stack limit. Without it, the worker ran until the memory limit stopped it.

getEnumCase() and getEnumCases() now resolve the case value through the new InitializerExprTypeResolver::getEnumCaseValueType(). This method uses the currentlyResolvingClassConstant guard that class constants already use, with the key Class::case. The read of a case that is being resolved now gives mixed, as the read of a class constant that is being resolved does. So case A = self::A->value; resolves to mixed, and case A = self::A->value . 'x'; resolves to non-falsy-string, the same as const A = self::A . 'x';.

When the value resolves to mixed, EnumSanityRule reports the case with enum.caseType:

Enum case E::A value mixed does not match the "string" type.

PHP rejects this code at runtime with "Cannot declare self-referencing constant", so the error is on code that cannot run.

The Turbo extension mirrors the change in InitializerExprTypeResolver.cpp and ClassReflection.cpp.

The tests cover these cases:

  • a string and an int backed case that read themselves
  • two cases that read each other, through getEnumCase() and through getEnumCases()
  • a cycle across two enums
  • a concatenation
  • a valid forward reference

Closes phpstan/phpstan#15458

🤖 Generated with Claude Code

Comment on lines +518 to +519
* Resolves the backing value of an enum case. A read of the case while
* its value is being resolved gives mixed, like a class constant does.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Resolves the backing value of an enum case. A read of the case while
* its value is being resolved gives mixed, like a class constant does.
* Resolves the backing value of an enum case. A read of the case while
* its value is being resolved gives mixed - like a class constant does - to prevent endless recursions.

@staabm staabm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs rebase

@SanderMuller
SanderMuller force-pushed the enum-case-self-reference branch from 425e467 to 305963f Compare October 14, 2026 13:29
@staabm
staabm requested a review from VincentLanglet October 14, 2026 13:30
SanderMuller and others added 2 commits October 16, 2026 10:17
ClassReflection::getEnumCase() and getEnumCases() now resolve a case
value through InitializerExprTypeResolver::getEnumCaseValueType(). It
uses the currentlyResolvingClassConstant guard of class constants.

Before, a case value that read itself, directly or through other cases,
recursed until the call stack limit or the memory limit stopped the
process. Now the read of the case that is being resolved gives mixed,
as the read of a class constant that is being resolved does.

The Turbo extension mirrors the change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@staabm
staabm force-pushed the enum-case-self-reference branch from 305963f to 5230ed7 Compare October 16, 2026 08:17
@staabm
staabm merged commit bfca0a6 into phpstan:2.3.x Oct 16, 2026
968 of 973 checks passed
@staabm

staabm commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Infinite recursion on a backed enum case whose value reads its own value

3 participants