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: