From 1d7dfda1b2f7b9104856b3cea5f21ac859da06df Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Mon, 27 Apr 2026 15:40:12 +0000 Subject: [PATCH] Add resolveExprPropertyFetch to support Enum::CASE->value expressions - Add resolveExprPropertyFetch method in NodeExpressionResolver to resolve property fetch on objects (e.g. BackedEnum::CASE->value) - Use native reflection for loaded enum classes in fetchReflectionClass so enum case constants resolve to actual enum instances - Reset isConstant/constantName in property fetch since PHP does not consider Enum::CASE->value a constant expression - Add ClassWithBackedEnumDefaultValue test stub - Add unit test for resolver and integration test for parameter defaults Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/Resolver/NodeExpressionResolver.php | 33 ++++++++++++++++++- tests/ReflectionParameterTest.php | 20 +++++++++++ tests/Resolver/NodeExpressionResolverTest.php | 13 ++++++++ tests/Stub/FileWithClasses81.php | 8 +++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/Resolver/NodeExpressionResolver.php b/src/Resolver/NodeExpressionResolver.php index 50a57db..8358b24 100644 --- a/src/Resolver/NodeExpressionResolver.php +++ b/src/Resolver/NodeExpressionResolver.php @@ -534,6 +534,37 @@ protected function resolveExprClassConstFetch(Expr\ClassConstFetch $node): mixed return $refClass->getConstant($constantName); } + /** + * Resolves property fetch on an object, e.g. SomeEnum::CASE->value + */ + protected function resolveExprPropertyFetch(Expr\PropertyFetch $node): mixed + { + $object = $this->resolve($node->var); + if (!is_object($object)) { + throw new ReflectionException("Property fetch requires an object, got " . gettype($object)); + } + + if ($node->name instanceof Node\Identifier) { + $propertyName = $node->name->toString(); + } else { + $resolvedName = $this->resolve($node->name); + if (!is_string($resolvedName)) { + throw new ReflectionException("Could not resolve property name for property fetch."); + } + $propertyName = $resolvedName; + } + + if (!property_exists($object, $propertyName)) { + throw new ReflectionException(sprintf("Property '%s' does not exist on object of type %s", $propertyName, get_class($object))); + } + + $this->isConstant = false; + $this->constantName = null; + $this->isConstExpr = true; + + return $object->$propertyName; + } + /** * @return array */ @@ -803,7 +834,7 @@ private function fetchReflectionClass(Node\Name $node) // PHP's ReflectionClass to determine if the class is user defined if (class_exists($className, false)) { $refClass = new \ReflectionClass($className); - if (!$refClass->isUserDefined()) { + if (!$refClass->isUserDefined() || $refClass->isEnum()) { return $refClass; } } diff --git a/tests/ReflectionParameterTest.php b/tests/ReflectionParameterTest.php index e2a0d89..aca4030 100644 --- a/tests/ReflectionParameterTest.php +++ b/tests/ReflectionParameterTest.php @@ -254,6 +254,26 @@ public function testParametersWithNewExpressionDefaults(): void $this->assertInstanceOf(\stdClass::class, $defaultValue3); } + /** + * Test that parameters with backed enum property fetch default values work correctly + */ + public function testParametersWithBackedEnumPropertyDefault(): void + { + $fileName = __DIR__ . '/Stub/FileWithClasses81.php'; + $reflectionFile = new ReflectionFile($fileName); + $parsedFileNamespace = $reflectionFile->getFileNamespace('Go\ParserReflection\Stub'); + + $parsedClass = $parsedFileNamespace->getClass('Go\ParserReflection\Stub\ClassWithBackedEnumDefaultValue'); + $parsedMethod = $parsedClass->getMethod('getRefusalDescription'); + $parsedParameter = $parsedMethod->getParameters()[0]; + + $this->assertSame('channel', $parsedParameter->getName()); + $this->assertTrue($parsedParameter->isDefaultValueAvailable()); + + $defaultValue = $parsedParameter->getDefaultValue(); + $this->assertSame('get', $defaultValue); + } + /** * @inheritDoc */ diff --git a/tests/Resolver/NodeExpressionResolverTest.php b/tests/Resolver/NodeExpressionResolverTest.php index d89d66c..97a5261 100644 --- a/tests/Resolver/NodeExpressionResolverTest.php +++ b/tests/Resolver/NodeExpressionResolverTest.php @@ -105,4 +105,17 @@ public function testResolveNewExpressionDateTimeImmutable(): void $value = $expressionSolver->getValue(); $this->assertInstanceOf(\DateTimeImmutable::class, $value); } + + /** + * Testing resolving property fetch on a backed enum case (e.g. Enum::CASE->value) + */ + public function testResolvePropertyFetchOnEnumCase(): void + { + require_once __DIR__ . '/../Stub/FileWithClasses81.php'; + + $expressionNodeTree = $this->parser->parse("value;"); + $expressionSolver = new NodeExpressionResolver(NULL); + $expressionSolver->process($expressionNodeTree[0]); + $this->assertSame('get', $expressionSolver->getValue()); + } } diff --git a/tests/Stub/FileWithClasses81.php b/tests/Stub/FileWithClasses81.php index b080901..18b8138 100644 --- a/tests/Stub/FileWithClasses81.php +++ b/tests/Stub/FileWithClasses81.php @@ -117,3 +117,11 @@ function functionWithPhp81NeverReturnType(): never class ClassWithPhp81FinalClassConst { final public const TEST = '1'; } + +class ClassWithBackedEnumDefaultValue +{ + public function getRefusalDescription(string $channel = BackedPhp81EnumHTTPMethods::GET->value): string + { + return $channel; + } +}