Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/phpstan
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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();
Expand Down
46 changes: 46 additions & 0 deletions src/Command/JsonErrorFormatterDeserializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command;

use Nette\Utils\Json;

class JsonErrorFormatterDeserializer
{

/**
* inverse of @see \PHPStan\Command\ErrorFormatter\JsonErrorFormatter
*
* @param string $jsonString produced by JsonErrorFormatter
*
* @throws \Nette\Utils\JsonException
*/
public static function deserializeErrors(string $jsonString): AnalysisResult
{
$json = Json::decode($jsonString, Json::FORCE_ARRAY);

$notFileSpecificErrors = $json['errors'] ?? [];

$fileSpecificErrors = [];

foreach ($json['files'] ?? [] as $file => ['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
);
}

}
87 changes: 87 additions & 0 deletions src/Command/ReformatCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command;

use PHPStan\Command\ErrorFormatter\ErrorFormatter;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class ReformatCommand extends Command
{

private const NAME = 'reformat';

protected function configure(): void
{
$this->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());
}

}
93 changes: 93 additions & 0 deletions tests/PHPStan/Command/CaptureOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command;

/**
* Test helper to capture and verify output
*/
class CaptureOutput implements Output
{

/** @var string */
private $result = '';

public function writeFormatted(string $message): void
{
$this->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;
}

}
63 changes: 63 additions & 0 deletions tests/PHPStan/Command/JsonErrorFormatterDeserializerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Command;

use PHPStan\Analyser\Error;
use PHPStan\Command\ErrorFormatter\JsonErrorFormatter;
use PHPStan\Testing\TestCase;

class JsonErrorFormatterDeserializerTest extends TestCase
{

public function testJsonIsBidirectional(): void
{
$jsonFormatter = new JsonErrorFormatter(true);

$firstSerialization = new CaptureOutput();
$jsonFormatter->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'
);
}

}