diff --git a/docs/assets/no-violation.svg b/docs/assets/no-violation.svg
index f5b65d6..18dc36a 100644
--- a/docs/assets/no-violation.svg
+++ b/docs/assets/no-violation.svg
@@ -14,7 +14,7 @@
➜ prj-ddd vendor/bin/structarmed analyze
-
+
===============================================
diff --git a/docs/assets/structarmed-showoff.svg b/docs/assets/structarmed-showoff.svg
index 0bf7332..fc08b66 100644
--- a/docs/assets/structarmed-showoff.svg
+++ b/docs/assets/structarmed-showoff.svg
@@ -15,7 +15,7 @@
➜ prj-ddd vendor/bin/structarmed analyze
-
+
===============================================
diff --git a/src/Analyser/Analyser.php b/src/Analyser/Analyser.php
index 97421e8..0e19337 100644
--- a/src/Analyser/Analyser.php
+++ b/src/Analyser/Analyser.php
@@ -11,6 +11,7 @@
use Boundwize\StructArmed\Composer\Psr4PathResolver;
use Boundwize\StructArmed\LayerResolver\ChainLayerResolver;
use Boundwize\StructArmed\Progress\ProgressHandlerInterface;
+use Boundwize\StructArmed\Rule\ComposerJsonRuleInterface;
use Boundwize\StructArmed\Rule\FileAnalysisRuleInterface;
use Boundwize\StructArmed\Rule\FixableInterface;
use Boundwize\StructArmed\Rule\LayerAwareRuleInterface;
@@ -42,7 +43,6 @@
use function is_dir;
use function is_file;
use function sprintf;
-use function str_ends_with;
use function str_starts_with;
use function strpbrk;
use function substr;
@@ -106,16 +106,21 @@ public function analyse(
continue;
}
+ $projectRuleSkipPaths = $ruleSkipPaths[$key] ?? [];
+
if ($rule instanceof MultipleProjectRuleViolationInterface) {
- $projectRuleViolations[$key] = $rule->evaluateProjectAll(
- $this->basePath,
- $architecture,
- $ruleSkipPaths[$key] ?? []
+ $projectRuleViolations[$key] = $this->withoutSkippedProjectViolations(
+ $rule->evaluateProjectAll($this->basePath, $architecture, $projectRuleSkipPaths),
+ $projectRuleSkipPaths,
);
} else {
- $single = $rule->evaluateProject($this->basePath, $architecture, $ruleSkipPaths[$key] ?? []);
+ $single = $rule->evaluateProject($this->basePath, $architecture, $projectRuleSkipPaths);
+ $violations = $single instanceof RuleViolation ? [$single] : [];
- $projectRuleViolations[$key] = $single instanceof RuleViolation ? [$single] : [];
+ $projectRuleViolations[$key] = $this->withoutSkippedProjectViolations(
+ $violations,
+ $projectRuleSkipPaths,
+ );
}
}
@@ -864,38 +869,53 @@ private function collectClassNodes(
}
/**
- * @param list $scanPaths
- * @param array>|null $layers
- * @return list
+ * @param list $violations
+ * @param list $skipPaths
+ * @return list
*/
- public function filesForAnalysis(Architecture $architecture, array $scanPaths = [], ?array $layers = null): array
+ private function withoutSkippedProjectViolations(array $violations, array $skipPaths): array
{
- return $this->collectPhpFiles(
- $layers ?? $this->resolveLayers($architecture),
- $scanPaths,
- $architecture->getSkipPaths()
- );
+ if ($violations === [] || $skipPaths === []) {
+ return $violations;
+ }
+
+ $skipMatchers = $this->compileSkipMatchers($skipPaths);
+
+ return array_values(array_filter(
+ $violations,
+ fn(RuleViolation $ruleViolation): bool => $ruleViolation->file === ''
+ || ! $this->isSkipped($ruleViolation->file, $skipMatchers),
+ ));
}
/**
- * @param array> $layers
* @param list $scanPaths
- * @param list $skipPaths
+ * @param array>|null $layers
* @return list
*/
- private function collectPhpFiles(array $layers, array $scanPaths, array $skipPaths): array
+ public function filesForAnalysis(Architecture $architecture, array $scanPaths = [], ?array $layers = null): array
{
+ $layers ??= $this->resolveLayers($architecture);
$files = [];
+ $skipPaths = $architecture->getSkipPaths();
$skipMatchers = $this->compileSkipMatchers($skipPaths);
+ $scanPaths = $this->scanPaths($layers, $scanPaths);
+
+ if ($this->shouldAnalyseComposerJson($architecture)) {
+ $scanPaths[] = 'composer.json';
+ }
- foreach ($this->scanPaths($layers, $scanPaths) as $layerPath) {
+ foreach (array_values(array_unique($scanPaths)) as $layerPath) {
$fullPath = Path::normalise(
Path::resolve($layerPath, $this->basePath),
canonicalise: true
);
if (is_file($fullPath)) {
- if (str_ends_with($fullPath, '.php') && ! $this->isSkipped($fullPath, $skipMatchers)) {
+ if (
+ Path::isAnalysableFile($fullPath, $this->basePath)
+ && ! $this->isSkipped($fullPath, $skipMatchers)
+ ) {
$files[] = $fullPath;
}
@@ -918,6 +938,45 @@ private function collectPhpFiles(array $layers, array $scanPaths, array $skipPat
return array_values(array_unique($files));
}
+ private function shouldAnalyseComposerJson(Architecture $architecture): bool
+ {
+ $skippedRuleKeys = $this->skippedRuleKeyMap($architecture->getSkippedRuleKeys());
+ $ruleSkipPaths = $architecture->getRuleSkipPaths();
+
+ foreach ($architecture->getRules() as $key => $rule) {
+ if (array_key_exists($key, $skippedRuleKeys)) {
+ continue;
+ }
+
+ if (! $rule instanceof ComposerJsonRuleInterface) {
+ continue;
+ }
+
+ if ($this->isComposerJsonSkippedByRule($ruleSkipPaths[$key] ?? [])) {
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param list $skipPaths
+ */
+ private function isComposerJsonSkippedByRule(array $skipPaths): bool
+ {
+ if ($skipPaths === []) {
+ return false;
+ }
+
+ return $this->isSkipped(
+ Path::resolve('composer.json', $this->normalisedBasePath),
+ $this->compileSkipMatchers($skipPaths)
+ );
+ }
+
/**
* @return array>
*/
diff --git a/src/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php
index 4554c14..3861a97 100644
--- a/src/Cli/AnalyseCommand.php
+++ b/src/Cli/AnalyseCommand.php
@@ -27,7 +27,6 @@
use function is_file;
use function microtime;
use function sprintf;
-use function str_ends_with;
use function str_starts_with;
use function strlen;
use function substr;
@@ -128,7 +127,7 @@ public function run(array $arguments, string $basePath): int
continue;
}
- if (is_file($fullScanPath) && str_ends_with($fullScanPath, '.php')) {
+ if (is_file($fullScanPath) && Path::isAnalysableFile($fullScanPath, $basePath)) {
continue;
}
diff --git a/src/Rule/ComposerJsonRuleInterface.php b/src/Rule/ComposerJsonRuleInterface.php
new file mode 100644
index 0000000..9258e47
--- /dev/null
+++ b/src/Rule/ComposerJsonRuleInterface.php
@@ -0,0 +1,9 @@
+
- * @param string[] $skipPaths
+ * @param list $skipPaths
*/
public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array
{
diff --git a/src/Rule/Rules/Composer/Psr4RootPathRule.php b/src/Rule/Rules/Composer/Psr4RootPathRule.php
index 6b5b607..0907660 100644
--- a/src/Rule/Rules/Composer/Psr4RootPathRule.php
+++ b/src/Rule/Rules/Composer/Psr4RootPathRule.php
@@ -6,6 +6,7 @@
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Composer\Psr4PathResolver;
+use Boundwize\StructArmed\Rule\ComposerJsonRuleInterface;
use Boundwize\StructArmed\Rule\MultipleProjectRuleViolationInterface;
use Boundwize\StructArmed\Rule\RuleViolation;
@@ -16,7 +17,7 @@
use function sprintf;
use function trim;
-final readonly class Psr4RootPathRule implements MultipleProjectRuleViolationInterface
+final readonly class Psr4RootPathRule implements MultipleProjectRuleViolationInterface, ComposerJsonRuleInterface
{
public function __construct(
private Psr4PathResolver $psr4PathResolver = new Psr4PathResolver(),
@@ -30,7 +31,7 @@ public function evaluateProject(string $basePath, Architecture $architecture, ar
/**
* @return list
- * @param string[] $skipPaths
+ * @param list $skipPaths
*/
public function evaluateProjectAll(string $basePath, Architecture $architecture, array $skipPaths = []): array
{
diff --git a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php
index f099361..dc92246 100644
--- a/src/Rule/Rules/Composer/Psr4SourcePathsRule.php
+++ b/src/Rule/Rules/Composer/Psr4SourcePathsRule.php
@@ -6,7 +6,7 @@
use Boundwize\StructArmed\Architecture;
use Boundwize\StructArmed\Composer\Psr4PathResolver;
-use Boundwize\StructArmed\Rule\ProjectRuleInterface;
+use Boundwize\StructArmed\Rule\ComposerJsonRuleInterface;
use Boundwize\StructArmed\Rule\RuleViolation;
use Boundwize\StructArmed\Util\Path;
@@ -21,7 +21,7 @@
use function substr;
use function trim;
-final readonly class Psr4SourcePathsRule implements ProjectRuleInterface
+final readonly class Psr4SourcePathsRule implements ComposerJsonRuleInterface
{
/**
* @param list $sourcePaths
diff --git a/src/Util/Path.php b/src/Util/Path.php
index b1e2316..50556dd 100644
--- a/src/Util/Path.php
+++ b/src/Util/Path.php
@@ -8,6 +8,7 @@
use function preg_replace;
use function realpath;
use function rtrim;
+use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function strlen;
@@ -64,6 +65,24 @@ public static function resolve(string $path, string $basePath): string
: rtrim($basePath, '/\\') . '/' . $path;
}
+ public static function isAnalysableFile(string $path, string $basePath): bool
+ {
+ if (str_ends_with($path, '.php')) {
+ return true;
+ }
+
+ if (! str_ends_with($path, 'composer.json')) {
+ return false;
+ }
+
+ $resolvedPath = self::resolve($path, $basePath);
+
+ return self::normalise($resolvedPath, canonicalise: true) === self::normalise(
+ self::resolve('composer.json', $basePath),
+ canonicalise: true,
+ );
+ }
+
private static function isAbsolute(string $path): bool
{
if (str_starts_with($path, '/') || str_starts_with($path, '\\\\')) {
diff --git a/tests/Analyser/AnalyserTest.php b/tests/Analyser/AnalyserTest.php
index a1d9883..8f367fb 100644
--- a/tests/Analyser/AnalyserTest.php
+++ b/tests/Analyser/AnalyserTest.php
@@ -13,9 +13,11 @@
use Boundwize\StructArmed\Preset\Preset;
use Boundwize\StructArmed\Preset\Presets\Psr15Preset;
use Boundwize\StructArmed\Preset\Presets\Psr1Preset;
+use Boundwize\StructArmed\Preset\Presets\Psr4Preset;
use Boundwize\StructArmed\Progress\ProgressHandlerInterface;
use Boundwize\StructArmed\Rule\FileAnalysisRuleInterface;
use Boundwize\StructArmed\Rule\Rules\Class_\MustBeFinalRule;
+use Boundwize\StructArmed\Rule\Rules\Composer\Psr4SourcePathsRule;
use Boundwize\StructArmed\Rule\Rules\File\Psr1PhpTagsRule;
use Boundwize\StructArmed\Rule\Rules\Layer\MayNotDependOnRule;
use Boundwize\StructArmed\Rule\Rules\Method\MaxMethodLengthRule;
@@ -254,6 +256,24 @@ public function testAnalyserEvaluatesProjectRules(): void
$this->assertCount(1, $ruleViolationCollection->forRule('psr4.source_paths.must_be_in_composer'));
}
+ public function testAnalyserSkipsComposerProjectRuleWithExplicitComposerJsonRuleSkip(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'composer.json' => '{"autoload":{"psr-4":{"App\\\\":"src/"}}}',
+ 'src/Foo.php' => 'withPreset(Preset::PSR4(sourcePaths: ['src/', 'tests/']))
+ ->skip([
+ Psr4Preset::SOURCE_PATHS_MUST_BE_IN_COMPOSER => ['composer.json'],
+ ]);
+
+ $ruleViolationCollection = (new Analyser($basePath))->analyse($architecture);
+
+ $this->assertCount(0, $ruleViolationCollection->forRule(Psr4Preset::SOURCE_PATHS_MUST_BE_IN_COMPOSER));
+ }
+
public function testAnalyserSkipPathOnProjectRuleSuppressesViolations(): void
{
$basePath = $this->makeTempProject([
@@ -303,11 +323,111 @@ public function testAnalyserContinuesWhenProjectRulePasses(): void
$architecture = Architecture::define()
->withPreset(Preset::PSR4(sourcePaths: ['src/']));
- $ruleViolationCollection = (new Analyser($basePath))->analyse($architecture);
+ $analyser = new Analyser($basePath);
+ $files = array_map($this->normalisePath(...), $analyser->filesForAnalysis($architecture));
+
+ sort($files);
+
+ $this->assertCount(2, $files);
+ $this->assertStringEndsWith('/composer.json', $files[0]);
+ $this->assertStringEndsWith('/src/Foo.php', $files[1]);
+
+ $ruleViolationCollection = $analyser->analyse($architecture);
$this->assertFalse($ruleViolationCollection->hasViolations());
}
+ public function testFilesForAnalysisIgnoresMissingRootComposerJsonCandidate(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'src/Foo.php' => 'withPreset(Preset::PSR4(sourcePaths: ['src/']));
+
+ $files = array_map($this->normalisePath(...), (new Analyser($basePath))->filesForAnalysis($architecture));
+
+ $this->assertCount(1, $files);
+ $this->assertStringEndsWith('/src/Foo.php', $files[0]);
+ }
+
+ public function testFilesForAnalysisIncludesRootComposerJsonForComposerJsonRule(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'composer.json' => '{"autoload":{"psr-4":{"App\\\\":"src/"}}}',
+ 'src/Foo.php' => 'layer('Domain', 'src/')
+ ->rule('composer.source_paths', new Psr4SourcePathsRule(['src/']));
+
+ $analyser = new Analyser($basePath);
+ $files = array_map(
+ $this->normalisePath(...),
+ $analyser->filesForAnalysis($architecture)
+ );
+
+ sort($files);
+
+ $this->assertCount(2, $files);
+ $this->assertStringEndsWith('/composer.json', $files[0]);
+ $this->assertStringEndsWith('/src/Foo.php', $files[1]);
+ }
+
+ public function testFilesForAnalysisDoesNotIncludeRootComposerJsonWithoutComposerJsonRule(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'composer.json' => '{"autoload":{"psr-4":{"App\\\\":"src/"}}}',
+ 'src/Foo.php' => 'layer('Domain', 'src/');
+
+ $files = array_map($this->normalisePath(...), (new Analyser($basePath))->filesForAnalysis($architecture));
+
+ $this->assertCount(1, $files);
+ $this->assertStringEndsWith('/src/Foo.php', $files[0]);
+ }
+
+ public function testFilesForAnalysisDoesNotIncludeRootComposerJsonWhenComposerJsonRuleSkipsIt(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'composer.json' => '{"autoload":{"psr-4":{"App\\\\":"src/"}}}',
+ 'src/Foo.php' => 'layer('Domain', 'src/')
+ ->rule('composer.source_paths', new Psr4SourcePathsRule(['src/']))
+ ->skip(['composer.source_paths' => ['composer.json']]);
+
+ $files = array_map($this->normalisePath(...), (new Analyser($basePath))->filesForAnalysis($architecture));
+
+ $this->assertCount(1, $files);
+ $this->assertStringEndsWith('/src/Foo.php', $files[0]);
+ }
+
+ public function testFilesForAnalysisDoesNotIncludeRootComposerJsonWhenComposerJsonRuleIsSkipped(): void
+ {
+ $basePath = $this->makeTempProject([
+ 'composer.json' => '{"autoload":{"psr-4":{"App\\\\":"src/"}}}',
+ 'src/Foo.php' => 'layer('Domain', 'src/')
+ ->rule('composer.source_paths', new Psr4SourcePathsRule(['src/']))
+ ->skipRule('composer.source_paths');
+
+ $files = array_map($this->normalisePath(...), (new Analyser($basePath))->filesForAnalysis($architecture));
+
+ $this->assertCount(1, $files);
+ $this->assertStringEndsWith('/src/Foo.php', $files[0]);
+ }
+
public function testAnalyserUsesComposerPsr4PathsForDefaultPsr4Preset(): void
{
$basePath = $this->makeTempProject([
@@ -667,18 +787,29 @@ public function testFilesForAnalysisWithAbsoluteScanPath(): void
public function testFilesForAnalysisWithRootRelativeScanPath(): void
{
- $basePath = $this->makeTempProject([
- 'index.php' => ' 'makeTempProject([
+ 'composer.json' => '{}',
+ 'index.php' => ' '{}',
+ 'src/Foo.php' => 'assertIsString($rootComposerFile);
$architecture = Architecture::define()
->layer('Source', 'src/');
- $files = (new Analyser($basePath))->filesForAnalysis($architecture, ['index.php']);
+ $analyser = new Analyser($basePath);
+ $files = $analyser->filesForAnalysis($architecture, ['index.php']);
$this->assertCount(1, $files);
$this->assertStringEndsWith('/index.php', $this->normalisePath($files[0]));
+ $this->assertSame(
+ [$this->normalisePath($rootComposerFile)],
+ array_map($this->normalisePath(...), $analyser->filesForAnalysis($architecture, ['composer.json']))
+ );
+ $this->assertSame([], $analyser->filesForAnalysis($architecture, ['nested/composer.json']));
}
public function testFilesForAnalysisUsesPreResolvedLayersWhenProvided(): void
diff --git a/tests/Cli/StructArmedApplicationCommandRoutingTest.php b/tests/Cli/StructArmedApplicationCommandRoutingTest.php
index 67916fd..1e54ce1 100644
--- a/tests/Cli/StructArmedApplicationCommandRoutingTest.php
+++ b/tests/Cli/StructArmedApplicationCommandRoutingTest.php
@@ -13,7 +13,8 @@
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
-use function ob_get_clean;
+use function ob_end_clean;
+use function ob_get_contents;
use function ob_start;
use function sprintf;
@@ -147,9 +148,9 @@ private function runApplication(array $argv, ?string $basePath = null): array
{
ob_start();
$exitCode = (new StructArmedApplication())->run($argv, $basePath);
- $output = ob_get_clean();
- $this->assertIsString($output);
+ $output = ob_get_contents();
+ ob_end_clean();
- return [$exitCode, $output];
+ return [$exitCode, (string) $output];
}
}
diff --git a/tests/Cli/StructArmedApplicationTest.php b/tests/Cli/StructArmedApplicationTest.php
index 51e5ddb..e1a689d 100644
--- a/tests/Cli/StructArmedApplicationTest.php
+++ b/tests/Cli/StructArmedApplicationTest.php
@@ -25,10 +25,12 @@
use function is_dir;
use function json_decode;
use function mkdir;
-use function ob_get_clean;
+use function ob_end_clean;
+use function ob_get_contents;
use function ob_start;
use function preg_replace;
use function random_bytes;
+use function realpath;
use function rmdir;
use function serialize;
use function str_replace;
@@ -485,6 +487,106 @@ public function testAnalyseCommandFixesFixableViolations(): void
}
}
+ public function testAnalyseCommandTracksComposerJsonProgressAsSingleFile(): void
+ {
+ $basePath = $this->createProjectDirectoryWithMissingComposerPsr4Path();
+ $progress = new class implements ProgressHandlerInterface {
+ /** @var list */
+ public array $totals = [];
+
+ /** @var list */
+ public array $files = [];
+
+ public function start(int $total): void
+ {
+ $this->totals[] = $total;
+ }
+
+ public function advance(string $file): void
+ {
+ $this->files[] = $file;
+ }
+
+ public function finish(): void
+ {
+ }
+ };
+
+ try {
+ $composerFile = $this->normalisePath((string) realpath($basePath . '/composer.json'));
+
+ [$firstExitCode, $firstOutput] = $this->runAnalyseCommand(
+ ['--config=' . $basePath . '/structarmed.php'],
+ $basePath,
+ $progress
+ );
+
+ $this->assertSame(1, $firstExitCode, $firstOutput);
+ $this->assertStringContainsString('declared in composer.json do not exist on disk', $firstOutput);
+ $this->assertSame([1], $progress->totals);
+ $this->assertCount(1, $progress->files);
+ $this->assertSame(
+ $composerFile,
+ $this->normalisePath($progress->files[0])
+ );
+
+ $fixProgress = new class implements ProgressHandlerInterface {
+ /** @var list */
+ public array $totals = [];
+
+ /** @var list */
+ public array $files = [];
+
+ public function start(int $total): void
+ {
+ $this->totals[] = $total;
+ }
+
+ public function advance(string $file): void
+ {
+ $this->files[] = $file;
+ }
+
+ public function finish(): void
+ {
+ }
+ };
+
+ [$exitCode, $output] = $this->runAnalyseCommand(
+ [
+ '--config=' . $basePath . '/structarmed.php',
+ '--clear-cache',
+ '--fix',
+ ],
+ $basePath,
+ $fixProgress
+ );
+
+ $this->assertSame(0, $exitCode, $output);
+ $this->assertStringContainsString('1 violation has been fixed.', $this->withoutAnsi($output));
+ $this->assertSame([1], $fixProgress->totals);
+ $this->assertCount(1, $fixProgress->files);
+ $this->assertSame(
+ $composerFile,
+ $this->normalisePath($fixProgress->files[0])
+ );
+
+ [$nestedExitCode, $nestedOutput] = $this->runAnalyseCommand(
+ [
+ 'nested/composer.json',
+ '--config=' . $basePath . '/structarmed.php',
+ ],
+ $basePath,
+ $fixProgress
+ );
+
+ $this->assertSame(1, $nestedExitCode, $nestedOutput);
+ $this->assertStringContainsString('Error: path [nested/composer.json] not found.', $nestedOutput);
+ } finally {
+ $this->removeTempDirectory($basePath);
+ }
+ }
+
public function testAnalyseCommandReportsPluralFixedViolations(): void
{
$basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation(<<<'PHP'
@@ -1177,10 +1279,10 @@ private function runApplication(array $argv, ?string $basePath = null): array
{
ob_start();
$exitCode = (new StructArmedApplication())->run($argv, $basePath);
- $output = ob_get_clean();
- $this->assertIsString($output);
+ $output = ob_get_contents();
+ ob_end_clean();
- return [$exitCode, $output];
+ return [$exitCode, (string) $output];
}
/**
@@ -1194,10 +1296,10 @@ private function runAnalyseCommand(
): array {
ob_start();
$exitCode = (new AnalyseCommand($progressHandler))->run($arguments, $basePath);
- $output = ob_get_clean();
- $this->assertIsString($output);
+ $output = ob_get_contents();
+ ob_end_clean();
- return [$exitCode, $output];
+ return [$exitCode, (string) $output];
}
private function createProjectDirectory(): string
@@ -1217,6 +1319,37 @@ private function createProjectDirectory(): string
return $basePath;
}
+ private function createProjectDirectoryWithMissingComposerPsr4Path(): string
+ {
+ $basePath = $this->createTempDirectory();
+ file_put_contents($basePath . '/composer.json', <<<'JSON'
+{
+ "autoload": {
+ "psr-4": {
+ "App\\": "src/",
+ "Missing\\": "missing/"
+ }
+ }
+}
+JSON);
+ mkdir($basePath . '/src');
+ mkdir($basePath . '/nested');
+ file_put_contents($basePath . '/nested/composer.json', '{}');
+ file_put_contents($basePath . '/structarmed.php', <<<'PHP'
+withPreset(Preset::PSR4(sourcePaths: ['src/']));
+PHP);
+
+ return $basePath;
+ }
+
private function createProjectDirectoryWithViolation(): string
{
$basePath = $this->createProjectDirectory();
@@ -1355,6 +1488,14 @@ private function removeTempDirectory(string $basePath): void
unlink($basePath . '/structarmed-baseline.php');
}
+ if (file_exists($basePath . '/composer.json')) {
+ unlink($basePath . '/composer.json');
+ }
+
+ if (file_exists($basePath . '/nested/composer.json')) {
+ unlink($basePath . '/nested/composer.json');
+ }
+
foreach (glob($basePath . '/var/cache/structarmed/*.json') ?: [] as $cacheFile) {
unlink($cacheFile);
}
@@ -1383,6 +1524,10 @@ private function removeTempDirectory(string $basePath): void
rmdir($basePath . '/var');
}
+ if (is_dir($basePath . '/nested')) {
+ rmdir($basePath . '/nested');
+ }
+
if (is_dir($basePath)) {
rmdir($basePath);
}
diff --git a/tests/Util/PathTest.php b/tests/Util/PathTest.php
index 91eba3d..88a8052 100644
--- a/tests/Util/PathTest.php
+++ b/tests/Util/PathTest.php
@@ -48,12 +48,33 @@ public static function provideResolve(): Iterator
yield 'windows UNC absolute' => ['\\\\server\\share\\src', '/project', '\\\\server\\share\\src'];
}
+ /**
+ * @return Iterator
+ */
+ public static function provideIsAnalysableFile(): Iterator
+ {
+ yield 'relative php' => ['src/Foo.php', '/project', true];
+ yield 'absolute php' => ['/project/src/Foo.php', '/project', true];
+ yield 'base-relative root composer' => ['composer.json', '/project', true];
+ yield 'absolute root composer' => ['/project/composer.json', '/project', true];
+ yield 'nested composer' => ['/project/nested/composer.json', '/project', false];
+ yield 'other root composer' => ['/other/composer.json', '/project', false];
+ yield 'composer lock' => ['/project/composer.lock', '/project', false];
+ yield 'readme' => ['/project/README.md', '/project', false];
+ }
+
#[DataProvider('provideResolve')]
public function testResolve(string $path, string $basePath, string $expected): void
{
$this->assertSame($expected, Path::resolve($path, $basePath));
}
+ #[DataProvider('provideIsAnalysableFile')]
+ public function testIsAnalysableFile(string $path, string $basePath, bool $expected): void
+ {
+ $this->assertSame($expected, Path::isAnalysableFile($path, $basePath));
+ }
+
public function testMemoisesNormalisedAndResolvedPaths(): void
{
$path = __DIR__ . '/../Util/PathTest.php';