From 2e0f60d17c17f1b6567d144f75dd282b883ad108 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 26 May 2026 21:02:35 +0700 Subject: [PATCH 1/3] Fix grouped import dependency collection --- src/Analyser/ClassCollector.php | 17 ++++- tests/Analyser/AnalyserTest.php | 70 +++++++++++++++++ tests/Analyser/ClassCollectorTest.php | 104 ++++++++++++++++++++++++++ 3 files changed, 188 insertions(+), 3 deletions(-) diff --git a/src/Analyser/ClassCollector.php b/src/Analyser/ClassCollector.php index 40f46289..c8d7529e 100644 --- a/src/Analyser/ClassCollector.php +++ b/src/Analyser/ClassCollector.php @@ -30,13 +30,14 @@ use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\Function_; +use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\Use_; use PhpParser\Node\Stmt\While_; -use PhpParser\Node\UseItem; use PhpParser\NodeTraverser; use PhpParser\NodeVisitorAbstract; @@ -99,8 +100,18 @@ public function getNodes(): array public function enterNode(Node $node): null { - if ($node instanceof UseItem) { - $this->fileUses[] = $node->name->toString(); + if ($node instanceof Use_) { + foreach ($node->uses as $use) { + $this->fileUses[] = $use->name->toString(); + } + } + + if ($node instanceof GroupUse) { + $prefix = $node->prefix->toString(); + + foreach ($node->uses as $use) { + $this->fileUses[] = $prefix . '\\' . $use->name->toString(); + } } if ($node instanceof Function_ && isset($node->namespacedName)) { diff --git a/tests/Analyser/AnalyserTest.php b/tests/Analyser/AnalyserTest.php index 4c1d2caf..3fd91b74 100644 --- a/tests/Analyser/AnalyserTest.php +++ b/tests/Analyser/AnalyserTest.php @@ -17,6 +17,7 @@ use Boundwize\StructArmed\Rule\RuleViolation; use Boundwize\StructArmed\Tests\Support\TemporaryDirectoryCleanupTrait; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use function array_map; @@ -770,6 +771,75 @@ public function __construct(private QueryBuilder $db) {} $this->assertStringContainsString('Database', $violations[0]->message); } + /** + * @return iterable + */ + public static function importedSymbolDependencyProvider(): iterable + { + yield 'constant fetch' => [ + <<<'PHP' + [ + <<<'PHP' + makeTempProject([ + 'src/HTTP/Request.php' => $sourceCode, + ]); + + $architecture = Architecture::define() + ->layerPattern('HTTP', '/^App\\\\HTTP\\\\.*$/') + ->layerPattern('Database', '/^App\\\\Database\\\\.*$/') + ->ruleset([ + 'HTTP' => [], + ]); + + $ruleViolationCollection = (new Analyser($basePath))->analyse($architecture, ['src/']); + + $violations = $ruleViolationCollection->forRule('ruleset.HTTP'); + + $this->assertCount(1, $violations); + $this->assertStringContainsString($dependency, $violations[0]->message); + $this->assertStringContainsString('Database', $violations[0]->message); + } + public function testAnalyserRulesetAllowsListedLayerDependency(): void { $basePath = $this->makeTempProject([ diff --git a/tests/Analyser/ClassCollectorTest.php b/tests/Analyser/ClassCollectorTest.php index 471f1182..bec50e55 100644 --- a/tests/Analyser/ClassCollectorTest.php +++ b/tests/Analyser/ClassCollectorTest.php @@ -11,6 +11,7 @@ use PhpParser\NodeVisitor\NameResolver; use PhpParser\ParserFactory; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use function getcwd; @@ -424,6 +425,109 @@ class Foo {} $this->assertContains('App\Domain\Order', $classNode->dependencies); } + /** + * @return iterable}> + */ + public static function groupedImportDependencyProvider(): iterable + { + yield 'class imports' => [ + <<<'PHP' + [ + <<<'PHP' + [ + <<<'PHP' + $expectedDependencies + */ + #[DataProvider('groupedImportDependencyProvider')] + public function testCollectsGroupedImportedUsageAsDependenciesWithoutShortNames( + string $code, + array $expectedDependencies + ): void { + $classNode = $this->collect($code); + + $this->assertSame($expectedDependencies, $classNode->dependencies); + } + + public function testCollectsImportedConstantUsageAsDependency(): void + { + $code = <<<'PHP' +collect($code); + + $this->assertContains('App\Infrastructure\Config\FEATURE_ENABLED', $classNode->dependencies); + } + public function testCollectsFullyQualifiedDependencies(): void { $classNode = $this->collect(' Date: Tue, 26 May 2026 21:03:48 +0700 Subject: [PATCH 2/3] more tests --- tests/Analyser/AnalyserTest.php | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Analyser/AnalyserTest.php b/tests/Analyser/AnalyserTest.php index 3fd91b74..cbf2673c 100644 --- a/tests/Analyser/AnalyserTest.php +++ b/tests/Analyser/AnalyserTest.php @@ -813,6 +813,45 @@ public function run(): void PHP, 'App\Database\Support\query', ]; + + yield 'grouped constant fetch' => [ + <<<'PHP' + [ + <<<'PHP' + Date: Tue, 26 May 2026 21:07:55 +0700 Subject: [PATCH 3/3] fix --- tests/Analyser/AnalyserTest.php | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/Analyser/AnalyserTest.php b/tests/Analyser/AnalyserTest.php index cbf2673c..9403640d 100644 --- a/tests/Analyser/AnalyserTest.php +++ b/tests/Analyser/AnalyserTest.php @@ -21,6 +21,7 @@ use PHPUnit\Framework\TestCase; use function array_map; +use function count; use function dirname; use function file_put_contents; use function is_dir; @@ -772,7 +773,7 @@ public function __construct(private QueryBuilder $db) {} } /** - * @return iterable + * @return iterable}> */ public static function importedSymbolDependencyProvider(): iterable { @@ -792,7 +793,7 @@ public function timeout(): int } } PHP, - 'App\Database\Config\DEFAULT_TIMEOUT', + ['App\Database\Config\DEFAULT_TIMEOUT'], ]; yield 'function call' => [ @@ -811,7 +812,7 @@ public function run(): void } } PHP, - 'App\Database\Support\query', + ['App\Database\Support\query'], ]; yield 'grouped constant fetch' => [ @@ -830,7 +831,10 @@ public function timeout(): int } } PHP, - 'App\Database\Config\DEFAULT_TIMEOUT', + [ + 'App\Database\Config\DEFAULT_TIMEOUT', + 'App\Database\Config\RETRY_LIMIT', + ], ]; yield 'grouped function call' => [ @@ -850,14 +854,20 @@ public function run(): void } } PHP, - 'App\Database\Support\query', + [ + 'App\Database\Support\query', + 'App\Database\Support\trace', + ], ]; } + /** + * @param list $dependencies + */ #[DataProvider('importedSymbolDependencyProvider')] public function testAnalyserRulesetTreatsImportedConstantsAndFunctionsAsDependencies( string $sourceCode, - string $dependency + array $dependencies ): void { $basePath = $this->makeTempProject([ 'src/HTTP/Request.php' => $sourceCode, @@ -874,9 +884,12 @@ public function testAnalyserRulesetTreatsImportedConstantsAndFunctionsAsDependen $violations = $ruleViolationCollection->forRule('ruleset.HTTP'); - $this->assertCount(1, $violations); - $this->assertStringContainsString($dependency, $violations[0]->message); - $this->assertStringContainsString('Database', $violations[0]->message); + $this->assertCount(count($dependencies), $violations); + + foreach ($dependencies as $index => $dependency) { + $this->assertStringContainsString($dependency, $violations[$index]->message); + $this->assertStringContainsString('Database', $violations[$index]->message); + } } public function testAnalyserRulesetAllowsListedLayerDependency(): void