Skip to content

Commit a2f518b

Browse files
authored
Merge pull request #177 from goaop/copilot/add-trait-support-in-reflection
[Feature] Implement complete trait adaptation support (insteadof + alias) in getMethods()
2 parents 348ce66 + 3841984 commit a2f518b

4 files changed

Lines changed: 172 additions & 26 deletions

File tree

src/ReflectionClass.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public static function collectTraitsFromClassNode(ClassLike $classLikeNode, arra
134134
$traits[$traitName] = $trait;
135135
}
136136
}
137-
$traitAdaptations = $classLevelNode->adaptations;
137+
$traitAdaptations = array_merge($traitAdaptations, $classLevelNode->adaptations);
138138
}
139139
}
140140
}

src/Traits/ReflectionClassLikeTrait.php

Lines changed: 165 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@
1515
use Closure;
1616
use Go\ParserReflection\ReflectionClass;
1717
use Go\ParserReflection\ReflectionClassConstant;
18+
use Go\ParserReflection\ReflectionEngine;
1819
use Go\ParserReflection\ReflectionException;
1920
use Go\ParserReflection\ReflectionMethod;
2021
use Go\ParserReflection\ReflectionProperty;
2122
use Go\ParserReflection\Resolver\NodeExpressionResolver;
23+
use PhpParser\Modifiers;
24+
use PhpParser\Node\Identifier;
2225
use PhpParser\Node\Name;
2326
use PhpParser\Node\Name\FullyQualified;
2427
use PhpParser\Node\Stmt\Class_;
2528
use PhpParser\Node\Stmt\ClassConst;
2629
use PhpParser\Node\Stmt\ClassLike;
30+
use PhpParser\Node\Stmt\ClassMethod;
2731
use PhpParser\Node\Stmt\Enum_;
2832
use PhpParser\Node\Stmt\Interface_;
2933
use PhpParser\Node\Stmt\Trait_;
@@ -397,19 +401,26 @@ public function getMethods(int|null $filter = null): array
397401
{
398402
if (!isset($this->methods)) {
399403
$directMethods = ReflectionMethod::collectFromClassNode($this->classLikeNode, $this);
400-
$parentMethods = $this->recursiveCollect(
401-
function (\ReflectionClass $instance, bool $isParent): array {
402-
$reflectionMethods = [];
403-
foreach ($instance->getMethods() as $reflectionMethod) {
404-
if (!$isParent || !$reflectionMethod->isPrivate()) {
405-
$reflectionMethods[$reflectionMethod->name] = $reflectionMethod;
406-
}
404+
$traitMethods = $this->collectTraitMethods();
405+
406+
// Collect from parent class and interfaces only (traits are handled by collectTraitMethods)
407+
$inheritedMethods = [];
408+
$parentClass = $this->getParentClass();
409+
if ($parentClass) {
410+
foreach ($parentClass->getMethods() as $reflectionMethod) {
411+
if (!$reflectionMethod->isPrivate()) {
412+
$inheritedMethods[$reflectionMethod->name] = $reflectionMethod;
407413
}
408-
409-
return $reflectionMethods;
410414
}
411-
);
412-
$methods = $directMethods + $parentMethods;
415+
}
416+
$interfaces = ReflectionClass::collectInterfacesFromClassNode($this->classLikeNode);
417+
foreach ($interfaces as $interface) {
418+
foreach ($interface->getMethods() as $reflectionMethod) {
419+
$inheritedMethods[$reflectionMethod->name] = $reflectionMethod;
420+
}
421+
}
422+
423+
$methods = $directMethods + $traitMethods + $inheritedMethods;
413424

414425
$this->methods = $methods;
415426
}
@@ -1065,6 +1076,149 @@ private function recursiveCollect(Closure $collector): array
10651076
return $result;
10661077
}
10671078

1079+
/**
1080+
* Collects methods from all used traits, applying insteadof and alias adaptations.
1081+
*
1082+
* @return array<string, \ReflectionMethod>
1083+
*/
1084+
private function collectTraitMethods(): array
1085+
{
1086+
$this->getTraits(); // Ensure traits and traitAdaptations are initialized
1087+
$traits = $this->traits ?? [];
1088+
1089+
if (empty($traits)) {
1090+
return [];
1091+
}
1092+
1093+
// The class that uses the traits — used as $className in ReflectionMethod so that the
1094+
// `class` property (and __debugInfo) match native PHP behaviour.
1095+
$usingClassName = $this->getName();
1096+
1097+
// Parse each trait's AST and build a map of ClassMethod nodes per trait.
1098+
// Also keep a ReflectionClass for the trait (used as $declaringClass).
1099+
/** @var array<string, array<string, ClassMethod>> $traitClassMethodNodes */
1100+
$traitClassMethodNodes = [];
1101+
/** @var array<string, ReflectionClass> $traitReflections */
1102+
$traitReflections = [];
1103+
1104+
foreach ($traits as $traitName => $existingReflection) {
1105+
$traitClassNode = ReflectionEngine::parseClass($traitName);
1106+
// Reuse the existing ReflectionClass if it's our AST-based implementation;
1107+
// otherwise (when the trait was already loaded and a native instance was stored)
1108+
// create a new AST-based ReflectionClass for use as $declaringClass.
1109+
$traitReflections[$traitName] = $existingReflection instanceof ReflectionClass
1110+
? $existingReflection
1111+
: new ReflectionClass($traitName, $traitClassNode);
1112+
$methodNodes = [];
1113+
foreach ($traitClassNode->stmts as $stmt) {
1114+
if ($stmt instanceof ClassMethod) {
1115+
// Mirror what collectFromClassNode does: propagate the file name
1116+
$stmt->setAttribute('fileName', $traitClassNode->getAttribute('fileName'));
1117+
$methodNodes[$stmt->name->toString()] = $stmt;
1118+
}
1119+
}
1120+
$traitClassMethodNodes[$traitName] = $methodNodes;
1121+
}
1122+
1123+
// Build exclusion map from Precedence (insteadof) adaptations:
1124+
// $excluded[traitFQN][methodName] = true means that method from that trait is excluded
1125+
$excluded = [];
1126+
foreach ($this->traitAdaptations as $adaptation) {
1127+
if ($adaptation instanceof TraitUseAdaptation\Precedence) {
1128+
$methodName = $adaptation->method->toString();
1129+
foreach ($adaptation->insteadof as $excludedTraitNameNode) {
1130+
$resolvedName = $excludedTraitNameNode->getAttribute('resolvedName');
1131+
$excludedFQN = $resolvedName instanceof FullyQualified
1132+
? $resolvedName->toString()
1133+
: $excludedTraitNameNode->toString();
1134+
$excluded[$excludedFQN][$methodName] = true;
1135+
}
1136+
}
1137+
}
1138+
1139+
// Collect trait methods respecting insteadof: first non-excluded method wins
1140+
$traitMethods = [];
1141+
foreach ($traitClassMethodNodes as $traitName => $methodNodes) {
1142+
foreach ($methodNodes as $methodName => $methodNode) {
1143+
if (isset($excluded[$traitName][$methodName])) {
1144+
continue; // Excluded by insteadof
1145+
}
1146+
if (isset($traitMethods[$methodName])) {
1147+
continue; // Already added from an earlier trait
1148+
}
1149+
$traitMethods[$methodName] = new ReflectionMethod(
1150+
$usingClassName,
1151+
$methodName,
1152+
$methodNode,
1153+
$traitReflections[$traitName]
1154+
);
1155+
}
1156+
}
1157+
1158+
// Apply Alias adaptations: add methods with new names and/or changed visibility
1159+
foreach ($this->traitAdaptations as $adaptation) {
1160+
if (!($adaptation instanceof TraitUseAdaptation\Alias)) {
1161+
continue;
1162+
}
1163+
1164+
$originalMethodName = $adaptation->method->toString();
1165+
$newName = $adaptation->newName !== null ? $adaptation->newName->toString() : null;
1166+
$newModifier = $adaptation->newModifier;
1167+
1168+
// Find the ClassMethod node for the original method
1169+
$originalMethodNode = null;
1170+
$declaringTraitName = null;
1171+
1172+
if ($adaptation->trait !== null) {
1173+
// Specific trait referenced — resolve to FQCN
1174+
$resolvedName = $adaptation->trait->getAttribute('resolvedName');
1175+
$traitFQN = $resolvedName instanceof FullyQualified
1176+
? $resolvedName->toString()
1177+
: $adaptation->trait->toString();
1178+
1179+
if (isset($traitClassMethodNodes[$traitFQN][$originalMethodName])) {
1180+
$originalMethodNode = $traitClassMethodNodes[$traitFQN][$originalMethodName];
1181+
$declaringTraitName = $traitFQN;
1182+
}
1183+
} else {
1184+
// No specific trait — search all traits in declaration order
1185+
foreach ($traitClassMethodNodes as $traitFQN => $methodNodes) {
1186+
if (isset($methodNodes[$originalMethodName])) {
1187+
$originalMethodNode = $methodNodes[$originalMethodName];
1188+
$declaringTraitName = $traitFQN;
1189+
break;
1190+
}
1191+
}
1192+
}
1193+
1194+
if ($originalMethodNode === null || $declaringTraitName === null) {
1195+
continue;
1196+
}
1197+
1198+
// Clone the AST node and apply name/visibility changes
1199+
$aliasMethodNode = clone $originalMethodNode;
1200+
$targetMethodName = $newName ?? $originalMethodName;
1201+
1202+
if ($newName !== null) {
1203+
$aliasMethodNode->name = new Identifier($newName);
1204+
}
1205+
if ($newModifier !== null) {
1206+
// Clear existing visibility bits and apply the new modifier
1207+
$aliasMethodNode->flags =
1208+
($aliasMethodNode->flags & ~Modifiers::VISIBILITY_MASK) | $newModifier;
1209+
}
1210+
1211+
$traitMethods[$targetMethodName] = new ReflectionMethod(
1212+
$usingClassName,
1213+
$targetMethodName,
1214+
$aliasMethodNode,
1215+
$traitReflections[$declaringTraitName]
1216+
);
1217+
}
1218+
1219+
return $traitMethods;
1220+
}
1221+
10681222
/**
10691223
* Collects list of constants from the class itself
10701224
*/

tests/ReflectionClassTest.php

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,6 @@ public function testGetMethodCount(
110110
): void {
111111
$parsedMethods = $parsedRefClass->getMethods();
112112
$originalMethods = $originalRefClass->getMethods();
113-
if ($parsedRefClass->getTraitAliases()) {
114-
$this->markTestIncomplete("Adoptation methods for traits are not supported yet");
115-
}
116113
$this->assertCount(count($originalMethods), $parsedMethods);
117114
}
118115

tests/Stub/FileWithClasses55.php

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,26 +182,21 @@ class ClassWithPhp54Trait
182182
use SimplePhp54Trait;
183183
}
184184

185-
/*
186-
* Current implementation doesn't support trait adaptation,
187-
* @see https://github.com/goaop/parser-reflection/issues/54
188-
*
189-
class ClassWithTraitAndAdaptation
185+
class ClassWithPhp54TraitAndAdaptation
190186
{
191-
use SimpleTrait {
187+
use SimplePhp54Trait {
192188
foo as protected fooBar;
193189
foo as private fooBaz;
194190
}
195191
}
196192

197-
class ClassWithTraitAndConflict
193+
class ClassWithPhp54TraitAndConflict
198194
{
199-
use SimpleTrait, ConflictedSimpleTrait {
200-
foo as protected fooBar;
201-
ConflictedSimpleTrait::foo insteadof SimpleTrait;
195+
use SimplePhp54Trait, SimplePhp54ConflictedTrait {
196+
SimplePhp54Trait::foo as protected fooBar;
197+
SimplePhp54ConflictedTrait::foo insteadof SimplePhp54Trait;
202198
}
203199
}
204-
*/
205200

206201
/*
207202
* Logic of prototype methods for interface and traits was changed since 7.0.6

0 commit comments

Comments
 (0)