diff --git a/src/Analyser/ClassCollector.php b/src/Analyser/ClassCollector.php index aa5b64d..45d7d68 100644 --- a/src/Analyser/ClassCollector.php +++ b/src/Analyser/ClassCollector.php @@ -90,6 +90,9 @@ final class ClassCollector extends NodeVisitorAbstract /** @var array */ private array $classLikeAnalysis = []; + /** @var array> */ + private array $classLikeMethods = []; + /** @var list */ private array $activeClassLikeAnalyses = []; @@ -111,6 +114,7 @@ public function setCurrentFile(string $file): void $this->fileClassLikes = []; $this->fileFunctions = []; $this->classLikeAnalysis = []; + $this->classLikeMethods = []; $this->activeClassLikeAnalyses = []; $this->activeMethodIds = []; $this->methodClassLikeAnalyses = []; @@ -188,6 +192,7 @@ public function afterTraverse(array $nodes): null $this->fileClassLikes = []; $this->classLikeAnalysis = []; + $this->classLikeMethods = []; $this->activeClassLikeAnalyses = []; $this->activeMethodIds = []; $this->methodClassLikeAnalyses = []; @@ -199,6 +204,7 @@ private function startClassLikeAnalysis(ClassLike $classLike): void { $classLikeId = spl_object_id($classLike); $classLikeAnalysis = new ClassLikeAnalysis(); + $classLikeMethods = []; $classLikeAnalysis->dependencies = $this->currentNamespaceUses; @@ -206,8 +212,13 @@ private function startClassLikeAnalysis(ClassLike $classLike): void $this->activeClassLikeAnalyses[] = $classLikeAnalysis; foreach ($classLike->getMethods() as $classMethod) { - $this->methodClassLikeAnalyses[spl_object_id($classMethod)] = $classLikeAnalysis; + $methodId = spl_object_id($classMethod); + + $classLikeMethods[$methodId] = $classMethod; + $this->methodClassLikeAnalyses[$methodId] = $classLikeAnalysis; } + + $this->classLikeMethods[$classLikeId] = $classLikeMethods; } private function startMethodAnalysis(ClassMethod $classMethod): void @@ -344,16 +355,18 @@ private function addLanguageConstruct(string $languageConstruct): void private function collectClassLike(ClassLike $classLike): void { - $analysis = $this->collectClassLikeAnalysis($classLike); + $classLikeId = spl_object_id($classLike); + $analysis = $this->collectClassLikeAnalysis($classLikeId); $className = $this->resolveClassName($classLike); $layers = $this->layerResolver->resolveAll($className, $this->currentFile); $layer = $this->layerResolver->resolve($className, $this->currentFile); - $methods = $this->collectMethods($classLike, $analysis['complexityByMethodId']); + $classLikeMethods = $this->classLikeMethods[$classLikeId]; + $methods = $this->collectMethods($classLikeMethods, $analysis['complexityByMethodId']); $implements = $this->collectImplements($classLike); $interfaceExtends = $this->collectInterfaceExtends($classLike); $traits = $this->collectTraits($classLike); $constants = $this->collectConstants($classLike); - $properties = $this->collectProperties($classLike); + $properties = $this->collectProperties($classLike, $classLikeMethods); $this->nodes[] = new ClassNode( className: $className, @@ -412,9 +425,10 @@ private function collectConstants(ClassLike $classLike): array } /** + * @param array $classLikeMethods * @return PropertyNode[] */ - private function collectProperties(ClassLike $classLike): array + private function collectProperties(ClassLike $classLike, array $classLikeMethods): array { $properties = []; @@ -436,12 +450,12 @@ private function collectProperties(ClassLike $classLike): array } } - foreach ($classLike->getMethods() as $classMethod) { - if ($classMethod->name->toLowerString() !== '__construct') { + foreach ($classLikeMethods as $classLikeMethod) { + if ($classLikeMethod->name->toLowerString() !== '__construct') { continue; } - foreach ($classMethod->params as $param) { + foreach ($classLikeMethod->params as $param) { if ($param->flags === 0 || ! $param->var instanceof Variable || ! is_string($param->var->name)) { continue; } @@ -477,9 +491,9 @@ private function resolveClassName(ClassLike $classLike): string * complexityByMethodId: array * } */ - private function collectClassLikeAnalysis(ClassLike $classLike): array + private function collectClassLikeAnalysis(int $classLikeId): array { - $analysis = $this->classLikeAnalysis[spl_object_id($classLike)] ?? new ClassLikeAnalysis(); + $analysis = $this->classLikeAnalysis[$classLikeId] ?? new ClassLikeAnalysis(); $functionCalls = []; foreach ($analysis->functionCallNames as $functionCallName) { @@ -592,21 +606,22 @@ private function collectTraits(ClassLike $classLike): array } /** - * @return MethodNode[] + * @param array $classLikeMethods * @param array $complexityByMethodId + * @return MethodNode[] */ - private function collectMethods(ClassLike $classLike, array $complexityByMethodId): array + private function collectMethods(array $classLikeMethods, array $complexityByMethodId): array { $methods = []; - foreach ($classLike->getMethods() as $classMethod) { + foreach ($classLikeMethods as $methodId => $classMethod) { $methods[] = new MethodNode( name: (string) $classMethod->name, visibility: $this->resolveVisibilityFromFlags($classMethod->flags), hasReturnType: $classMethod->returnType !== null, isStatic: $classMethod->isStatic(), paramCount: count($classMethod->params), - cyclomaticComplexity: $complexityByMethodId[spl_object_id($classMethod)] ?? 1, + cyclomaticComplexity: $complexityByMethodId[$methodId] ?? 1, lineCount: $this->calculateMethodLineCount($classMethod), hasExplicitVisibility: $this->hasExplicitVisibilityFlag($classMethod->flags), line: $classMethod->getStartLine(), diff --git a/tests/Analyser/ClassCollectorTest.php b/tests/Analyser/ClassCollectorTest.php index fd3768d..7621a5b 100644 --- a/tests/Analyser/ClassCollectorTest.php +++ b/tests/Analyser/ClassCollectorTest.php @@ -8,6 +8,7 @@ use Boundwize\StructArmed\Analyser\ClassLikeAnalysis; use Boundwize\StructArmed\Analyser\ClassNode; use Boundwize\StructArmed\LayerResolver\Resolvers\NamespaceLayerResolver; +use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\NameResolver; @@ -16,6 +17,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use function array_column; + #[CoversClass(ClassCollector::class)] #[CoversClass(ClassLikeAnalysis::class)] final class ClassCollectorTest extends TestCase @@ -173,6 +176,53 @@ public function testCollectsMethodReturnType(): void $this->assertSame('bar', $classNode->methods[0]->name); } + public function testFiltersClassMethodsOncePerClassLike(): void + { + $namespaceLayerResolver = new NamespaceLayerResolver(['Domain' => 'src/Domain/'], self::BASE_PATH); + $classCollector = new ClassCollector($namespaceLayerResolver); + $classLike = new class ('Foo', [ + 'stmts' => [new ClassMethod('__construct'), new ClassMethod('bar')], + ]) extends Class_ { + public int $getMethodsCallCount = 0; + + public function getMethods(): array + { + ++$this->getMethodsCallCount; + + return parent::getMethods(); + } + }; + + $classCollector->setCurrentFile('/fake/path/Foo.php'); + + (new NodeTraverser(new NameResolver(), $classCollector))->traverse([$classLike]); + + $this->assertSame(1, $classLike->getMethodsCallCount); + $this->assertSame( + ['__construct', 'bar'], + array_column($classCollector->getNodes()[0]->methods, 'name'), + ); + } + + public function testMemoizesMethodsIndependentlyForSiblingClassLikesIncludingEmpty(): void + { + $nodes = $this->collectNodes(<<<'PHP' + assertCount(2, $nodes); + $this->assertSame(['one', 'two'], array_column($nodes[0]->methods, 'name')); + $this->assertSame([], $nodes[1]->methods); + } + public function testCollectsClassConstants(): void { $classNode = $this->collect(