diff --git a/README.md b/README.md
index 7682d55..3c0ad8e 100644
--- a/README.md
+++ b/README.md
@@ -17,11 +17,11 @@ Each file looks like this:
# total 1 error
parameters:
- ignoreErrors:
- -
- message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#'
- path: ../app/index.php
- count: 1
+ ignoreErrors:
+ -
+ message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#'
+ path: ../app/index.php
+ count: 1
```
## Installation:
@@ -30,20 +30,40 @@ parameters:
composer require --dev shipmonk/phpstan-baseline-per-identifier
```
-Use [official extension-installer](https://phpstan.org/user-guide/extension-library#installing-extensions) or just load the extension:
+## Usage
+Setup baselines loader, other files will be placed beside that file:
```neon
+# phpstan.neon.dist
includes:
- - vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon
+ - baselines/loader.neon # instead of traditional phpstan-baseline.neon
```
+Run native baseline generation and split it into multiple files via our script:
+```sh
+vendor/bin/phpstan --generate-baseline=baselines/loader.neon && vendor/bin/split-phpstan-baseline baselines/loader.neon
+```
-## Usage:
+_(optional)_ You can simplify generation with e.g. composer script:
+```json
+{
+ "scripts": {
+ "generate:baseline:phpstan": [
+ "@phpstan --generate-baseline=baselines/loader.neon",
+ "@split-phpstan-baseline baselines/loader.neon"
+ ]
+ }
+}
+```
+
+
+Legacy usage
Setup where your baseline files should be stored and include its loader:
```neon
# phpstan.neon.dist
includes:
+ - vendor/shipmonk/phpstan-baseline-per-identifier/extension.neon # or use extension-installer
- baselines/loader.neon
parameters:
@@ -66,14 +86,15 @@ Prepare composer script to simplify generation:
}
```
-Regenerate the baselines:
+
-```sh
-composer generate:baseline:phpstan
-```
+## Cli options
+- ``--tabs`` to use tabs as indents in generated neon files
-## Migration from single baseline
+## Migrating from single baseline
1. `rm phpstan-baseline.neon` (and remove its include from `phpstan.neon.dist`)
2. `mkdir baselines`
-3. `composer generate:baseline:phpstan`
+3. `touch baselines/loader.neon` (and include it in `phpstan.neon.dist`)
+4. Run the split script from above
+
diff --git a/bin/split-phpstan-baseline b/bin/split-phpstan-baseline
new file mode 100644
index 0000000..f8ddc5f
--- /dev/null
+++ b/bin/split-phpstan-baseline
@@ -0,0 +1,88 @@
+#!/usr/bin/env php
+getPath();
+$extension = $splFile->getExtension();
+
+if ($extension !== 'neon') {
+ fwrite(STDERR, "\n! Invalid file extension '$extension' of '$loaderFile', expected neon file\n\n");
+ exit(1);
+}
+
+try {
+ $data = Neon::decodeFile($loaderFile);
+} catch (NeonException $e) {
+ fwrite(STDERR, "\n! Invalid argument, expected a valid neon file: " . $e->getMessage() . "\n\n");
+ exit(1);
+}
+if (!isset($data['parameters']['ignoreErrors'])) {
+ fwrite(
+ STDERR,
+ "\n! Invalid argument, expected neon file with 'parameters.ignoreErrors' key in '$loaderFile'." .
+ "\n - Did you run native baseline generation first?" .
+ "\n - You can so via vendor/bin/phpstan --generate-baseline=$loaderFile\n\n"
+ );
+ exit(1);
+}
+
+$groupedErrors = [];
+foreach ($data['parameters']['ignoreErrors'] as $error) {
+ $identifier = $error['identifier'] ?? 'missing-identifier';
+ unset($error['identifier']);
+ $groupedErrors[$identifier][] = $error;
+}
+
+ksort($groupedErrors);
+
+$loaderData = [];
+
+foreach ($groupedErrors as $identifier => $errors) {
+ $filePath = $folder . '/' . $identifier . '.neon';
+ $loaderData['includes'][] = $identifier . '.neon';
+ $outputData = ['parameters' => ['ignoreErrors' => $errors]];
+ $errorsCount = count($errors);
+ $plural = $errorsCount === 1 ? '' : 's';
+ $prefix = "# total $errorsCount error$plural\n\n";
+ $contents = $prefix . NeonHelper::encode($outputData, $indent);
+ file_put_contents($filePath, $contents);
+ echo "Writing baseline file $filePath with $errorsCount errors\n";
+}
+
+file_put_contents($loaderFile, NeonHelper::encode($loaderData, $indent));
+echo "Writing baseline loader to $loaderFile\n";
diff --git a/composer.json b/composer.json
index 1d1ba4c..450faf1 100644
--- a/composer.json
+++ b/composer.json
@@ -38,6 +38,9 @@
"ShipMonk\\PHPStan\\Baseline\\": "tests/"
}
},
+ "bin": [
+ "bin/split-phpstan-baseline"
+ ],
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": false,
diff --git a/src/BaselinePerIdentifierFormatter.php b/src/BaselinePerIdentifierFormatter.php
index bcfee9f..ee260e4 100644
--- a/src/BaselinePerIdentifierFormatter.php
+++ b/src/BaselinePerIdentifierFormatter.php
@@ -3,7 +3,6 @@
namespace ShipMonk\PHPStan\Baseline;
use LogicException;
-use Nette\Neon\Neon;
use PHPStan\Command\AnalysisResult;
use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use PHPStan\Command\Output;
@@ -14,14 +13,15 @@
use function implode;
use function ksort;
use function preg_quote;
-use function preg_replace;
use function realpath;
use function sprintf;
use function str_repeat;
-use function trim;
use const DIRECTORY_SEPARATOR;
use const SORT_STRING;
+/**
+ * @deprecated Use new approach, see readme
+ */
class BaselinePerIdentifierFormatter implements ErrorFormatter
{
@@ -86,9 +86,9 @@ public function formatErrors(
foreach ($fileErrorsCounts as $message => $count) {
$errorsToOutput[] = [
- 'message' => $this->escape('#^' . preg_quote($message, '#') . '$#'),
+ 'message' => NeonHelper::escape('#^' . preg_quote($message, '#') . '$#'),
'count' => $count,
- 'path' => $this->escape($file),
+ 'path' => NeonHelper::escape($file),
];
}
}
@@ -99,8 +99,9 @@ public function formatErrors(
$output->writeLineFormatted(sprintf('Writing baseline file %s with %d errors', $baselineFilePath, $errorsCount));
- $prefix = "# total $errorsCount errors\n\n";
- $contents = $prefix . $this->getNeon(['parameters' => ['ignoreErrors' => $errorsToOutput]]);
+ $plurality = $errorsCount === 1 ? '' : 's';
+ $prefix = "# total $errorsCount error$plurality\n\n";
+ $contents = $prefix . NeonHelper::encode(['parameters' => ['ignoreErrors' => $errorsToOutput]], $this->indent);
$written = file_put_contents($baselineFilePath, $contents);
if ($written === false) {
@@ -108,29 +109,19 @@ public function formatErrors(
}
}
- $writtenLoader = file_put_contents($this->baselinesDir . '/loader.neon', $this->getNeon(['includes' => $includes]));
+ $writtenLoader = file_put_contents($this->baselinesDir . '/loader.neon', NeonHelper::encode(['includes' => $includes], $this->indent));
if ($writtenLoader === false) {
throw new LogicException('Error while writing to ' . $this->baselinesDir . '/loader.neon');
}
- return 0;
- }
-
- private function getNeon(mixed $data): string
- {
- return trim(Neon::encode($data, blockMode: true, indentation: $this->indent)) . "\n";
- }
+ $output->writeLineFormatted('');
+ $output->writeLineFormatted('⚠️ You are using deprecated approach to split baselines which cannot utilize PHPStan result cache ⚠️');
+ $output->writeLineFormatted(' Consider switching to new approach via:');
+ $output->writeLineFormatted(" vendor/bin/phpstan --generate-baseline=$this->baselinesDir/loader.neon && vendor/bin/split-phpstan-baseline $this->baselinesDir/loader.neon");
+ $output->writeLineFormatted('');
- private function escape(string $value): string
- {
- $return = preg_replace('#^@|%#', '$0$0', $value);
-
- if ($return === null) {
- throw new LogicException('Error while escaping ' . $value);
- }
-
- return $return;
+ return 0;
}
private function getPathDifference(string $from, string $to): string
diff --git a/src/NeonHelper.php b/src/NeonHelper.php
new file mode 100644
index 0000000..c4e24d0
--- /dev/null
+++ b/src/NeonHelper.php
@@ -0,0 +1,29 @@
+ [
+ 'ignoreErrors' => [
+ [
+ 'message' => '#^Error simple$#',
+ 'count' => 1,
+ 'path' => '../app/file.php',
+ 'identifier' => 'sample.identifier',
+ ],
+ [
+ 'message' => '#^Error to escape \'\#$#',
+ 'count' => 1,
+ 'path' => '../app/config.php',
+ 'identifier' => 'another.identifier',
+ ],
+ [
+ 'message' => '#^Error 3$#',
+ 'count' => 1,
+ 'path' => '../app/index.php',
+ ],
+ ],
+ ],
+ ];
+
+ file_put_contents($fakeRoot . '/baselines/loader.neon', Neon::encode($squashed));
+
+ $this->runCommand('php bin/split-phpstan-baseline ' . $fakeRoot . '/baselines/loader.neon', __DIR__ . '/..', 0);
+
+ self::assertFileEquals(__DIR__ . '/Rule/data/baselines/loader.neon', $fakeRoot . '/baselines/loader.neon');
+ self::assertFileEquals(__DIR__ . '/Rule/data/baselines/sample.identifier.neon', $fakeRoot . '/baselines/sample.identifier.neon');
+ self::assertFileEquals(__DIR__ . '/Rule/data/baselines/another.identifier.neon', $fakeRoot . '/baselines/another.identifier.neon');
+ self::assertFileEquals(__DIR__ . '/Rule/data/baselines/missing-identifier.neon', $fakeRoot . '/baselines/missing-identifier.neon');
+ }
+
+ private function runCommand(
+ string $command,
+ string $cwd,
+ int $expectedExitCode,
+ ?string $expectedOutputContains = null,
+ ?string $expectedErrorContains = null
+ ): void
+ {
+ $desc = [
+ ['pipe', 'r'],
+ ['pipe', 'w'],
+ ['pipe', 'w'],
+ ];
+
+ $procHandle = proc_open($command, $desc, $pipes, $cwd);
+ self::assertNotFalse($procHandle);
+
+ /** @var list $pipes */
+ $output = stream_get_contents($pipes[1]); // @phpstan-ignore offsetAccess.notFound
+ $errorOutput = stream_get_contents($pipes[2]); // @phpstan-ignore offsetAccess.notFound
+ self::assertNotFalse($output);
+ self::assertNotFalse($errorOutput);
+
+ foreach ($pipes as $pipe) {
+ fclose($pipe);
+ }
+
+ $extraInfo = "Output was:\n" . $output . "\nError was:\n" . $errorOutput . "\n";
+
+ $exitCode = proc_close($procHandle);
+ self::assertSame(
+ $expectedExitCode,
+ $exitCode,
+ $extraInfo,
+ );
+
+ if ($expectedOutputContains !== null) {
+ self::assertStringContainsString(
+ $expectedOutputContains,
+ $output,
+ $extraInfo,
+ );
+ }
+
+ if ($expectedErrorContains !== null) {
+ self::assertStringContainsString(
+ $expectedErrorContains,
+ $errorOutput,
+ $extraInfo,
+ );
+ }
+ }
+
+}
diff --git a/tests/Rule/data/baselines/another.identifier.neon b/tests/Rule/data/baselines/another.identifier.neon
index 93e36b0..8ebec17 100644
--- a/tests/Rule/data/baselines/another.identifier.neon
+++ b/tests/Rule/data/baselines/another.identifier.neon
@@ -1,4 +1,4 @@
-# total 1 errors
+# total 1 error
parameters:
ignoreErrors:
diff --git a/tests/Rule/data/baselines/missing-identifier.neon b/tests/Rule/data/baselines/missing-identifier.neon
index 15a2473..5f0fe31 100644
--- a/tests/Rule/data/baselines/missing-identifier.neon
+++ b/tests/Rule/data/baselines/missing-identifier.neon
@@ -1,4 +1,4 @@
-# total 1 errors
+# total 1 error
parameters:
ignoreErrors:
diff --git a/tests/Rule/data/baselines/sample.identifier.neon b/tests/Rule/data/baselines/sample.identifier.neon
index 6c25030..371a4de 100644
--- a/tests/Rule/data/baselines/sample.identifier.neon
+++ b/tests/Rule/data/baselines/sample.identifier.neon
@@ -1,4 +1,4 @@
-# total 1 errors
+# total 1 error
parameters:
ignoreErrors: