diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index c7c3e9f8aba..72b16060ad2 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -11,6 +11,7 @@ use PHPStan\Command\Symfony\SymfonyOutput; use PHPStan\Command\Symfony\SymfonyStyle; use PHPStan\File\CouldNotWriteFileException; +use PHPStan\File\FileReader; use PHPStan\File\FileWriter; use PHPStan\File\ParentDirectoryRelativePathHelper; use PHPStan\File\PathNotFoundException; @@ -32,6 +33,7 @@ use function is_array; use function is_bool; use function is_dir; +use function is_file; use function is_string; use function mkdir; use function pathinfo; @@ -279,10 +281,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $baselineFileDirectory = dirname($generateBaselineFile); $baselineErrorFormatter = new BaselineNeonErrorFormatter(new ParentDirectoryRelativePathHelper($baselineFileDirectory)); + $existingBaselineContent = is_file($generateBaselineFile) ? FileReader::read($generateBaselineFile) : ''; + $streamOutput = $this->createStreamOutput(); $errorConsoleStyle = new ErrorsConsoleStyle(new StringInput(''), $streamOutput); $baselineOutput = new SymfonyOutput($streamOutput, new SymfonyStyle($errorConsoleStyle)); - $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput); + $baselineErrorFormatter->formatErrors($analysisResult, $baselineOutput, $existingBaselineContent); $stream = $streamOutput->getStream(); rewind($stream); diff --git a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php index 83cc9a13c0d..545eeed111c 100644 --- a/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php +++ b/src/Command/ErrorFormatter/BaselineNeonErrorFormatter.php @@ -4,6 +4,7 @@ use Nette\DI\Helpers; use Nette\Neon\Neon; +use Nette\Utils\Strings; use PHPStan\Command\AnalysisResult; use PHPStan\Command\Output; use PHPStan\File\RelativePathHelper; @@ -23,10 +24,11 @@ public function __construct(private RelativePathHelper $relativePathHelper) public function formatErrors( AnalysisResult $analysisResult, Output $output, + string $existingBaselineContent, ): int { if (!$analysisResult->hasErrors()) { - $output->writeRaw($this->getNeon([])); + $output->writeRaw($this->getNeon([], $existingBaselineContent)); return 0; } @@ -61,7 +63,7 @@ public function formatErrors( } } - $output->writeRaw($this->getNeon($errorsToOutput)); + $output->writeRaw($this->getNeon($errorsToOutput, $existingBaselineContent)); return 1; } @@ -69,7 +71,7 @@ public function formatErrors( /** * @param array $ignoreErrors */ - private function getNeon(array $ignoreErrors): string + private function getNeon(array $ignoreErrors, string $existingBaselineContent): string { $neon = Neon::encode([ 'parameters' => [ @@ -81,7 +83,16 @@ private function getNeon(array $ignoreErrors): string throw new ShouldNotHappenException(); } - return substr($neon, 0, -1); + if ($existingBaselineContent === '') { + return substr($neon, 0, -1); + } + + $existingBaselineContentEndOfFileNewlinesMatches = Strings::match($existingBaselineContent, "~(\n)+$~"); + $existingBaselineContentEndOfFileNewlines = $existingBaselineContentEndOfFileNewlinesMatches !== null + ? $existingBaselineContentEndOfFileNewlinesMatches[0] + : ''; + + return substr($neon, 0, -2) . $existingBaselineContentEndOfFileNewlines; } } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index 7cbcb7adb9e..e6e23f8e46e 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -128,6 +128,7 @@ public function testFormatErrors( $this->assertSame($exitCode, $formatter->formatErrors( $this->getAnalysisResult($numFileErrors, $numGenericErrors), $this->getOutput(), + '' ), sprintf('%s: response code do not match', $message)); $this->assertSame(trim(Neon::encode(['parameters' => ['ignoreErrors' => $expected]], Neon::BLOCK)), trim($this->getOutputContent()), sprintf('%s: output do not match', $message)); @@ -150,6 +151,7 @@ public function testFormatErrorMessagesRegexEscape(): void $formatter->formatErrors( $result, $this->getOutput(), + '' ); self::assertSame( @@ -186,6 +188,7 @@ public function testEscapeDiNeon(): void $formatter->formatErrors( $result, $this->getOutput(), + '' ); self::assertSame( trim( @@ -248,6 +251,7 @@ public function testOutputOrdering(array $errors): void $formatter->formatErrors( $result, $this->getOutput(), + '' ); self::assertSame( trim(Neon::encode([ @@ -300,14 +304,63 @@ public function testOutputOrdering(array $errors): void */ public function endOfFileNewlinesProvider(): Generator { + $existingBaselineContentWithoutEndNewlines = 'parameters: + ignoreErrors: + - + message: "#^Existing error$#" + count: 1 + path: TestfileA'; + yield 'one error' => [ 'errors' => [ new Error('Error #1', 'TestfileA', 1), ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n", + 'expectedNewlinesCount' => 1, ]; yield 'no errors' => [ 'errors' => [], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n", + 'expectedNewlinesCount' => 1, + ]; + + yield 'one error with 2 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n", + 'expectedNewlinesCount' => 2, + ]; + + yield 'no errors with 2 newlines' => [ + 'errors' => [], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n", + 'expectedNewlinesCount' => 2, + ]; + + yield 'one error with 0 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines, + 'expectedNewlinesCount' => 0, + ]; + + yield 'one error with 3 newlines' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => $existingBaselineContentWithoutEndNewlines . "\n\n\n", + 'expectedNewlinesCount' => 3, + ]; + + yield 'empty existing baseline' => [ + 'errors' => [ + new Error('Error #1', 'TestfileA', 1), + ], + 'existingBaselineContent' => '', + 'expectedNewlinesCount' => 1, ]; } @@ -316,7 +369,11 @@ public function endOfFileNewlinesProvider(): Generator * * @param list $errors */ - public function testEndOfFileNewlines(array $errors): void + public function testEndOfFileNewlines( + array $errors, + string $existingBaselineContent, + int $expectedNewlinesCount, + ): void { $formatter = new BaselineNeonErrorFormatter(new SimpleRelativePathHelper(self::DIRECTORY_PATH)); $result = new AnalysisResult( @@ -341,6 +398,7 @@ public function testEndOfFileNewlines(array $errors): void $formatter->formatErrors( $result, $output, + $existingBaselineContent ); rewind($outputStream->getStream()); @@ -350,8 +408,10 @@ public function testEndOfFileNewlines(array $errors): void throw new ShouldNotHappenException(); } - Assert::assertSame("\n", substr($content, -1)); - Assert::assertNotSame("\n", substr($content, -2, 1)); + if ($expectedNewlinesCount > 0) { + Assert::assertSame(str_repeat("\n", $expectedNewlinesCount), substr($content, -$expectedNewlinesCount)); + } + Assert::assertNotSame("\n", substr($content, -($expectedNewlinesCount + 1), 1)); } }