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;
+ }
+}