diff --git a/src/ReflectionClass.php b/src/ReflectionClass.php index 930acb4..069333d 100644 --- a/src/ReflectionClass.php +++ b/src/ReflectionClass.php @@ -134,7 +134,7 @@ public static function collectTraitsFromClassNode(ClassLike $classLikeNode, arra $traits[$traitName] = $trait; } } - $traitAdaptations = $classLevelNode->adaptations; + $traitAdaptations = array_merge($traitAdaptations, $classLevelNode->adaptations); } } } diff --git a/src/Traits/ReflectionClassLikeTrait.php b/src/Traits/ReflectionClassLikeTrait.php index 09e3870..d1e6ac5 100644 --- a/src/Traits/ReflectionClassLikeTrait.php +++ b/src/Traits/ReflectionClassLikeTrait.php @@ -15,15 +15,19 @@ use Closure; use Go\ParserReflection\ReflectionClass; use Go\ParserReflection\ReflectionClassConstant; +use Go\ParserReflection\ReflectionEngine; use Go\ParserReflection\ReflectionException; use Go\ParserReflection\ReflectionMethod; use Go\ParserReflection\ReflectionProperty; use Go\ParserReflection\Resolver\NodeExpressionResolver; +use PhpParser\Modifiers; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; use PhpParser\Node\Stmt\ClassLike; +use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Trait_; @@ -397,19 +401,26 @@ public function getMethods(int|null $filter = null): array { if (!isset($this->methods)) { $directMethods = ReflectionMethod::collectFromClassNode($this->classLikeNode, $this); - $parentMethods = $this->recursiveCollect( - function (\ReflectionClass $instance, bool $isParent): array { - $reflectionMethods = []; - foreach ($instance->getMethods() as $reflectionMethod) { - if (!$isParent || !$reflectionMethod->isPrivate()) { - $reflectionMethods[$reflectionMethod->name] = $reflectionMethod; - } + $traitMethods = $this->collectTraitMethods(); + + // Collect from parent class and interfaces only (traits are handled by collectTraitMethods) + $inheritedMethods = []; + $parentClass = $this->getParentClass(); + if ($parentClass) { + foreach ($parentClass->getMethods() as $reflectionMethod) { + if (!$reflectionMethod->isPrivate()) { + $inheritedMethods[$reflectionMethod->name] = $reflectionMethod; } - - return $reflectionMethods; } - ); - $methods = $directMethods + $parentMethods; + } + $interfaces = ReflectionClass::collectInterfacesFromClassNode($this->classLikeNode); + foreach ($interfaces as $interface) { + foreach ($interface->getMethods() as $reflectionMethod) { + $inheritedMethods[$reflectionMethod->name] = $reflectionMethod; + } + } + + $methods = $directMethods + $traitMethods + $inheritedMethods; $this->methods = $methods; } @@ -1065,6 +1076,149 @@ private function recursiveCollect(Closure $collector): array return $result; } + /** + * Collects methods from all used traits, applying insteadof and alias adaptations. + * + * @return array + */ + private function collectTraitMethods(): array + { + $this->getTraits(); // Ensure traits and traitAdaptations are initialized + $traits = $this->traits ?? []; + + if (empty($traits)) { + return []; + } + + // The class that uses the traits — used as $className in ReflectionMethod so that the + // `class` property (and __debugInfo) match native PHP behaviour. + $usingClassName = $this->getName(); + + // Parse each trait's AST and build a map of ClassMethod nodes per trait. + // Also keep a ReflectionClass for the trait (used as $declaringClass). + /** @var array> $traitClassMethodNodes */ + $traitClassMethodNodes = []; + /** @var array $traitReflections */ + $traitReflections = []; + + foreach ($traits as $traitName => $existingReflection) { + $traitClassNode = ReflectionEngine::parseClass($traitName); + // Reuse the existing ReflectionClass if it's our AST-based implementation; + // otherwise (when the trait was already loaded and a native instance was stored) + // create a new AST-based ReflectionClass for use as $declaringClass. + $traitReflections[$traitName] = $existingReflection instanceof ReflectionClass + ? $existingReflection + : new ReflectionClass($traitName, $traitClassNode); + $methodNodes = []; + foreach ($traitClassNode->stmts as $stmt) { + if ($stmt instanceof ClassMethod) { + // Mirror what collectFromClassNode does: propagate the file name + $stmt->setAttribute('fileName', $traitClassNode->getAttribute('fileName')); + $methodNodes[$stmt->name->toString()] = $stmt; + } + } + $traitClassMethodNodes[$traitName] = $methodNodes; + } + + // Build exclusion map from Precedence (insteadof) adaptations: + // $excluded[traitFQN][methodName] = true means that method from that trait is excluded + $excluded = []; + foreach ($this->traitAdaptations as $adaptation) { + if ($adaptation instanceof TraitUseAdaptation\Precedence) { + $methodName = $adaptation->method->toString(); + foreach ($adaptation->insteadof as $excludedTraitNameNode) { + $resolvedName = $excludedTraitNameNode->getAttribute('resolvedName'); + $excludedFQN = $resolvedName instanceof FullyQualified + ? $resolvedName->toString() + : $excludedTraitNameNode->toString(); + $excluded[$excludedFQN][$methodName] = true; + } + } + } + + // Collect trait methods respecting insteadof: first non-excluded method wins + $traitMethods = []; + foreach ($traitClassMethodNodes as $traitName => $methodNodes) { + foreach ($methodNodes as $methodName => $methodNode) { + if (isset($excluded[$traitName][$methodName])) { + continue; // Excluded by insteadof + } + if (isset($traitMethods[$methodName])) { + continue; // Already added from an earlier trait + } + $traitMethods[$methodName] = new ReflectionMethod( + $usingClassName, + $methodName, + $methodNode, + $traitReflections[$traitName] + ); + } + } + + // Apply Alias adaptations: add methods with new names and/or changed visibility + foreach ($this->traitAdaptations as $adaptation) { + if (!($adaptation instanceof TraitUseAdaptation\Alias)) { + continue; + } + + $originalMethodName = $adaptation->method->toString(); + $newName = $adaptation->newName !== null ? $adaptation->newName->toString() : null; + $newModifier = $adaptation->newModifier; + + // Find the ClassMethod node for the original method + $originalMethodNode = null; + $declaringTraitName = null; + + if ($adaptation->trait !== null) { + // Specific trait referenced — resolve to FQCN + $resolvedName = $adaptation->trait->getAttribute('resolvedName'); + $traitFQN = $resolvedName instanceof FullyQualified + ? $resolvedName->toString() + : $adaptation->trait->toString(); + + if (isset($traitClassMethodNodes[$traitFQN][$originalMethodName])) { + $originalMethodNode = $traitClassMethodNodes[$traitFQN][$originalMethodName]; + $declaringTraitName = $traitFQN; + } + } else { + // No specific trait — search all traits in declaration order + foreach ($traitClassMethodNodes as $traitFQN => $methodNodes) { + if (isset($methodNodes[$originalMethodName])) { + $originalMethodNode = $methodNodes[$originalMethodName]; + $declaringTraitName = $traitFQN; + break; + } + } + } + + if ($originalMethodNode === null || $declaringTraitName === null) { + continue; + } + + // Clone the AST node and apply name/visibility changes + $aliasMethodNode = clone $originalMethodNode; + $targetMethodName = $newName ?? $originalMethodName; + + if ($newName !== null) { + $aliasMethodNode->name = new Identifier($newName); + } + if ($newModifier !== null) { + // Clear existing visibility bits and apply the new modifier + $aliasMethodNode->flags = + ($aliasMethodNode->flags & ~Modifiers::VISIBILITY_MASK) | $newModifier; + } + + $traitMethods[$targetMethodName] = new ReflectionMethod( + $usingClassName, + $targetMethodName, + $aliasMethodNode, + $traitReflections[$declaringTraitName] + ); + } + + return $traitMethods; + } + /** * Collects list of constants from the class itself */ diff --git a/tests/ReflectionClassTest.php b/tests/ReflectionClassTest.php index b320453..d3a8700 100644 --- a/tests/ReflectionClassTest.php +++ b/tests/ReflectionClassTest.php @@ -110,9 +110,6 @@ public function testGetMethodCount( ): void { $parsedMethods = $parsedRefClass->getMethods(); $originalMethods = $originalRefClass->getMethods(); - if ($parsedRefClass->getTraitAliases()) { - $this->markTestIncomplete("Adoptation methods for traits are not supported yet"); - } $this->assertCount(count($originalMethods), $parsedMethods); } diff --git a/tests/Stub/FileWithClasses55.php b/tests/Stub/FileWithClasses55.php index 827bc9f..84a288c 100644 --- a/tests/Stub/FileWithClasses55.php +++ b/tests/Stub/FileWithClasses55.php @@ -182,26 +182,21 @@ class ClassWithPhp54Trait use SimplePhp54Trait; } -/* - * Current implementation doesn't support trait adaptation, - * @see https://github.com/goaop/parser-reflection/issues/54 - * -class ClassWithTraitAndAdaptation +class ClassWithPhp54TraitAndAdaptation { - use SimpleTrait { + use SimplePhp54Trait { foo as protected fooBar; foo as private fooBaz; } } -class ClassWithTraitAndConflict +class ClassWithPhp54TraitAndConflict { - use SimpleTrait, ConflictedSimpleTrait { - foo as protected fooBar; - ConflictedSimpleTrait::foo insteadof SimpleTrait; + use SimplePhp54Trait, SimplePhp54ConflictedTrait { + SimplePhp54Trait::foo as protected fooBar; + SimplePhp54ConflictedTrait::foo insteadof SimplePhp54Trait; } } -*/ /* * Logic of prototype methods for interface and traits was changed since 7.0.6