diff --git a/src/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor.php b/src/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor.php new file mode 100644 index 0000000..94d0341 --- /dev/null +++ b/src/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor.php @@ -0,0 +1,66 @@ +getRequiredECSSets(); + if ($requiredECSSets === []) { + return; + } + + foreach ($monitorConfig->getRepositoryCollection()->all() as $repository) { + foreach ($repository->getRootFiles() as $repositoryRootFile) { + if ($repositoryRootFile->getFilename() !== 'ecs.php') { + continue; + } + + $configuredECSSets = $this->resolveConfiguredECSSets($repositoryRootFile->getContents()); + + foreach ($requiredECSSets as $requiredECSSet) { + if (in_array($requiredECSSet, $configuredECSSets, true)) { + continue; + } + + $errorCollector->addErrorMessage(sprintf( + ' * ECS set "%s" is missing in "%s". Add it via "->withPreparedSets(%s: true)"', + $requiredECSSet, + $repositoryRootFile->getFilename(), + $requiredECSSet + )); + } + } + } + } + + /** + * @return string[] + */ + private function resolveConfiguredECSSets(string $ecsContents): array + { + if (preg_match('#withPreparedSets\s*\((?[^)]*)\)#s', $ecsContents, $match) !== 1) { + return []; + } + + if (preg_match_all('#(?\w+)\s*:\s*true\b#', $match['arguments'], $argumentMatches) === false) { + return []; + } + + return $argumentMatches['name']; + } +} diff --git a/src/Config/MonitorConfig.php b/src/Config/MonitorConfig.php index 866d8f5..98d5cdd 100644 --- a/src/Config/MonitorConfig.php +++ b/src/Config/MonitorConfig.php @@ -36,6 +36,11 @@ final class MonitorConfig */ private array $minPackagesVersions = []; + /** + * @var string[] + */ + private array $requiredECSSets = []; + private bool $noPHPStanBaseline = false; private ?int $minPHPStanLevel = null; @@ -169,6 +174,27 @@ public function getMinPHPStanLevel(): ?int return $this->minPHPStanLevel; } + /** + * @api to be used + * @param string[] $ecsSetNames + */ + public function requireECSSets(array $ecsSetNames): self + { + Assert::allString($ecsSetNames); + + $this->requiredECSSets = array_merge($this->requiredECSSets, $ecsSetNames); + + return $this; + } + + /** + * @return string[] + */ + public function getRequiredECSSets(): array + { + return $this->requiredECSSets; + } + /** * @return string[] */ diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/with-psr12/ecs.php b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/with-psr12/ecs.php new file mode 100644 index 0000000..0785696 --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/with-psr12/ecs.php @@ -0,0 +1,9 @@ +withPaths([__DIR__ . '/src']) + ->withPreparedSets(psr12: true); diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/without-psr12/ecs.php b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/without-psr12/ecs.php new file mode 100644 index 0000000..b3f2545 --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/Fixture/without-psr12/ecs.php @@ -0,0 +1,9 @@ +withPaths([__DIR__ . '/src']) + ->withPreparedSets(common: true); diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/MissingECSSetsMetafileProcessorTest.php b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/MissingECSSetsMetafileProcessorTest.php new file mode 100644 index 0000000..b959660 --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/MissingECSSetsMetafileProcessor/MissingECSSetsMetafileProcessorTest.php @@ -0,0 +1,85 @@ +missingECSSetsMetafileProcessor = $this->make(MissingECSSetsMetafileProcessor::class); + } + + public function testReportsMissingSet(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/without-psr12/ecs.php'); + $monitorConfig->requireECSSets(['psr12']); + + $errorCollector = new ErrorCollector(); + $this->missingECSSetsMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $errorMessages = $errorCollector->getErrorMessages(); + $this->assertCount(1, $errorMessages); + $this->assertStringContainsString('psr12', $errorMessages[0]); + $this->assertStringContainsString('missing', $errorMessages[0]); + } + + public function testSkipsWhenSetIsConfigured(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/with-psr12/ecs.php'); + $monitorConfig->requireECSSets(['psr12']); + + $errorCollector = new ErrorCollector(); + $this->missingECSSetsMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $this->assertEmpty($errorCollector->getErrorMessages()); + } + + public function testSkipsWhenNoRequiredSetsConfigured(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/without-psr12/ecs.php'); + + $errorCollector = new ErrorCollector(); + $this->missingECSSetsMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $this->assertEmpty($errorCollector->getErrorMessages()); + } + + private function createMonitorConfigWithRepository(string $ecsConfigPath): MonitorConfig + { + $monitorConfig = new MonitorConfig(); + $monitorConfig->addRepositories(['https://example.com/some/repo.git']); + + $repository = $monitorConfig->getRepositoryCollection() + ->all()[0]; + $repository->decorateRootFiles([new SplFileInfo($ecsConfigPath, '', 'ecs.php')]); + + return $monitorConfig; + } +}