From b5e72658d5745b26c29b57f7f1b25ba1e37bb27f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 22:12:15 +0000 Subject: [PATCH] feat: add NodeAwareInterface exposing getNode() on all parsed reflections Introduces Go\ParserReflection\NodeAwareInterface with a single getNode(): Node method and implements it on all 11 parsed reflection classes that wrap an AST node. Each class narrows the return type via covariance to its concrete node type (ClassLike, ClassMethod, Param, EnumCase, etc.), making direct AST access an official client API. Closes #195 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NDeyey11xvinYJshLsA4Tn --- src/NodeAwareInterface.php | 29 +++++++ src/ReflectionAttribute.php | 2 +- src/ReflectionClass.php | 2 +- src/ReflectionClassConstant.php | 2 +- src/ReflectionEnum.php | 2 +- src/ReflectionEnumBackedCase.php | 2 +- src/ReflectionEnumUnitCase.php | 2 +- src/ReflectionFileNamespace.php | 2 +- src/ReflectionFunction.php | 2 +- src/ReflectionMethod.php | 2 +- src/ReflectionParameter.php | 2 +- src/ReflectionProperty.php | 2 +- tests/NodeAwareInterfaceTest.php | 144 +++++++++++++++++++++++++++++++ 13 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 src/NodeAwareInterface.php create mode 100644 tests/NodeAwareInterfaceTest.php diff --git a/src/NodeAwareInterface.php b/src/NodeAwareInterface.php new file mode 100644 index 0000000..4cedff3 --- /dev/null +++ b/src/NodeAwareInterface.php @@ -0,0 +1,29 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\ParserReflection; + +use PhpParser\Node; + +/** + * Interface for parsed reflections that are aware of their underlying AST node + * + * Implementations narrow the return type of getNode() via covariance to expose + * the concrete node type they wrap (e.g. ClassLike, ClassMethod, Param, etc.) + */ +interface NodeAwareInterface +{ + /** + * Returns the underlying AST node for this reflection + */ + public function getNode(): Node; +} diff --git a/src/ReflectionAttribute.php b/src/ReflectionAttribute.php index 1416144..85299b6 100644 --- a/src/ReflectionAttribute.php +++ b/src/ReflectionAttribute.php @@ -28,7 +28,7 @@ * * @extends \ReflectionAttribute */ -class ReflectionAttribute extends BaseReflectionAttribute +class ReflectionAttribute extends BaseReflectionAttribute implements NodeAwareInterface { /** * Fully-qualified attribute class name. diff --git a/src/ReflectionClass.php b/src/ReflectionClass.php index 00866d6..d54887d 100644 --- a/src/ReflectionClass.php +++ b/src/ReflectionClass.php @@ -30,7 +30,7 @@ * @see \Go\ParserReflection\ReflectionClassTest * @extends \ReflectionClass */ -final class ReflectionClass extends InternalReflectionClass +final class ReflectionClass extends InternalReflectionClass implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use ReflectionClassLikeTrait; diff --git a/src/ReflectionClassConstant.php b/src/ReflectionClassConstant.php index a55926e..6a24445 100644 --- a/src/ReflectionClassConstant.php +++ b/src/ReflectionClassConstant.php @@ -26,7 +26,7 @@ /** * @see \Go\ParserReflection\ReflectionClassConstantTest */ -final class ReflectionClassConstant extends BaseReflectionClassConstant +final class ReflectionClassConstant extends BaseReflectionClassConstant implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use AttributeResolverTrait; diff --git a/src/ReflectionEnum.php b/src/ReflectionEnum.php index 5c44a44..6b7ed3e 100644 --- a/src/ReflectionEnum.php +++ b/src/ReflectionEnum.php @@ -25,7 +25,7 @@ * @see \Go\ParserReflection\ReflectionEnumTest * @extends InternalReflectionEnum<\UnitEnum> */ -final class ReflectionEnum extends InternalReflectionEnum +final class ReflectionEnum extends InternalReflectionEnum implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use ReflectionClassLikeTrait; diff --git a/src/ReflectionEnumBackedCase.php b/src/ReflectionEnumBackedCase.php index bc3e14f..2447b2c 100644 --- a/src/ReflectionEnumBackedCase.php +++ b/src/ReflectionEnumBackedCase.php @@ -20,7 +20,7 @@ /** * AST-based reflection for enum backed cases */ -final class ReflectionEnumBackedCase extends InternalReflectionEnumBackedCase +final class ReflectionEnumBackedCase extends InternalReflectionEnumBackedCase implements NodeAwareInterface { use Traits\InternalPropertiesEmulationTrait; use Traits\AttributeResolverTrait; diff --git a/src/ReflectionEnumUnitCase.php b/src/ReflectionEnumUnitCase.php index 5d47fc6..59f68a9 100644 --- a/src/ReflectionEnumUnitCase.php +++ b/src/ReflectionEnumUnitCase.php @@ -21,7 +21,7 @@ /** * AST-based reflection for enum unit cases */ -final class ReflectionEnumUnitCase extends InternalReflectionEnumUnitCase +final class ReflectionEnumUnitCase extends InternalReflectionEnumUnitCase implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use AttributeResolverTrait; diff --git a/src/ReflectionFileNamespace.php b/src/ReflectionFileNamespace.php index 82a432e..8761d09 100644 --- a/src/ReflectionFileNamespace.php +++ b/src/ReflectionFileNamespace.php @@ -28,7 +28,7 @@ * AST-based reflection for the concrete namespace in the file * @see \Go\ParserReflection\ReflectionFileNamespaceTest */ -class ReflectionFileNamespace +class ReflectionFileNamespace implements NodeAwareInterface { /** * List of classes in the namespace diff --git a/src/ReflectionFunction.php b/src/ReflectionFunction.php index 539a9d4..dbb431d 100644 --- a/src/ReflectionFunction.php +++ b/src/ReflectionFunction.php @@ -22,7 +22,7 @@ * AST-based reflection for function * @see \Go\ParserReflection\ReflectionFunctionTest */ -final class ReflectionFunction extends BaseReflectionFunction +final class ReflectionFunction extends BaseReflectionFunction implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use ReflectionFunctionLikeTrait; diff --git a/src/ReflectionMethod.php b/src/ReflectionMethod.php index fe4659b..90381fb 100644 --- a/src/ReflectionMethod.php +++ b/src/ReflectionMethod.php @@ -26,7 +26,7 @@ * AST-based reflection for the method in a class * @see \Go\ParserReflection\ReflectionMethodTest */ -final class ReflectionMethod extends BaseReflectionMethod +final class ReflectionMethod extends BaseReflectionMethod implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use ReflectionFunctionLikeTrait; diff --git a/src/ReflectionParameter.php b/src/ReflectionParameter.php index 53b5dea..51e8b95 100644 --- a/src/ReflectionParameter.php +++ b/src/ReflectionParameter.php @@ -32,7 +32,7 @@ * AST-based reflection for method/function parameter * @see \Go\ParserReflection\ReflectionParameterTest */ -final class ReflectionParameter extends BaseReflectionParameter +final class ReflectionParameter extends BaseReflectionParameter implements NodeAwareInterface { use InternalPropertiesEmulationTrait; use AttributeResolverTrait; diff --git a/src/ReflectionProperty.php b/src/ReflectionProperty.php index 1db37f1..345fa91 100644 --- a/src/ReflectionProperty.php +++ b/src/ReflectionProperty.php @@ -36,7 +36,7 @@ * AST-based reflection for class property * @see \Go\ParserReflection\ReflectionPropertyTest */ -final class ReflectionProperty extends BaseReflectionProperty +final class ReflectionProperty extends BaseReflectionProperty implements NodeAwareInterface { use InitializationTrait; use InternalPropertiesEmulationTrait; diff --git a/tests/NodeAwareInterfaceTest.php b/tests/NodeAwareInterfaceTest.php new file mode 100644 index 0000000..717a493 --- /dev/null +++ b/tests/NodeAwareInterfaceTest.php @@ -0,0 +1,144 @@ +parsedRefFileNamespace = $reflectionFile->getFileNamespace('Go\ParserReflection\Stub'); + + include_once $fileName; + } + + /** + * Asserts that a reflection implements NodeAwareInterface and returns the expected node type + * + * @param class-string $expectedNodeClass + */ + private function assertNodeAware(object $reflection, string $expectedNodeClass): void + { + $this->assertInstanceOf(NodeAwareInterface::class, $reflection); + $this->assertInstanceOf($expectedNodeClass, $reflection->getNode()); + } + + public function testFileNamespaceIsNodeAware(): void + { + $this->assertNodeAware($this->parsedRefFileNamespace, Namespace_::class); + } + + public function testClassIsNodeAware(): void + { + $refClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp81ReadOnlyProperties::class); + $this->assertNodeAware($refClass, ClassLike::class); + } + + public function testEnumIsNodeAware(): void + { + $refEnum = $this->parsedRefFileNamespace->getEnums()[SimplePhp81EnumWithSuit::class]; + $this->assertNodeAware($refEnum, Enum_::class); + } + + public function testEnumUnitCaseIsNodeAware(): void + { + $refEnum = $this->parsedRefFileNamespace->getEnums()[SimplePhp81EnumWithSuit::class]; + $this->assertNodeAware($refEnum->getCase('Clubs'), EnumCase::class); + } + + public function testEnumBackedCaseIsNodeAware(): void + { + $refEnum = $this->parsedRefFileNamespace->getEnums()[BackedPhp81EnumHTTPMethods::class]; + $this->assertNodeAware($refEnum->getCase('GET'), EnumCase::class); + } + + public function testMethodIsNodeAware(): void + { + $refClass = $this->parsedRefFileNamespace->getClass(ClassWithBackedEnumDefaultValue::class); + $refMethod = $refClass->getMethod('getRefusalDescription'); + $this->assertNodeAware($refMethod, ClassMethod::class); + } + + public function testFunctionIsNodeAware(): void + { + $refFunction = $this->parsedRefFileNamespace->getFunction('functionWithPhp81IntersectionType'); + $this->assertNodeAware($refFunction, Function_::class); + } + + public function testParameterIsNodeAware(): void + { + $refClass = $this->parsedRefFileNamespace->getClass(ClassWithBackedEnumDefaultValue::class); + $refMethod = $refClass->getMethod('getRefusalDescription'); + [$refParameter] = $refMethod->getParameters(); + $this->assertNodeAware($refParameter, Param::class); + } + + public function testPropertyIsNodeAware(): void + { + $refClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp81ReadOnlyProperties::class); + $refProperty = $refClass->getProperty('publicReadonlyInt'); + $this->assertNodeAware($refProperty, PropertyItem::class); + } + + public function testClassConstantIsNodeAware(): void + { + $refClass = $this->parsedRefFileNamespace->getClass(ClassWithPhp81FinalClassConst::class); + $refConstant = $refClass->getReflectionConstant('TEST'); + $this->assertNodeAware($refConstant, ClassConst::class); + } + + public function testEnumCaseConstantIsNodeAware(): void + { + $refEnum = $this->parsedRefFileNamespace->getEnums()[SimplePhp81EnumWithSuit::class]; + $refConstant = $refEnum->getReflectionConstant('Clubs'); + $this->assertNodeAware($refConstant, EnumCase::class); + } + + public function testAttributeIsNodeAware(): void + { + $fileName = stream_resolve_include_path(__DIR__ . '/Stub/FileWithFunction80.php'); + $fileNode = ReflectionEngine::parseFile($fileName); + + $reflectionFile = new ReflectionFile($fileName, $fileNode); + $fileNamespace = $reflectionFile->getFileNamespace('Go\ParserReflection\Stub'); + + $refFunction = $fileNamespace->getFunction('function_with_attribute'); + $attributes = $refFunction->getAttributes(); + + $this->assertNotEmpty($attributes); + $this->assertNodeAware($attributes[0], Attribute::class); + } +}