diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php index 2ad7ae6b..e3fa6024 100644 --- a/src/Analyser/Analyser.php +++ b/src/Analyser/Analyser.php @@ -540,7 +540,12 @@ private function collectPhpFiles(array $layers, array $scanPaths, array $skipPat $files = []; foreach ($this->scanPaths($layers, $scanPaths) as $layerPath) { - $fullPath = rtrim($this->basePath, '/') . '/' . ltrim($layerPath, '/'); + $isAbsolute = str_starts_with($layerPath, '/') || (strlen($layerPath) >= 2 && $layerPath[1] === ':'); + $fullPath = $this->normalisePath( + $isAbsolute + ? $layerPath + : rtrim($this->basePath, '/') . '/' . ltrim($layerPath, '/') + ); if (is_file($fullPath)) { if (str_ends_with($fullPath, '.php') && ! $this->isSkipped($fullPath, $skipPaths)) { diff --git a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php index 2922c830..75bf1f7f 100644 --- a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php +++ b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php @@ -16,6 +16,9 @@ use function rtrim; use function sprintf; use function str_replace; +use function str_starts_with; +use function strlen; +use function substr; use function trim; final readonly class Psr4SourcePathsRule implements ProjectRuleInterface @@ -38,7 +41,7 @@ public function sourcePathsFor(string $basePath): array return $this->psr4PathResolver->paths($basePath); } - return $this->normalisePaths($this->sourcePaths); + return $this->normalisePaths($this->sourcePaths, $basePath); } public function evaluateProject(string $basePath, Architecture $architecture, array $skipPaths = []): ?RuleViolation @@ -68,7 +71,7 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar $autoloadPaths = $this->psr4PathResolver->paths($basePath); $missingPaths = []; - foreach ($this->normalisePaths($this->sourcePaths) as $sourcePath) { + foreach ($this->normalisePaths($this->sourcePaths, $basePath) as $sourcePath) { if (! in_array($sourcePath, $autoloadPaths, true)) { $missingPaths[] = $sourcePath; } @@ -91,10 +94,20 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar * @param list $paths * @return list */ - private function normalisePaths(array $paths): array + private function normalisePaths(array $paths, string $basePath): array { + $normalisedBase = rtrim(str_replace('\\', '/', $basePath), '/'); + return array_map( - static fn(string $path): string => rtrim(str_replace('\\', '/', trim($path)), '/'), + static function (string $path) use ($normalisedBase): string { + $path = rtrim(str_replace('\\', '/', trim($path)), '/'); + + if ($normalisedBase !== '' && str_starts_with($path, $normalisedBase . '/')) { + return substr($path, strlen($normalisedBase) + 1); + } + + return $path; + }, $paths ); } diff --git a/tests/Analyser/AnalyserTest.php b/tests/Analyser/AnalyserTest.php index 77dd9d21..4dd24d40 100644 --- a/tests/Analyser/AnalyserTest.php +++ b/tests/Analyser/AnalyserTest.php @@ -434,6 +434,40 @@ public function testFilesForAnalysisIgnoresDirectorySymlinks(): void $this->assertStringEndsWith('/src/Foo.php', $this->normalisePath($files[0])); } + public function testFilesForAnalysisWithAbsoluteScanPath(): void + { + $basePath = $this->makeTempProject([ + 'index.php' => ' 'layer('Source', 'src/'); + + $absoluteScanPath = $basePath . '/index.php'; + + $files = (new Analyser($basePath))->filesForAnalysis($architecture, [$absoluteScanPath]); + + $this->assertCount(1, $files); + $this->assertStringEndsWith('/index.php', $this->normalisePath($files[0])); + } + + public function testFilesForAnalysisWithRootRelativeScanPath(): void + { + $basePath = $this->makeTempProject([ + 'index.php' => ' 'layer('Source', 'src/'); + + $files = (new Analyser($basePath))->filesForAnalysis($architecture, ['index.php']); + + $this->assertCount(1, $files); + $this->assertStringEndsWith('/index.php', $this->normalisePath($files[0])); + } + public function testFilesForAnalysisUsesPreResolvedLayersWhenProvided(): void { $basePath = $this->makeTempProject([ diff --git a/tests/Rule/Composer/Psr4SourcePathsRuleTest.php b/tests/Rule/Composer/Psr4SourcePathsRuleTest.php index 5ceee621..6fc6151e 100644 --- a/tests/Rule/Composer/Psr4SourcePathsRuleTest.php +++ b/tests/Rule/Composer/Psr4SourcePathsRuleTest.php @@ -164,6 +164,32 @@ public function testNormalisesExplicitSourcePaths(): void $this->assertSame(['src', 'tests'], $psr4SourcePathsRule->sourcePathsFor($this->makeTempDir())); } + public function testPassesWhenAbsoluteSourcePathsExistInComposerPsr4Autoloads(): void + { + $basePath = $this->makeTempProject(<<<'JSON' +{ + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + } +} +JSON); + + $psr4SourcePathsRule = new Psr4SourcePathsRule([$basePath . '/src', $basePath . '/tests']); + + $this->assertNotInstanceOf( + RuleViolation::class, + $psr4SourcePathsRule->evaluateProject($basePath, Architecture::define()) + ); + $this->assertSame(['src', 'tests'], $psr4SourcePathsRule->sourcePathsFor($basePath)); + } + private function makeTempProject(string $composerJson): string { $basePath = $this->makeTempDir();