diff --git a/README.md b/README.md index 965c25a..131023f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ return MonitorConfig::configure() ->minPackageVersion('rector/rector', '2.2') // other rules - ->noPhpstanBaseline(); + ->noPhpstanBaseline() + ->minPHPStanLevel(8); ```
diff --git a/composer.json b/composer.json index 4e3d743..d0d1be8 100644 --- a/composer.json +++ b/composer.json @@ -25,9 +25,9 @@ "shipmonk/composer-dependency-analyser": "^1.8", "symplify/phpstan-extensions": "^12.0", "symplify/phpstan-rules": "^14.10", - "tomasvotruba/class-leak": "^2.1", "tomasvotruba/unused-public": "^2.2", - "tracy/tracy": "^2.12" + "tracy/tracy": "^2.12", + "tomasvotruba/class-leak": "^2.1.3" }, "autoload": { "psr-4": { diff --git a/monitor.php b/monitor.php index c034bac..ec3f36e 100644 --- a/monitor.php +++ b/monitor.php @@ -30,4 +30,5 @@ ->minPackageVersion('rector/rector', '2.0') // other rules - ->noPhpstanBaseline(); + ->noPhpstanBaseline() + ->minPHPStanLevel(8); diff --git a/src/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor.php b/src/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor.php new file mode 100644 index 0000000..43ccc6f --- /dev/null +++ b/src/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor.php @@ -0,0 +1,87 @@ +getMinPHPStanLevel(); + if ($minPHPStanLevel === null) { + return; + } + + foreach ($monitorConfig->getRepositoryCollection()->all() as $repository) { + foreach ($repository->getRootFiles() as $repositoryRootFile) { + if (! str_contains($repositoryRootFile->getFilename(), 'phpstan')) { + continue; + } + + if (! str_ends_with($repositoryRootFile->getFilename(), '.neon')) { + continue; + } + + if (str_contains($repositoryRootFile->getFilename(), 'baseline')) { + continue; + } + + $phpstanLevel = $this->resolvePHPStanLevel($repositoryRootFile->getContents()); + if ($phpstanLevel === null) { + continue; + } + + if ($phpstanLevel >= $minPHPStanLevel) { + continue; + } + + $errorCollector->addErrorMessage(sprintf( + ' * PHPStan level in "%s" is too low. Current level "%d", should be at least "%d"', + $repositoryRootFile->getFilename(), + $phpstanLevel, + $minPHPStanLevel + )); + } + } + } + + private function resolvePHPStanLevel(string $neonContents): ?int + { + $decoded = Neon::decode($neonContents); + if (! is_array($decoded)) { + return null; + } + + $level = $decoded['parameters']['level'] ?? null; + if ($level === null) { + return null; + } + + if ($level === 'max') { + return 10; + } + + if (is_int($level)) { + return $level; + } + + if (is_string($level) && preg_match('#^\d+$#', $level) === 1) { + return (int) $level; + } + + return null; + } +} diff --git a/src/Config/MonitorConfig.php b/src/Config/MonitorConfig.php index 63ff6d6..866d8f5 100644 --- a/src/Config/MonitorConfig.php +++ b/src/Config/MonitorConfig.php @@ -38,6 +38,8 @@ final class MonitorConfig private bool $noPHPStanBaseline = false; + private ?int $minPHPStanLevel = null; + private ?RepositoryCollection $repositoryCollection = null; public static function configure(): self @@ -153,6 +155,20 @@ public function isNoPHPStanBaseline(): bool return $this->noPHPStanBaseline; } + public function minPHPStanLevel(int $minPHPStanLevel): self + { + Assert::range($minPHPStanLevel, 0, 10); + + $this->minPHPStanLevel = $minPHPStanLevel; + + return $this; + } + + public function getMinPHPStanLevel(): ?int + { + return $this->minPHPStanLevel; + } + /** * @return string[] */ diff --git a/src/Helper/SymfonyColumnStyler.php b/src/Helper/SymfonyColumnStyler.php index 3c3f18f..4e05a74 100644 --- a/src/Helper/SymfonyColumnStyler.php +++ b/src/Helper/SymfonyColumnStyler.php @@ -43,8 +43,6 @@ public static function styleHighsAndLows(array $tableRow): array $secondVersion ) ); - - $uniqueValues = array_unique($stringValues); $highValue = array_shift($stringValues); $lowValue = array_pop($stringValues); diff --git a/templates/monitor.php.dist b/templates/monitor.php.dist index 73dc9b9..25c5128 100644 --- a/templates/monitor.php.dist +++ b/templates/monitor.php.dist @@ -24,4 +24,5 @@ return MonitorConfig::configure() // other rules // ->noPhpstanBaseline() + // ->minPHPStanLevel(8) ; diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-high.neon b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-high.neon new file mode 100644 index 0000000..22254bc --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-high.neon @@ -0,0 +1,2 @@ +parameters: + level: max diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-too-low.neon b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-too-low.neon new file mode 100644 index 0000000..4e10079 --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/Fixture/phpstan-too-low.neon @@ -0,0 +1,2 @@ +parameters: + level: 3 diff --git a/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/TooLowPHPStanLevelMetafileProcessorTest.php b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/TooLowPHPStanLevelMetafileProcessorTest.php new file mode 100644 index 0000000..06b2462 --- /dev/null +++ b/tests/Analyze/RuleProcessor/MetafileProcessor/TooLowPHPStanLevelMetafileProcessor/TooLowPHPStanLevelMetafileProcessorTest.php @@ -0,0 +1,84 @@ +tooLowPHPStanLevelMetafileProcessor = $this->make(TooLowPHPStanLevelMetafileProcessor::class); + } + + public function testReportsTooLowLevel(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/phpstan-too-low.neon'); + $monitorConfig->minPHPStanLevel(8); + + $errorCollector = new ErrorCollector(); + $this->tooLowPHPStanLevelMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $errorMessages = $errorCollector->getErrorMessages(); + $this->assertCount(1, $errorMessages); + $this->assertStringContainsString('too low', $errorMessages[0]); + } + + public function testSkipsWhenLevelIsHighEnough(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/phpstan-high.neon'); + $monitorConfig->minPHPStanLevel(8); + + $errorCollector = new ErrorCollector(); + $this->tooLowPHPStanLevelMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $this->assertEmpty($errorCollector->getErrorMessages()); + } + + public function testSkipsWhenNoMinLevelConfigured(): void + { + $monitorConfig = $this->createMonitorConfigWithRepository(__DIR__ . '/Fixture/phpstan-too-low.neon'); + + $errorCollector = new ErrorCollector(); + $this->tooLowPHPStanLevelMetafileProcessor->process( + $monitorConfig, + new ComposerJson('https://example.com/some/repo.git', []), + $errorCollector + ); + + $this->assertEmpty($errorCollector->getErrorMessages()); + } + + private function createMonitorConfigWithRepository(string $phpstanNeonPath): MonitorConfig + { + $monitorConfig = new MonitorConfig(); + $monitorConfig->addRepositories(['https://example.com/some/repo.git']); + + $repository = $monitorConfig->getRepositoryCollection() + ->all()[0]; + $repository->decorateRootFiles([new SplFileInfo($phpstanNeonPath, '', basename($phpstanNeonPath))]); + + return $monitorConfig; + } +}