Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 29 additions & 14 deletions src/Analyser/ClassCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ final class ClassCollector extends NodeVisitorAbstract
/** @var array<int, ClassLikeAnalysis> */
private array $classLikeAnalysis = [];

/** @var array<int, array<int, ClassMethod>> */
private array $classLikeMethods = [];

/** @var list<ClassLikeAnalysis> */
private array $activeClassLikeAnalyses = [];

Expand All @@ -111,6 +114,7 @@ public function setCurrentFile(string $file): void
$this->fileClassLikes = [];
$this->fileFunctions = [];
$this->classLikeAnalysis = [];
$this->classLikeMethods = [];
$this->activeClassLikeAnalyses = [];
$this->activeMethodIds = [];
$this->methodClassLikeAnalyses = [];
Expand Down Expand Up @@ -188,6 +192,7 @@ public function afterTraverse(array $nodes): null

$this->fileClassLikes = [];
$this->classLikeAnalysis = [];
$this->classLikeMethods = [];
$this->activeClassLikeAnalyses = [];
$this->activeMethodIds = [];
$this->methodClassLikeAnalyses = [];
Expand All @@ -199,15 +204,21 @@ private function startClassLikeAnalysis(ClassLike $classLike): void
{
$classLikeId = spl_object_id($classLike);
$classLikeAnalysis = new ClassLikeAnalysis();
$classLikeMethods = [];

$classLikeAnalysis->dependencies = $this->currentNamespaceUses;

$this->classLikeAnalysis[$classLikeId] = $classLikeAnalysis;
$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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -412,9 +425,10 @@ private function collectConstants(ClassLike $classLike): array
}

/**
* @param array<int, ClassMethod> $classLikeMethods
* @return PropertyNode[]
*/
private function collectProperties(ClassLike $classLike): array
private function collectProperties(ClassLike $classLike, array $classLikeMethods): array
{
$properties = [];

Expand All @@ -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;
}
Expand Down Expand Up @@ -477,9 +491,9 @@ private function resolveClassName(ClassLike $classLike): string
* complexityByMethodId: array<int, int>
* }
*/
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) {
Expand Down Expand Up @@ -592,21 +606,22 @@ private function collectTraits(ClassLike $classLike): array
}

/**
* @return MethodNode[]
* @param array<int, ClassMethod> $classLikeMethods
* @param array<int, int> $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(),
Expand Down
50 changes: 50 additions & 0 deletions tests/Analyser/ClassCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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'
<?php

class First
{
public function one(): void {}
public function two(): void {}
}

class Second {}
PHP);

$this->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(
Expand Down
Loading