diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 7ed3221cd0..700a81c858 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -28,6 +28,7 @@ use PHPStan\Reflection\TrivialParametersAcceptor; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnionTypePropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; @@ -154,6 +155,29 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { + $classReflection = $this->getClassReflection(); + if ($classReflection !== null) { + if ($classReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $classReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getProperty($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypePropertyReflection($properties); + } + } + } + } + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index ce6d5c6a58..efe62f843c 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -12,6 +12,7 @@ use PHPStan\Reflection\ReflectionProviderStaticAccessor; use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection; +use PHPStan\Reflection\Type\UnionTypePropertyReflection; use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; @@ -20,6 +21,7 @@ use PHPStan\Type\Traits\NonGeneralizableTypeTrait; use PHPStan\Type\Traits\NonGenericTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use function count; use function get_class; use function sprintf; @@ -218,6 +220,27 @@ public function hasProperty(string $propertyName): TrinaryLogic public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection { + $classReflection = $this->getClassReflection(); + if ($classReflection->isEnum()) { + if ( + $propertyName === 'name' + || ($propertyName === 'value' && $classReflection->isBackedEnum()) + ) { + $properties = []; + foreach ($this->getEnumCases() as $enumCase) { + $properties[] = $enumCase->getProperty($propertyName, $scope); + } + + if (count($properties) > 0) { + if (count($properties) === 1) { + return $properties[0]; + } + + return new UnionTypePropertyReflection($properties); + } + } + } + return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty(); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index ec6700978a..af3748420c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1207,6 +1207,10 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Constants/data/bug-8957.php'); } + if (PHP_VERSION_ID >= 80100) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8486.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8956.php'); } diff --git a/tests/PHPStan/Analyser/data/bug-8486.php b/tests/PHPStan/Analyser/data/bug-8486.php new file mode 100644 index 0000000000..1f2025276a --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-8486.php @@ -0,0 +1,88 @@ += 8.1 + +namespace Bug8486; + +use function PHPStan\Testing\assertType; + +enum Operator: string +{ + case Foo = 'foo'; + case Bar = 'bar'; + case None = ''; + + public function explode(): void + { + $character = match ($this) { + self::None => 'baz', + default => $this->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(): void + { + match ($this) { + self::None => 'baz', + default => assertType('$this(Bug8486\Operator~Bug8486\Operator::None)', $this), + }; + } + + public function typeInference2(): void + { + if ($this === self::None) { + return; + } + + assertType("'Bar'|'Foo'", $this->name); + assertType("'bar'|'foo'", $this->value); + } +} + +class Foo +{ + + public function doFoo(Operator $operator) + { + $character = match ($operator) { + Operator::None => 'baz', + default => $operator->value, + }; + + assertType("'bar'|'baz'|'foo'", $character); + } + + public function typeInference(Operator $operator): void + { + match ($operator) { + Operator::None => 'baz', + default => assertType('Bug8486\Operator~Bug8486\Operator::None', $operator), + }; + } + + public function typeInference2(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + assertType("'Bar'|'Foo'", $operator->name); + assertType("'bar'|'foo'", $operator->value); + } + + public function typeInference3(Operator $operator): void + { + if ($operator === Operator::None) { + return; + } + + if ($operator === Operator::Foo) { + return; + } + + assertType("Bug8486\Operator::Bar", $operator); + assertType("'Bar'", $operator->name); + assertType("'bar'", $operator->value); + } + +} diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 8c013acd93..2cab38f8d2 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -84,7 +84,7 @@ public function testRule(): void 'Readonly property ReadonlyPropertyAssign\ListAssign::$foo is assigned outside of the constructor.', 127, ], - [ + /*[ 'Readonly property ReadonlyPropertyAssign\FooEnum::$name is assigned outside of the constructor.', 140, ], @@ -99,7 +99,7 @@ public function testRule(): void [ 'Readonly property ReadonlyPropertyAssign\FooEnum::$value is assigned outside of its declaring class.', 152, - ], + ],*/ [ 'Readonly property ReadonlyPropertyAssign\Foo::$baz is assigned outside of its declaring class.', 162,