Infer non-falsy-string&non-decimal-int-string for the magic name property on the UnitEnum/BackedEnum interfaces#5894
Conversation
| $phpDocType = TypeCombinator::union(...$types); | ||
| $nativeType = new MixedType(); | ||
| } else { | ||
| $phpDocType = TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()); |
There was a problem hiding this comment.
this could even include non-decimal-int-string
There was a problem hiding this comment.
Done. I addressed staabm's review suggestion that the magic enum name property "could even include non-decimal-int-string".
What I changed
src/Reflection/Php/PhpClassReflectionExtension.php — The UnitEnum/BackedEnum interface name property now intersects three accessory constraints instead of two:
$phpDocType = TypeCombinator::intersect(
new StringType(),
new AccessoryNonFalsyStringType(),
new AccessoryDecimalIntegerStringType(true), // non-decimal-int-string
);This is correct because an enum case name is a valid PHP label — it must start with a letter or underscore, so it can never be a decimal integer string ("0", "123", "-1", …). AccessoryDecimalIntegerStringType(true) is the existing inverse flag that represents non-decimal-int-string.
tests/PHPStan/Analyser/nsrt/bug-14839.php — Updated the three interface/template assertions from non-falsy-string to non-decimal-int-string&non-falsy-string (the order PHPStan's describe() produces).
Verification
bug-14839test passes; all 15 enum nsrt tests and the fullNodeScopeResolverTest(1656 tests) pass.make phpstan→ no errors.
Committed as ada64bf43 and pushed to the PR branch.
…um`/`BackedEnum` interfaces - In `PhpClassReflectionExtension::createProperty()`, set the PHPDoc type of the magic `name` property to `non-falsy-string` when it is declared on the `UnitEnum` interface (also inherited by `BackedEnum` and any `T of UnitEnum` template bound). Previously BetterReflection synthesized it as plain `string`. - Enum case names are always valid PHP labels, so they can never be empty or "0". - Concrete enum types (`Foo $foo`) and individual enum case objects (`Foo::A->name`) were already correct — they resolve to a union of constant strings / a single constant string, which are non-falsy. The backed-enum `value` property is intentionally left as-is, since it may legitimately be an empty string or "0".
Move the UnitEnum/BackedEnum interface `name` narrowing next to the existing concrete-enum `name`/`value` narrowing, so all the synthesized enum-property logic lives in one block instead of being split across the method. The interface branch produces non-falsy-string; `value` is deliberately left as its native int|string (a backing value may be "" or "0"), which the test now documents and guards with `Bar::value`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7aa0920 to
fec4c76
Compare
An enum case name is a valid PHP label, so it must start with a letter or underscore and can never be a decimal integer string. Intersect the synthesized non-falsy-string with non-decimal-int-string for the UnitEnum/BackedEnum interface name property. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
non-falsy-string for the magic name property on the UnitEnum/BackedEnum interfacesnon-falsy-string&non-decimal-int-string for the magic name property on the UnitEnum/BackedEnum interfaces
Summary
Accessing the magic
nameproperty on a value typed as the\UnitEnumor\BackedEnuminterface (or a@template T of \UnitEnumbound) inferred plainstring. Since an enum case name is always a valid PHP label, it can never be an empty string or"0", so it should be inferred asnon-falsy-string.Changes
src/Reflection/Php/PhpClassReflectionExtension.php: increateProperty(), when the resolved property is the magicnamedeclared on theUnitEnuminterface and no PHPDoc type was found, set its PHPDoc type tonon-falsy-string(StringTypeintersected withAccessoryNonFalsyStringType).tests/PHPStan/Analyser/nsrt/bug-14839.php.Root cause
BetterReflection synthesizes the magic enum members (
ReflectionClass::getEnumProperties()): thenameproperty on theUnitEnuminterface is created with the native typestring. PHPStan already special-cases concrete enum classes (producing a union of constant-string case names) and individual enum-case objects (EnumCaseObjectType, producing a singleConstantStringType) — both of which are non-falsy. But values typed only by theUnitEnum/BackedEnuminterface (or a template bounded by them) never hit those special cases and fell back to the synthesizedstring.The fix narrows the
nameproperty tonon-falsy-stringat the single place where the interface-declared property is reflected, which also coversBackedEnum(inheritsnamefromUnitEnum) and@template T of \UnitEnumparameters.Test
tests/PHPStan/Analyser/nsrt/bug-14839.phpasserts:Foo $foo/Bar $bar(concrete enums) →'A'|'B'(already correct, kept as a guard).\UnitEnum $u→non-falsy-string(wasstring).\BackedEnum $b→non-falsy-stringfor->name(wasstring) andint|stringfor->value(unchanged).@template T of \UnitEnumparameter →non-falsy-string(wasstring).Verified the test fails before the fix (the
UnitEnum/BackedEnum/template assertions reportedstring) and passes after.Analogous cases probed
EnumCaseObjectType) — already non-falsy, no change needed.valueproperty — intentionally left asint|string, since a backing value may legitimately be an empty string or"0".Fixes phpstan/phpstan#14839