From 35dd520ff38609f42a663c51d825819ec7d427eb Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 31 Aug 2026 22:41:44 +0700 Subject: [PATCH 1/3] Fix: normalise reported composer.json path in PSR-4 rules for consistent violation display on Windows --- src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php | 2 +- .../Rules/Composer/Psr4EmptyNamespacePrefixRule.php | 10 +++------- src/Rule/Rules/Composer/Psr4RootPathRule.php | 9 ++------- src/Rule/Rules/Composer/Psr4SourcePathsRule.php | 2 +- 4 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php b/src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php index 900c0944..633cdca0 100644 --- a/src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php +++ b/src/Rule/Rules/Composer/Psr4DirectoryExistsRule.php @@ -28,7 +28,7 @@ public function __construct( public function evaluateProject(string $basePath, Architecture $architecture, array $skipPaths = []): ?RuleViolation { - $composerFile = rtrim($basePath, '/') . '/composer.json'; + $composerFile = Path::normalise(rtrim($basePath, '/') . '/composer.json', canonicalise: true); if (! file_exists($composerFile)) { return $this->violation( diff --git a/src/Rule/Rules/Composer/Psr4EmptyNamespacePrefixRule.php b/src/Rule/Rules/Composer/Psr4EmptyNamespacePrefixRule.php index 58424005..e8194d04 100644 --- a/src/Rule/Rules/Composer/Psr4EmptyNamespacePrefixRule.php +++ b/src/Rule/Rules/Composer/Psr4EmptyNamespacePrefixRule.php @@ -9,9 +9,9 @@ use Boundwize\StructArmed\Rule\ComposerJsonRuleInterface; use Boundwize\StructArmed\Rule\MultipleProjectRuleViolationInterface; use Boundwize\StructArmed\Rule\RuleViolation; +use Boundwize\StructArmed\Util\Path; use function array_keys; -use function file_exists; use function is_array; use function is_string; use function rtrim; @@ -38,18 +38,14 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar */ public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array { - $composerFile = rtrim($basePath, '/') . '/composer.json'; - - if (! file_exists($composerFile)) { - return []; - } - $composer = $this->psr4PathResolver->composerConfig($basePath); if ($composer === null) { return []; } + $composerFile = Path::normalise(rtrim($basePath, '/') . '/composer.json', canonicalise: true); + $violations = []; foreach (['autoload', 'autoload-dev'] as $section) { diff --git a/src/Rule/Rules/Composer/Psr4RootPathRule.php b/src/Rule/Rules/Composer/Psr4RootPathRule.php index 5fb93676..4b131441 100644 --- a/src/Rule/Rules/Composer/Psr4RootPathRule.php +++ b/src/Rule/Rules/Composer/Psr4RootPathRule.php @@ -11,7 +11,6 @@ use Boundwize\StructArmed\Rule\RuleViolation; use Boundwize\StructArmed\Util\Path; -use function file_exists; use function is_array; use function is_string; use function rtrim; @@ -36,18 +35,14 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar */ public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array { - $composerFile = rtrim($basePath, '/') . '/composer.json'; - - if (! file_exists($composerFile)) { - return []; - } - $composer = $this->psr4PathResolver->composerConfig($basePath); if ($composer === null) { return []; } + $composerFile = Path::normalise(rtrim($basePath, '/') . '/composer.json', canonicalise: true); + $violations = []; $normalisedBasePath = Path::normalise($basePath, canonicalise: true); diff --git a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php index 8f8b2fe2..8f84fce5 100644 --- a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php +++ b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php @@ -34,7 +34,7 @@ public function __construct( public function evaluateProject(string $basePath, Architecture $architecture, array $skipPaths = []): ?RuleViolation { - $composerFile = rtrim($basePath, '/') . '/composer.json'; + $composerFile = Path::normalise(rtrim($basePath, '/') . '/composer.json', canonicalise: true); if (! file_exists($composerFile)) { return $this->violation( From 31ab0b8e2df4d28c6ffc05b010ac63be090d261c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 31 Aug 2026 22:41:51 +0700 Subject: [PATCH 2/3] add test --- .../Psr4ComposerFilePathNormalisationTest.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php diff --git a/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php b/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php new file mode 100644 index 00000000..19f6bc8f --- /dev/null +++ b/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php @@ -0,0 +1,107 @@ +evaluateProject( + self::WINDOWS_STYLE_MISSING_BASE_PATH, + Architecture::define() + ); + + $this->assertInstanceOf(RuleViolation::class, $violation); + $this->assertSame('C:/structarmed-missing-fixture/app/composer.json', $violation->file); + } + + public function testSourcePathsRuleReportsForwardSlashesForWindowsStyleBasePath(): void + { + $violation = (new Psr4SourcePathsRule(null))->evaluateProject( + self::WINDOWS_STYLE_MISSING_BASE_PATH, + Architecture::define() + ); + + $this->assertInstanceOf(RuleViolation::class, $violation); + $this->assertSame('C:/structarmed-missing-fixture/app/composer.json', $violation->file); + } + + public function testEmptyNamespacePrefixRuleReportsCanonicalisedComposerFilePath(): void + { + $basePath = $this->makeTempProject(<<<'JSON' +{ + "autoload": { + "psr-4": { + "": "src/" + } + } +} +JSON); + + $violations = (new Psr4EmptyNamespacePrefixRule())->evaluateProjectAll( + $basePath . '/.', + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); + } + + public function testRootPathRuleReportsCanonicalisedComposerFilePath(): void + { + $basePath = $this->makeTempProject(<<<'JSON' +{ + "autoload": { + "psr-4": { + "App\\": "." + } + } +} +JSON); + + $violations = (new Psr4RootPathRule())->evaluateProjectAll( + $basePath . '/.', + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); + } + + private function expectedComposerFilePath(string $basePath): string + { + return str_replace('\\', '/', (string) realpath($basePath)) . '/composer.json'; + } + + private function makeTempProject(string $composerJson): string + { + $basePath = $this->makeTemporaryDirectory('structarmed-psr4-composer-file-path'); + file_put_contents($basePath . '/composer.json', $composerJson); + + return $basePath; + } +} From e81f8cbb4a9f88201d0031b29b7bc01b8d37f986 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 31 Aug 2026 22:48:06 +0700 Subject: [PATCH 3/3] fix test --- ...r4ComposerFilePathCanonicalisationTest.php | 78 +++++++++++++++++++ .../Psr4ComposerFilePathNormalisationTest.php | 66 ---------------- 2 files changed, 78 insertions(+), 66 deletions(-) create mode 100644 tests/Rule/Composer/Psr4ComposerFilePathCanonicalisationTest.php diff --git a/tests/Rule/Composer/Psr4ComposerFilePathCanonicalisationTest.php b/tests/Rule/Composer/Psr4ComposerFilePathCanonicalisationTest.php new file mode 100644 index 00000000..9d36125b --- /dev/null +++ b/tests/Rule/Composer/Psr4ComposerFilePathCanonicalisationTest.php @@ -0,0 +1,78 @@ +makeTempProject(<<<'JSON' +{ + "autoload": { + "psr-4": { + "": "src/" + } + } +} +JSON); + + $violations = (new Psr4EmptyNamespacePrefixRule())->evaluateProjectAll( + $basePath . '/.', + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); + } + + public function testRootPathRuleReportsCanonicalisedComposerFilePath(): void + { + $basePath = $this->makeTempProject(<<<'JSON' +{ + "autoload": { + "psr-4": { + "App\\": "." + } + } +} +JSON); + + $violations = (new Psr4RootPathRule())->evaluateProjectAll( + $basePath . '/.', + Architecture::define() + ); + + $this->assertCount(1, $violations); + $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); + } + + private function expectedComposerFilePath(string $basePath): string + { + return str_replace('\\', '/', (string) realpath($basePath)) . '/composer.json'; + } + + private function makeTempProject(string $composerJson): string + { + $basePath = $this->makeTemporaryDirectory('structarmed-psr4-composer-file-path'); + file_put_contents($basePath . '/composer.json', $composerJson); + + return $basePath; + } +} diff --git a/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php b/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php index 19f6bc8f..10929a3f 100644 --- a/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php +++ b/tests/Rule/Composer/Psr4ComposerFilePathNormalisationTest.php @@ -6,26 +6,15 @@ use Boundwize\StructArmed\Architecture; use Boundwize\StructArmed\Rule\Rules\Composer\Psr4DirectoryExistsRule; -use Boundwize\StructArmed\Rule\Rules\Composer\Psr4EmptyNamespacePrefixRule; -use Boundwize\StructArmed\Rule\Rules\Composer\Psr4RootPathRule; use Boundwize\StructArmed\Rule\Rules\Composer\Psr4SourcePathsRule; use Boundwize\StructArmed\Rule\RuleViolation; -use Boundwize\StructArmed\Tests\Support\TemporaryDirectoryCleanupTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use function file_put_contents; -use function realpath; -use function str_replace; - #[CoversClass(Psr4DirectoryExistsRule::class)] -#[CoversClass(Psr4EmptyNamespacePrefixRule::class)] -#[CoversClass(Psr4RootPathRule::class)] #[CoversClass(Psr4SourcePathsRule::class)] final class Psr4ComposerFilePathNormalisationTest extends TestCase { - use TemporaryDirectoryCleanupTrait; - private const WINDOWS_STYLE_MISSING_BASE_PATH = 'C:\structarmed-missing-fixture\app'; public function testDirectoryExistsRuleReportsForwardSlashesForWindowsStyleBasePath(): void @@ -49,59 +38,4 @@ public function testSourcePathsRuleReportsForwardSlashesForWindowsStyleBasePath( $this->assertInstanceOf(RuleViolation::class, $violation); $this->assertSame('C:/structarmed-missing-fixture/app/composer.json', $violation->file); } - - public function testEmptyNamespacePrefixRuleReportsCanonicalisedComposerFilePath(): void - { - $basePath = $this->makeTempProject(<<<'JSON' -{ - "autoload": { - "psr-4": { - "": "src/" - } - } -} -JSON); - - $violations = (new Psr4EmptyNamespacePrefixRule())->evaluateProjectAll( - $basePath . '/.', - Architecture::define() - ); - - $this->assertCount(1, $violations); - $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); - } - - public function testRootPathRuleReportsCanonicalisedComposerFilePath(): void - { - $basePath = $this->makeTempProject(<<<'JSON' -{ - "autoload": { - "psr-4": { - "App\\": "." - } - } -} -JSON); - - $violations = (new Psr4RootPathRule())->evaluateProjectAll( - $basePath . '/.', - Architecture::define() - ); - - $this->assertCount(1, $violations); - $this->assertSame($this->expectedComposerFilePath($basePath), $violations[0]->file); - } - - private function expectedComposerFilePath(string $basePath): string - { - return str_replace('\\', '/', (string) realpath($basePath)) . '/composer.json'; - } - - private function makeTempProject(string $composerJson): string - { - $basePath = $this->makeTemporaryDirectory('structarmed-psr4-composer-file-path'); - file_put_contents($basePath . '/composer.json', $composerJson); - - return $basePath; - } }