diff --git a/bin/phpstan b/bin/phpstan index b5dff81ecd..a73ce1646b 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -4,6 +4,7 @@ use PHPStan\Command\AnalyseCommand; use PHPStan\Command\ClearResultCacheCommand; use PHPStan\Command\DumpDependenciesCommand; +use PHPStan\Command\ReformatCommand; use PHPStan\Command\WorkerCommand; (function () { @@ -75,6 +76,7 @@ use PHPStan\Command\WorkerCommand; $reversedComposerAutoloaderProjectPaths = array_reverse($composerAutoloaderProjectPaths); $application->add(new AnalyseCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new DumpDependenciesCommand($reversedComposerAutoloaderProjectPaths)); + $application->add(new ReformatCommand()); $application->add(new WorkerCommand($reversedComposerAutoloaderProjectPaths)); $application->add(new ClearResultCacheCommand($reversedComposerAutoloaderProjectPaths)); $application->run(); diff --git a/src/Command/JsonErrorFormatterDeserializer.php b/src/Command/JsonErrorFormatterDeserializer.php new file mode 100644 index 0000000000..ab087add0c --- /dev/null +++ b/src/Command/JsonErrorFormatterDeserializer.php @@ -0,0 +1,46 @@ + ['messages' => $messages]) { + foreach ($messages as $message) { + $fileSpecificErrors[] = new \PHPStan\Analyser\Error( + $message['message'], + $file, + $message['line'] ?? null, + $message['ignorable'] + ); + } + } + + return new AnalysisResult( + $fileSpecificErrors, + $notFileSpecificErrors, + [], + false, + false, + null + ); + } + +} diff --git a/src/Command/ReformatCommand.php b/src/Command/ReformatCommand.php new file mode 100644 index 0000000000..4a0f7c605d --- /dev/null +++ b/src/Command/ReformatCommand.php @@ -0,0 +1,87 @@ +setName(self::NAME) + ->setDescription('Read a previously generated analysis result from STDIN in JSON format and converts it to a different format') + ->setDefinition([ + new InputOption('error-format', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', 'table'), + new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for the run'), + new InputOption('xdebug', null, InputOption::VALUE_NONE, 'Allow running with XDebug for debugging purposes'), + ]); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $memoryLimit = $input->getOption('memory-limit'); + $allowXdebug = $input->getOption('xdebug'); + + if ((!is_string($memoryLimit) && $memoryLimit !== null) + || (!is_bool($allowXdebug)) + ) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $inceptionResult = CommandHelper::begin( + $input, + $output, + ['.'], + null, + $memoryLimit, + null, + [], + null, + null, + $allowXdebug + ); + + $errorFormat = $input->getOption('error-format'); + if (!is_string($errorFormat)) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $errorOutput = $inceptionResult->getErrorOutput(); + $container = $inceptionResult->getContainer(); + + $errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat); + if (!$container->hasService($errorFormatterServiceName)) { + $errorOutput->writeLineFormatted(sprintf( + 'Error formatter "%s" not found. Available error formatters are: %s', + $errorFormat, + implode(', ', array_map(static function (string $name): string { + return substr($name, strlen('errorFormatter.')); + }, $container->findServiceNamesByType(ErrorFormatter::class))) + )); + return 1; + } + + /** @var ErrorFormatter $errorFormatter */ + $errorFormatter = $container->getService($errorFormatterServiceName); + + if (!defined('STDIN')) { + $errorOutput->writeLineFormatted('STDIN is not defined'); + return 1; + } + $jsonString = stream_get_contents(STDIN); + if ($jsonString === false) { + $errorOutput->writeLineFormatted('reading from STDIN failed'); + return 1; + } + $analysisResult = JsonErrorFormatterDeserializer::deserializeErrors($jsonString); + return $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput()); + } + +} diff --git a/tests/PHPStan/Command/CaptureOutput.php b/tests/PHPStan/Command/CaptureOutput.php new file mode 100644 index 0000000000..317a73d410 --- /dev/null +++ b/tests/PHPStan/Command/CaptureOutput.php @@ -0,0 +1,93 @@ +result .= $message; + } + + public function writeLineFormatted(string $message): void + { + $this->result .= $message . "\n"; + } + + public function writeRaw(string $message): void + { + $this->result .= $message; + } + + public function getStyle(): OutputStyle + { + return new class implements OutputStyle { + + public function title(string $message): void + { + } + + public function section(string $message): void + { + } + + public function listing(array $elements): void + { + } + + public function success(string $message): void + { + } + + public function error(string $message): void + { + } + + public function warning(string $message): void + { + } + + public function note(string $message): void + { + } + + public function caution(string $message): void + { + } + + public function table(array $headers, array $rows): void + { + } + + public function newLine(int $count = 1): void + { + } + + public function progressStart(int $max = 0): void + { + } + + public function progressAdvance(int $step = 1): void + { + } + + public function progressFinish(): void + { + } + + }; + } + + public function getResult(): string + { + return $this->result; + } + +} diff --git a/tests/PHPStan/Command/JsonErrorFormatterDeserializerTest.php b/tests/PHPStan/Command/JsonErrorFormatterDeserializerTest.php new file mode 100644 index 0000000000..08ae984d51 --- /dev/null +++ b/tests/PHPStan/Command/JsonErrorFormatterDeserializerTest.php @@ -0,0 +1,63 @@ +formatErrors(self::createSampleAnalysisResult(), $firstSerialization); + + $parsedAnalysisResult = JsonErrorFormatterDeserializer::deserializeErrors($firstSerialization->getResult()); + + $secondSerialization = new CaptureOutput(); + $jsonFormatter->formatErrors($parsedAnalysisResult, $secondSerialization); + + self::assertEquals('{', $firstSerialization->getResult()[0]); + self::assertEquals($firstSerialization->getResult(), $secondSerialization->getResult()); + } + + private static function createSampleAnalysisResult(): AnalysisResult + { + $fileSpecificErrors = [ + new Error( + 'Cushioned proxy should only be used untethered', + 'src/Code.php', + 42, + true + ), + new Error( + 'Liaised mutex found to be grandfathered', + 'src/MoreCode.php', + 43, + false + ), + ]; + + $notFileSpecificErrors = [ + 'Orchestration was under quarantine', + ]; + + $warnings = [ + 'Technobabble detected', + ]; + + return new AnalysisResult( + $fileSpecificErrors, + $notFileSpecificErrors, + $warnings, + false, + false, + './jsontest.neon' + ); + } + +}