|
15 | 15 | use Closure; |
16 | 16 | use Go\ParserReflection\ReflectionClass; |
17 | 17 | use Go\ParserReflection\ReflectionClassConstant; |
| 18 | +use Go\ParserReflection\ReflectionEngine; |
18 | 19 | use Go\ParserReflection\ReflectionException; |
19 | 20 | use Go\ParserReflection\ReflectionMethod; |
20 | 21 | use Go\ParserReflection\ReflectionProperty; |
21 | 22 | use Go\ParserReflection\Resolver\NodeExpressionResolver; |
| 23 | +use PhpParser\Modifiers; |
| 24 | +use PhpParser\Node\Identifier; |
22 | 25 | use PhpParser\Node\Name; |
23 | 26 | use PhpParser\Node\Name\FullyQualified; |
24 | 27 | use PhpParser\Node\Stmt\Class_; |
25 | 28 | use PhpParser\Node\Stmt\ClassConst; |
26 | 29 | use PhpParser\Node\Stmt\ClassLike; |
| 30 | +use PhpParser\Node\Stmt\ClassMethod; |
27 | 31 | use PhpParser\Node\Stmt\Enum_; |
28 | 32 | use PhpParser\Node\Stmt\Interface_; |
29 | 33 | use PhpParser\Node\Stmt\Trait_; |
@@ -397,19 +401,26 @@ public function getMethods(int|null $filter = null): array |
397 | 401 | { |
398 | 402 | if (!isset($this->methods)) { |
399 | 403 | $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; |
407 | 413 | } |
408 | | - |
409 | | - return $reflectionMethods; |
410 | 414 | } |
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; |
413 | 424 |
|
414 | 425 | $this->methods = $methods; |
415 | 426 | } |
@@ -1065,6 +1076,149 @@ private function recursiveCollect(Closure $collector): array |
1065 | 1076 | return $result; |
1066 | 1077 | } |
1067 | 1078 |
|
| 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 | + |
1068 | 1222 | /** |
1069 | 1223 | * Collects list of constants from the class itself |
1070 | 1224 | */ |
|
0 commit comments