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
17 changes: 14 additions & 3 deletions src/Analyser/ClassCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)) {
Expand Down
122 changes: 122 additions & 0 deletions tests/Analyser/AnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
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;
use function count;
use function dirname;
use function file_put_contents;
use function is_dir;
Expand Down Expand Up @@ -770,6 +772,126 @@ public function __construct(private QueryBuilder $db) {}
$this->assertStringContainsString('Database', $violations[0]->message);
}

/**
* @return iterable<string, array{string, list<string>}>
*/
public static function importedSymbolDependencyProvider(): iterable
{
yield 'constant fetch' => [
<<<'PHP'
<?php

namespace App\HTTP;

use const App\Database\Config\DEFAULT_TIMEOUT;

final class Request
{
public function timeout(): int
{
return DEFAULT_TIMEOUT;
}
}
PHP,
['App\Database\Config\DEFAULT_TIMEOUT'],
];

yield 'function call' => [
<<<'PHP'
<?php

namespace App\HTTP;

use function App\Database\Support\query;

final class Request
{
public function run(): void
{
query();
}
}
PHP,
['App\Database\Support\query'],
];

yield 'grouped constant fetch' => [
<<<'PHP'
<?php

namespace App\HTTP;

use const App\Database\Config\{DEFAULT_TIMEOUT, RETRY_LIMIT};

final class Request
{
public function timeout(): int
{
return DEFAULT_TIMEOUT + RETRY_LIMIT;
}
}
PHP,
[
'App\Database\Config\DEFAULT_TIMEOUT',
'App\Database\Config\RETRY_LIMIT',
],
];

yield 'grouped function call' => [
<<<'PHP'
<?php

namespace App\HTTP;

use function App\Database\Support\{query, trace};

final class Request
{
public function run(): void
{
query();
trace();
}
}
PHP,
[
'App\Database\Support\query',
'App\Database\Support\trace',
],
];
}

/**
* @param list<string> $dependencies
*/
#[DataProvider('importedSymbolDependencyProvider')]
public function testAnalyserRulesetTreatsImportedConstantsAndFunctionsAsDependencies(
string $sourceCode,
array $dependencies
): void {
$basePath = $this->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(count($dependencies), $violations);

foreach ($dependencies as $index => $dependency) {
$this->assertStringContainsString($dependency, $violations[$index]->message);
$this->assertStringContainsString('Database', $violations[$index]->message);
}
}

public function testAnalyserRulesetAllowsListedLayerDependency(): void
{
$basePath = $this->makeTempProject([
Expand Down
104 changes: 104 additions & 0 deletions tests/Analyser/ClassCollectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -424,6 +425,109 @@ class Foo {}
$this->assertContains('App\Domain\Order', $classNode->dependencies);
}

/**
* @return iterable<string, array{string, list<string>}>
*/
public static function groupedImportDependencyProvider(): iterable
{
yield 'class imports' => [
<<<'PHP'
<?php
namespace App\Domain;

use App\Infrastructure\{Bar, Baz};

class Foo
{
public function __construct(private Bar $bar, private Baz $baz)
{
}
}
PHP,
[
'App\Infrastructure\Bar',
'App\Infrastructure\Baz',
],
];

yield 'constant imports' => [
<<<'PHP'
<?php
namespace App\Domain;

use const App\Infrastructure\Config\{FEATURE_ENABLED, OTHER_FLAG};

class Foo
{
public function isEnabled(): bool
{
return FEATURE_ENABLED && OTHER_FLAG;
}
}
PHP,
[
'App\Infrastructure\Config\FEATURE_ENABLED',
'App\Infrastructure\Config\OTHER_FLAG',
],
];

yield 'function imports' => [
<<<'PHP'
<?php
namespace App\Domain;

use function App\Infrastructure\Support\{debug, trace};

class Foo
{
public function run(): void
{
debug();
trace();
}
}
PHP,
[
'App\Infrastructure\Support\debug',
'App\Infrastructure\Support\trace',
],
];
}

/**
* @param list<string> $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'
<?php
namespace App\Domain;

use const App\Infrastructure\Config\FEATURE_ENABLED;

class Foo
{
public function isEnabled(): bool
{
return FEATURE_ENABLED;
}
}
PHP;
$classNode = $this->collect($code);

$this->assertContains('App\Infrastructure\Config\FEATURE_ENABLED', $classNode->dependencies);
}

public function testCollectsFullyQualifiedDependencies(): void
{
$classNode = $this->collect('<?php class Foo { public function bar(): void { new \DateTimeImmutable(); } }');
Expand Down
Loading