diff --git a/build/more-enum-adapter-errors.neon b/build/more-enum-adapter-errors.neon index 05336963934..9a1d09049f8 100644 --- a/build/more-enum-adapter-errors.neon +++ b/build/more-enum-adapter-errors.neon @@ -36,3 +36,23 @@ parameters: rawMessage: 'Parameter #2 $reflection of method PHPStan\Reflection\ClassReflectionFactory::create() expects ReflectionClass, PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass|PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum given.' count: 1 path: ../src/Analyser/NodeScopeResolver.php + + - + rawMessage: 'Right side of || is always false.' + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php + + - + rawMessage: 'If condition is always true.' + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php + + - + rawMessage: 'Result of && is always false.' + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php + + - + rawMessage: 'Strict comparison using === between class-string and ''UnitEnum'' will always evaluate to false.' + count: 1 + path: ../src/Reflection/Php/PhpClassReflectionExtension.php diff --git a/src/Reflection/Php/PhpClassReflectionExtension.php b/src/Reflection/Php/PhpClassReflectionExtension.php index 8d856b59a7d..c278717e8fe 100644 --- a/src/Reflection/Php/PhpClassReflectionExtension.php +++ b/src/Reflection/Php/PhpClassReflectionExtension.php @@ -15,6 +15,7 @@ use PHPStan\BetterReflection\Reflection\Adapter\ReflectionParameter; use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; use PHPStan\Parser\Parser; +use PHPStan\Php\PhpVersion; use PHPStan\PhpDoc\PhpDocInheritanceResolver; use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\PhpDoc\StubPhpDocProvider; @@ -38,6 +39,7 @@ use PHPStan\Reflection\SignatureMap\SignatureMapProvider; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantStringType; @@ -51,6 +53,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypehintHelper; @@ -103,6 +106,7 @@ public function __construct( private AttributeReflectionFactory $attributeReflectionFactory, private ParameterAllowedConstantsMapProvider $allowedConstantsMapProvider, private bool $inferPrivatePropertyTypeFromConstructor, + private PhpVersion $phpVersion, ) { } @@ -202,32 +206,42 @@ private function createProperty( )); } - if ($declaringClassReflection->isEnum()) { + $isUnitEnumInterfaceNameProperty = $this->phpVersion->supportsEnums() + && $propertyName === 'name' + && $declaringClassName === 'UnitEnum'; + + if ($declaringClassReflection->isEnum() || $isUnitEnumInterfaceNameProperty) { if ( $propertyName === 'name' || ($declaringClassReflection->isBackedEnum() && $propertyName === 'value') ) { - $types = []; - foreach ($classReflection->getEnumCases() as $name => $case) { - if ($propertyName === 'name') { - $types[] = new ConstantStringType($name); - continue; - } + if ($declaringClassReflection->isEnum()) { + $types = []; + foreach ($classReflection->getEnumCases() as $name => $case) { + if ($propertyName === 'name') { + $types[] = new ConstantStringType($name); + continue; + } + + $value = $case->getBackingValueType(); + if ($value === null) { + throw new ShouldNotHappenException(); + } - $value = $case->getBackingValueType(); - if ($value === null) { - throw new ShouldNotHappenException(); + $types[] = $value; } - $types[] = $value; + $phpDocType = TypeCombinator::union(...$types); + $nativeType = new MixedType(); + } else { + $phpDocType = TypeCombinator::intersect(new StringType(), new AccessoryNonFalsyStringType()); + $nativeType = new StringType(); } - $phpDocType = TypeCombinator::union(...$types); - return new PhpPropertyReflection( $declaringClassReflection, null, - new MixedType(), + $nativeType, $phpDocType, $phpDocType, $classReflection->getNativeReflection()->getProperty($propertyName), diff --git a/tests/PHPStan/Analyser/nsrt/bug-14839.php b/tests/PHPStan/Analyser/nsrt/bug-14839.php new file mode 100644 index 00000000000..db77f081f29 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14839.php @@ -0,0 +1,43 @@ += 8.1 + +declare(strict_types=1); + +namespace Bug14839; + +use function PHPStan\Testing\assertType; + +enum Foo +{ + + case A; + case B; + +} + +enum Bar: string +{ + + case A = 'a'; + case B = 'b'; + +} + +function test(Foo $foo, Bar $bar, \UnitEnum $u, \BackedEnum $b): void +{ + assertType("'A'|'B'", $foo->name); + assertType("'A'|'B'", $bar->name); + assertType("'a'|'b'", $bar->value); + assertType('non-falsy-string', $u->name); + assertType('non-falsy-string', $b->name); + // `value` stays as its native `int|string`: unlike a case name, a backing value may legitimately be "" or "0". + assertType('int|string', $b->value); +} + +/** + * @template T of \UnitEnum + * @param T $enum + */ +function testTemplate($enum): void +{ + assertType('non-falsy-string', $enum->name); +}