Skip to content

Infer non-falsy-string&non-decimal-int-string for the magic name property on the UnitEnum/BackedEnum interfaces#5894

Merged
staabm merged 9 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-xlf7wmo
Jun 19, 2026
Merged

Infer non-falsy-string&non-decimal-int-string for the magic name property on the UnitEnum/BackedEnum interfaces#5894
staabm merged 9 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-xlf7wmo

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Accessing the magic name property on a value typed as the \UnitEnum or \BackedEnum interface (or a @template T of \UnitEnum bound) inferred plain string. 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 as non-falsy-string.

Changes

  • src/Reflection/Php/PhpClassReflectionExtension.php: in createProperty(), when the resolved property is the magic name declared on the UnitEnum interface and no PHPDoc type was found, set its PHPDoc type to non-falsy-string (StringType intersected with AccessoryNonFalsyStringType).
  • Added a regression test tests/PHPStan/Analyser/nsrt/bug-14839.php.

Root cause

BetterReflection synthesizes the magic enum members (ReflectionClass::getEnumProperties()): the name property on the UnitEnum interface is created with the native type string. PHPStan already special-cases concrete enum classes (producing a union of constant-string case names) and individual enum-case objects (EnumCaseObjectType, producing a single ConstantStringType) — both of which are non-falsy. But values typed only by the UnitEnum/BackedEnum interface (or a template bounded by them) never hit those special cases and fell back to the synthesized string.

The fix narrows the name property to non-falsy-string at the single place where the interface-declared property is reflected, which also covers BackedEnum (inherits name from UnitEnum) and @template T of \UnitEnum parameters.

Test

tests/PHPStan/Analyser/nsrt/bug-14839.php asserts:

  • Foo $foo / Bar $bar (concrete enums) → 'A'|'B' (already correct, kept as a guard).
  • \UnitEnum $unon-falsy-string (was string).
  • \BackedEnum $bnon-falsy-string for ->name (was string) and int|string for ->value (unchanged).
  • @template T of \UnitEnum parameter → non-falsy-string (was string).

Verified the test fails before the fix (the UnitEnum/BackedEnum/template assertions reported string) and passes after.

Analogous cases probed

  • Concrete enum types and individual enum-case objects (EnumCaseObjectType) — already non-falsy, no change needed.
  • The backed-enum value property — intentionally left as int|string, since a backing value may legitimately be an empty string or "0".

Fixes phpstan/phpstan#14839

Comment thread tests/PHPStan/Analyser/nsrt/bug-14839.php
$phpDocType = TypeCombinator::union(...$types);
$nativeType = new MixedType();
} else {
$phpDocType = TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType());

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.

this could even include non-decimal-int-string

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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-14839 test passes; all 15 enum nsrt tests and the full NodeScopeResolverTest (1656 tests) pass.
  • make phpstan → no errors.

Committed as ada64bf43 and pushed to the PR branch.

phpstan-bot and others added 7 commits June 19, 2026 15:03
…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>
@staabm staabm force-pushed the create-pull-request/patch-xlf7wmo branch from 7aa0920 to fec4c76 Compare June 19, 2026 13:03
phpstan-bot and others added 2 commits June 19, 2026 13:08
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>
@staabm staabm changed the title Infer non-falsy-string for the magic name property on the UnitEnum/BackedEnum interfaces Infer non-falsy-string&non-decimal-int-string for the magic name property on the UnitEnum/BackedEnum interfaces Jun 19, 2026
@staabm staabm merged commit f692c09 into phpstan:2.2.x Jun 19, 2026
666 of 671 checks passed
@staabm staabm deleted the create-pull-request/patch-xlf7wmo branch June 19, 2026 13:33
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.

magic "name" property on enum case should be non-falsy-string

3 participants