diff --git a/docs/assets/no-violation.svg b/docs/assets/no-violation.svg
index be47332..f5b65d6 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 43017dc..0bf7332 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/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php
index c56ba08..4554c14 100644
--- a/src/Cli/AnalyseCommand.php
+++ b/src/Cli/AnalyseCommand.php
@@ -183,69 +183,54 @@ public function run(array $arguments, string $basePath): int
$analysisResultCache->store($cacheKey, $metadata, $ruleViolationCollection);
}
- $elapsed = microtime(true) - $start;
- $baseline = new Baseline();
+ $elapsed = microtime(true) - $start;
+ $baseline = new Baseline();
+ $unfilteredRuleViolationCollection = $ruleViolationCollection;
+ $shouldGenerateBaseline = isset($options['generate-baseline']);
+ $fixedCount = 0;
- if (isset($options['generate-baseline'])) {
+ if (isset($options['fix'])) {
try {
- $baseline->generate($ruleViolationCollection, $options['generate-baseline'], $basePath);
+ $ruleViolationCollection = $this->resolveRuleViolationCollection(
+ $unfilteredRuleViolationCollection,
+ $architecture,
+ $basePath,
+ $shouldGenerateBaseline
+ );
} catch (RuntimeException $runtimeException) {
echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL;
return 1;
}
- echo sprintf(
- "Generated baseline [%s] with %d violation(s).\n",
- $options['generate-baseline'],
- $ruleViolationCollection->count()
- );
-
- return 0;
- }
-
- try {
- $ruleViolationCollection = (new BaselineFilter())->apply(
- $ruleViolationCollection,
- $architecture,
- $basePath
- );
- } catch (RuntimeException $runtimeException) {
- echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL;
-
- return 1;
- }
-
- $fixedCount = 0;
-
- if (isset($options['fix'])) {
$fixedCount = $this->fixViolations($architecture, $ruleViolationCollection);
if ($fixedCount > 0) {
$analysisResultCache->clear();
- $files = $analyser->filesForAnalysis($architecture, $scanPaths);
- $metadata = $analysisCacheMetadataFactory->metadata(
+ $files = $analyser->filesForAnalysis($architecture, $scanPaths);
+ $metadata = $analysisCacheMetadataFactory->metadata(
$basePath,
$configFile,
$scanPaths,
$files
);
- $cacheKey = $analysisCacheMetadataFactory->key($metadata);
- $ruleViolationCollection = $analyser->analyse(
+ $cacheKey = $analysisCacheMetadataFactory->key($metadata);
+ $unfilteredRuleViolationCollection = $analyser->analyse(
$architecture,
$scanPaths,
null,
$analyserOptions,
$files
);
- $analysisResultCache->store($cacheKey, $metadata, $ruleViolationCollection);
+ $analysisResultCache->store($cacheKey, $metadata, $unfilteredRuleViolationCollection);
try {
- $ruleViolationCollection = (new BaselineFilter())->apply(
- $ruleViolationCollection,
+ $ruleViolationCollection = $this->resolveRuleViolationCollection(
+ $unfilteredRuleViolationCollection,
$architecture,
- $basePath
+ $basePath,
+ $shouldGenerateBaseline
);
} catch (RuntimeException $runtimeException) {
echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL;
@@ -255,6 +240,41 @@ public function run(array $arguments, string $basePath): int
$elapsed = microtime(true) - $start;
}
+ } else {
+ try {
+ $ruleViolationCollection = $this->resolveRuleViolationCollection(
+ $unfilteredRuleViolationCollection,
+ $architecture,
+ $basePath,
+ $shouldGenerateBaseline
+ );
+ } catch (RuntimeException $runtimeException) {
+ echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL;
+
+ return 1;
+ }
+ }
+
+ if ($reportType === 'console' && $fixedCount > 0) {
+ echo PHP_EOL . $this->fixedViolationMessage($fixedCount) . PHP_EOL;
+ }
+
+ if ($shouldGenerateBaseline) {
+ try {
+ $baseline->generate($unfilteredRuleViolationCollection, $options['generate-baseline'], $basePath);
+ } catch (RuntimeException $runtimeException) {
+ echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL;
+
+ return 1;
+ }
+
+ echo sprintf(
+ "Generated baseline [%s] with %d violation(s).\n",
+ $options['generate-baseline'],
+ $unfilteredRuleViolationCollection->count()
+ );
+
+ return 0;
}
$report = match ($reportType) {
@@ -262,15 +282,24 @@ public function run(array $arguments, string $basePath): int
default => (new ConsoleReport())->render($ruleViolationCollection, $elapsed),
};
- if ($reportType === 'console' && $fixedCount > 0) {
- echo PHP_EOL . $this->fixedViolationMessage($fixedCount) . PHP_EOL;
- }
-
echo $report;
return $ruleViolationCollection->hasViolations() ? 1 : 0;
}
+ private function resolveRuleViolationCollection(
+ RuleViolationCollection $unfilteredRuleViolationCollection,
+ Architecture $architecture,
+ string $basePath,
+ bool $shouldGenerateBaseline
+ ): RuleViolationCollection {
+ if ($shouldGenerateBaseline) {
+ return $unfilteredRuleViolationCollection;
+ }
+
+ return (new BaselineFilter())->apply($unfilteredRuleViolationCollection, $architecture, $basePath);
+ }
+
private function fixViolations(Architecture $architecture, RuleViolationCollection $ruleViolationCollection): int
{
$rules = $architecture->getRules();
diff --git a/tests/Cli/AnalyseCommandTest.php b/tests/Cli/AnalyseCommandTest.php
new file mode 100644
index 0000000..738a218
--- /dev/null
+++ b/tests/Cli/AnalyseCommandTest.php
@@ -0,0 +1,35 @@
+
+ */
+ public static function fixedViolationMessageProvider(): iterable
+ {
+ yield 'singular' => [1, '1 violation has been fixed.'];
+ yield 'plural' => [2, '2 violations have been fixed.'];
+ }
+
+ #[DataProvider('fixedViolationMessageProvider')]
+ public function testFormatsFixedViolationMessage(int $fixedCount, string $expectedMessage): void
+ {
+ $reflectionMethod = new ReflectionMethod(AnalyseCommand::class, 'fixedViolationMessage');
+
+ $message = $reflectionMethod->invoke(new AnalyseCommand(), $fixedCount);
+
+ $this->assertIsString($message);
+ $this->assertStringContainsString($expectedMessage, $message);
+ }
+}
diff --git a/tests/Cli/StructArmedApplicationTest.php b/tests/Cli/StructArmedApplicationTest.php
index 11388e4..51e5ddb 100644
--- a/tests/Cli/StructArmedApplicationTest.php
+++ b/tests/Cli/StructArmedApplicationTest.php
@@ -636,6 +636,178 @@ public function testAnalyseCommandGeneratesBaselineWithSeparateOptionValue(): vo
}
}
+ public function testAnalyseCommandFixesViolationsBeforeGeneratingBaseline(): void
+ {
+ $basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation();
+
+ try {
+ [$exitCode, $output] = $this->runApplication(
+ [
+ 'structarmed',
+ 'analyse',
+ '--config=' . $basePath . '/structarmed.php',
+ '--fix',
+ '--no-progress',
+ '--generate-baseline=structarmed-baseline.php',
+ ],
+ $basePath
+ );
+
+ $this->assertSame(0, $exitCode, $output);
+ $this->assertStringContainsString(
+ 'Generated baseline [structarmed-baseline.php] with 0 violation(s).',
+ $output
+ );
+ $this->assertStringContainsString(
+ ' public function handle(): void',
+ (string) file_get_contents($basePath . '/src/Foo.php')
+ );
+ $this->assertSame(
+ <<<'PHP'
+removeTempDirectory($basePath);
+ }
+ }
+
+ public function testAnalyseCommandReportsFixedCountWhenGeneratingBaselineAfterFix(): void
+ {
+ $basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation();
+
+ try {
+ [$exitCode, $output] = $this->runApplication(
+ [
+ 'structarmed',
+ 'analyse',
+ '--config=' . $basePath . '/structarmed.php',
+ '--fix',
+ '--no-progress',
+ '--generate-baseline=structarmed-baseline.php',
+ ],
+ $basePath
+ );
+
+ $this->assertSame(0, $exitCode, $output);
+ $this->assertStringContainsString(
+ '1 violation has been fixed.',
+ $this->withoutAnsi($output)
+ );
+ $this->assertStringContainsString(
+ 'Generated baseline [structarmed-baseline.php] with 0 violation(s).',
+ $output
+ );
+ } finally {
+ $this->removeTempDirectory($basePath);
+ }
+ }
+
+ public function testAnalyseCommandCanFixAndGenerateMissingConfiguredBaseline(): void
+ {
+ $basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation();
+
+ try {
+ file_put_contents($basePath . '/structarmed.php', <<<'PHP'
+baseline('structarmed-baseline.php')
+ ->layer('Source', 'src/')
+ ->rule('source.must_declare_method_visibility', new MustDeclareMethodVisibilityRule('Source'));
+PHP);
+
+ [$exitCode, $output] = $this->runApplication(
+ [
+ 'structarmed',
+ 'analyse',
+ '--config=' . $basePath . '/structarmed.php',
+ '--fix',
+ '--no-progress',
+ '--generate-baseline=structarmed-baseline.php',
+ ],
+ $basePath
+ );
+
+ $this->assertSame(0, $exitCode, $output);
+ $this->assertStringContainsString(
+ 'Generated baseline [structarmed-baseline.php] with 0 violation(s).',
+ $output
+ );
+ $this->assertStringContainsString(
+ ' public function handle(): void',
+ (string) file_get_contents($basePath . '/src/Foo.php')
+ );
+ $this->assertFileExists($basePath . '/structarmed-baseline.php');
+ } finally {
+ $this->removeTempDirectory($basePath);
+ }
+ }
+
+ public function testAnalyseCommandGeneratesBaselineFromRemainingViolationsAfterFix(): void
+ {
+ $basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation();
+
+ try {
+ file_put_contents($basePath . '/structarmed.php', <<<'PHP'
+layer('Source', 'src/')
+ ->rule('source.must_declare_method_visibility', new MustDeclareMethodVisibilityRule('Source'))
+ ->rule('source.class_name_must_have_suffix', new ClassNameMustHaveSuffixRule('Source', 'Service'));
+PHP);
+
+ [$exitCode, $output] = $this->runApplication(
+ [
+ 'structarmed',
+ 'analyse',
+ '--config=' . $basePath . '/structarmed.php',
+ '--fix',
+ '--no-progress',
+ '--generate-baseline=structarmed-baseline.php',
+ ],
+ $basePath
+ );
+
+ $this->assertSame(0, $exitCode, $output);
+ $this->assertStringContainsString(
+ 'Generated baseline [structarmed-baseline.php] with 1 violation(s).',
+ $output
+ );
+ $this->assertStringContainsString(
+ ' public function handle(): void',
+ (string) file_get_contents($basePath . '/src/Foo.php')
+ );
+
+ $baseline = file_get_contents($basePath . '/structarmed-baseline.php');
+
+ $this->assertIsString($baseline);
+ $this->assertStringContainsString("'rule' => 'source.class_name_must_have_suffix'", $baseline);
+ $this->assertStringContainsString("'message' => 'Class [App\\Foo] must have suffix [Service]'", $baseline);
+ $this->assertStringNotContainsString('source.must_declare_method_visibility', $baseline);
+ } finally {
+ $this->removeTempDirectory($basePath);
+ }
+ }
+
public function testAnalyseCommandReportsBaselineGenerationFailure(): void
{
$basePath = $this->createProjectDirectoryWithViolation();
@@ -748,6 +920,50 @@ public function testAnalyseCommandReportsConfiguredBaselineFailure(): void
}
}
+ public function testAnalyseCommandReportsConfiguredBaselineFailureBeforeFix(): void
+ {
+ $basePath = $this->createProjectDirectoryWithImplicitMethodVisibilityViolation();
+
+ try {
+ file_put_contents($basePath . '/structarmed.php', <<<'PHP'
+baseline('missing-baseline.php')
+ ->layer('Source', 'src/')
+ ->rule('source.must_declare_method_visibility', new MustDeclareMethodVisibilityRule('Source'));
+PHP);
+
+ [$exitCode, $output] = $this->runApplication(
+ [
+ 'structarmed',
+ 'analyse',
+ '--config=' . $basePath . '/structarmed.php',
+ '--fix',
+ '--no-progress',
+ ],
+ $basePath
+ );
+
+ $this->assertSame(1, $exitCode);
+ $this->assertStringContainsString(
+ 'Error: Baseline file [missing-baseline.php] does not exist.',
+ $output
+ );
+ $this->assertStringContainsString(
+ ' function handle(): void',
+ (string) file_get_contents($basePath . '/src/Foo.php')
+ );
+ } finally {
+ $this->removeTempDirectory($basePath);
+ }
+ }
+
public function testAnalyseCommandReportsConfiguredBaselineFailureAfterFix(): void
{
$basePath = $this->createProjectDirectoryWithViolation();