From f0cab89c5f4f298b9e16668cadafd92bebaf9951 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 07:43:12 +0700 Subject: [PATCH 1/6] Fix baseline generation after applying --fix --- src/Cli/AnalyseCommand.php | 81 +++++++------ tests/Cli/StructArmedApplicationTest.php | 139 +++++++++++++++++++++++ 2 files changed, 186 insertions(+), 34 deletions(-) diff --git a/src/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php index c56ba08..ca5a924 100644 --- a/src/Cli/AnalyseCommand.php +++ b/src/Cli/AnalyseCommand.php @@ -183,67 +183,50 @@ 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 = (new BaselineFilter())->apply( + $unfilteredRuleViolationCollection, + $architecture, + $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'], - $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, + $unfilteredRuleViolationCollection, $architecture, $basePath ); @@ -255,6 +238,36 @@ public function run(array $arguments, string $basePath): int $elapsed = microtime(true) - $start; } + } elseif (! $shouldGenerateBaseline) { + try { + $ruleViolationCollection = (new BaselineFilter())->apply( + $unfilteredRuleViolationCollection, + $architecture, + $basePath + ); + } catch (RuntimeException $runtimeException) { + echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; + + return 1; + } + } + + 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) { diff --git a/tests/Cli/StructArmedApplicationTest.php b/tests/Cli/StructArmedApplicationTest.php index 11388e4..4f93bde 100644 --- a/tests/Cli/StructArmedApplicationTest.php +++ b/tests/Cli/StructArmedApplicationTest.php @@ -636,6 +636,101 @@ 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 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 +843,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(); From 7e59019a54e88e83dfd6d7bdb3f6a12672989480 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 07:43:16 +0700 Subject: [PATCH 2/6] Fix baseline generation after applying --fix --- tests/Cli/AnalyseCommandTest.php | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/Cli/AnalyseCommandTest.php 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); + } +} From 7d7c5e9bbffc7ef0c5cda831679a15bc9525356a Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 07:51:20 +0700 Subject: [PATCH 3/6] fix use of unfiltered collection --- src/Cli/AnalyseCommand.php | 46 ++++++++++++++---------- tests/Cli/StructArmedApplicationTest.php | 46 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php index ca5a924..c8107b3 100644 --- a/src/Cli/AnalyseCommand.php +++ b/src/Cli/AnalyseCommand.php @@ -190,16 +190,20 @@ public function run(array $arguments, string $basePath): int $fixedCount = 0; if (isset($options['fix'])) { - try { - $ruleViolationCollection = (new BaselineFilter())->apply( - $unfilteredRuleViolationCollection, - $architecture, - $basePath - ); - } catch (RuntimeException $runtimeException) { - echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; + if ($shouldGenerateBaseline) { + $ruleViolationCollection = $unfilteredRuleViolationCollection; + } else { + try { + $ruleViolationCollection = (new BaselineFilter())->apply( + $unfilteredRuleViolationCollection, + $architecture, + $basePath + ); + } catch (RuntimeException $runtimeException) { + echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; - return 1; + return 1; + } } $fixedCount = $this->fixViolations($architecture, $ruleViolationCollection); @@ -224,16 +228,20 @@ public function run(array $arguments, string $basePath): int ); $analysisResultCache->store($cacheKey, $metadata, $unfilteredRuleViolationCollection); - try { - $ruleViolationCollection = (new BaselineFilter())->apply( - $unfilteredRuleViolationCollection, - $architecture, - $basePath - ); - } catch (RuntimeException $runtimeException) { - echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; - - return 1; + if ($shouldGenerateBaseline) { + $ruleViolationCollection = $unfilteredRuleViolationCollection; + } else { + try { + $ruleViolationCollection = (new BaselineFilter())->apply( + $unfilteredRuleViolationCollection, + $architecture, + $basePath + ); + } catch (RuntimeException $runtimeException) { + echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; + + return 1; + } } $elapsed = microtime(true) - $start; diff --git a/tests/Cli/StructArmedApplicationTest.php b/tests/Cli/StructArmedApplicationTest.php index 4f93bde..4af1188 100644 --- a/tests/Cli/StructArmedApplicationTest.php +++ b/tests/Cli/StructArmedApplicationTest.php @@ -678,6 +678,52 @@ public function testAnalyseCommandFixesViolationsBeforeGeneratingBaseline(): voi } } + 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(); From 8d5e7daf59e552826b4d39bc14c8596b11c2b08c Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 13:07:29 +0700 Subject: [PATCH 4/6] show fixed count --- src/Cli/AnalyseCommand.php | 8 +++--- tests/Cli/StructArmedApplicationTest.php | 31 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php index c8107b3..7597313 100644 --- a/src/Cli/AnalyseCommand.php +++ b/src/Cli/AnalyseCommand.php @@ -260,6 +260,10 @@ public function run(array $arguments, string $basePath): int } } + if ($reportType === 'console' && $fixedCount > 0) { + echo PHP_EOL . $this->fixedViolationMessage($fixedCount) . PHP_EOL; + } + if ($shouldGenerateBaseline) { try { $baseline->generate($unfilteredRuleViolationCollection, $options['generate-baseline'], $basePath); @@ -283,10 +287,6 @@ 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; diff --git a/tests/Cli/StructArmedApplicationTest.php b/tests/Cli/StructArmedApplicationTest.php index 4af1188..51e5ddb 100644 --- a/tests/Cli/StructArmedApplicationTest.php +++ b/tests/Cli/StructArmedApplicationTest.php @@ -678,6 +678,37 @@ public function testAnalyseCommandFixesViolationsBeforeGeneratingBaseline(): voi } } + 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(); From ac69b325fa72880fda11d3b8b32c8416b98fdf80 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 14:22:50 +0700 Subject: [PATCH 5/6] simplify repeated check --- src/Cli/AnalyseCommand.php | 68 +++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/src/Cli/AnalyseCommand.php b/src/Cli/AnalyseCommand.php index 7597313..4554c14 100644 --- a/src/Cli/AnalyseCommand.php +++ b/src/Cli/AnalyseCommand.php @@ -190,20 +190,17 @@ public function run(array $arguments, string $basePath): int $fixedCount = 0; if (isset($options['fix'])) { - if ($shouldGenerateBaseline) { - $ruleViolationCollection = $unfilteredRuleViolationCollection; - } else { - try { - $ruleViolationCollection = (new BaselineFilter())->apply( - $unfilteredRuleViolationCollection, - $architecture, - $basePath - ); - } catch (RuntimeException $runtimeException) { - echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; + try { + $ruleViolationCollection = $this->resolveRuleViolationCollection( + $unfilteredRuleViolationCollection, + $architecture, + $basePath, + $shouldGenerateBaseline + ); + } catch (RuntimeException $runtimeException) { + echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; - return 1; - } + return 1; } $fixedCount = $this->fixViolations($architecture, $ruleViolationCollection); @@ -228,30 +225,28 @@ public function run(array $arguments, string $basePath): int ); $analysisResultCache->store($cacheKey, $metadata, $unfilteredRuleViolationCollection); - if ($shouldGenerateBaseline) { - $ruleViolationCollection = $unfilteredRuleViolationCollection; - } else { - try { - $ruleViolationCollection = (new BaselineFilter())->apply( - $unfilteredRuleViolationCollection, - $architecture, - $basePath - ); - } catch (RuntimeException $runtimeException) { - echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; - - return 1; - } + try { + $ruleViolationCollection = $this->resolveRuleViolationCollection( + $unfilteredRuleViolationCollection, + $architecture, + $basePath, + $shouldGenerateBaseline + ); + } catch (RuntimeException $runtimeException) { + echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; + + return 1; } $elapsed = microtime(true) - $start; } - } elseif (! $shouldGenerateBaseline) { + } else { try { - $ruleViolationCollection = (new BaselineFilter())->apply( + $ruleViolationCollection = $this->resolveRuleViolationCollection( $unfilteredRuleViolationCollection, $architecture, - $basePath + $basePath, + $shouldGenerateBaseline ); } catch (RuntimeException $runtimeException) { echo 'Error: ' . $runtimeException->getMessage() . PHP_EOL; @@ -292,6 +287,19 @@ public function run(array $arguments, string $basePath): int 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(); From 796f6eb49f918308443921b5c63157d4d8f79b66 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Mon, 29 Jun 2026 14:26:34 +0700 Subject: [PATCH 6/6] update screenshot --- docs/assets/no-violation.svg | 2 +- docs/assets/structarmed-showoff.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 - StructArmed 0.14.5 — Architecture Enforcement + StructArmed 0.14.6 — Architecture Enforcement =============================================== 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 - StructArmed 0.14.5 — Architecture Enforcement + StructArmed 0.14.6 — Architecture Enforcement ===============================================