Skip to content

Commit

Permalink
Merge pull request #331 from dkreuer/feature/different-output-formats
Browse files Browse the repository at this point in the history
Add `--output=json` and `--output=text` option to CLI, to allow for better automation/reporting based on `STDOUT`
  • Loading branch information
Ocramius authored Dec 7, 2021
2 parents 1d996a8 + 3d4cd6f commit 537138b
Show file tree
Hide file tree
Showing 7 changed files with 398 additions and 19 deletions.
86 changes: 67 additions & 19 deletions src/ComposerRequireChecker/Cli/CheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace ComposerRequireChecker\Cli;

use ComposerRequireChecker\ASTLocator\LocateASTFromFiles;
use ComposerRequireChecker\Cli\ResultsWriter\CliJson;
use ComposerRequireChecker\Cli\ResultsWriter\CliText;
use ComposerRequireChecker\DefinedExtensionsResolver\DefinedExtensionsResolver;
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromASTRoots;
use ComposerRequireChecker\DefinedSymbolsLocator\LocateDefinedSymbolsFromComposerRuntimeApi;
Expand All @@ -18,25 +20,28 @@
use ComposerRequireChecker\GeneratorUtil\ComposeGenerators;
use ComposerRequireChecker\JsonLoader;
use ComposerRequireChecker\UsedSymbolsLocator\LocateUsedSymbolsFromASTRoots;
use DateTimeImmutable;
use InvalidArgumentException;
use LogicException;
use PhpParser\ErrorHandler\Collecting as CollectingErrorHandler;
use PhpParser\Lexer;
use PhpParser\ParserFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Webmozart\Assert\Assert;

use function array_combine;
use function array_diff;
use function array_map;
use function array_merge;
use function assert;
use function count;
use function dirname;
use function gettype;
use function implode;
use function in_array;
use function is_string;
use function realpath;
use function sprintf;
Expand Down Expand Up @@ -68,11 +73,37 @@ protected function configure(): void
InputOption::VALUE_NONE,
'this will cause ComposerRequireChecker to ignore errors when files cannot be parsed, otherwise'
. ' errors will be thrown'
)
->addOption(
'output',
null,
InputOption::VALUE_REQUIRED,
'generate output either as "text" or as "json", if specified, "quiet mode" is implied'
);
}

protected function initialize(InputInterface $input, OutputInterface $output): void
{
if ($input->getOption('output') === null) {
return;
}

$optionValue = $input->getOption('output');
assert(is_string($optionValue));

if (! in_array($optionValue, ['text', 'json'])) {
throw new InvalidArgumentException(
'Option "output" must be either of value "json", "text" or omitted altogether'
);
}
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($input->getOption('output') !== null) {
$output->setVerbosity(OutputInterface::VERBOSITY_QUIET);
}

if (! $output->isQuiet()) {
$application = $this->getApplication();
$output->writeln($application !== null ? $application->getLongVersion() : 'Unknown version');
Expand Down Expand Up @@ -147,26 +178,43 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$options->getSymbolWhitelist()
);

if (! $unknownSymbols) {
$output->writeln('There were no unknown symbols found.');

return 0;
switch ($input->getOption('output')) {
case 'json':
$application = $this->getApplication();
$resultsWriter = new CliJson(
static function (string $string) use ($output): void {
$output->write($string, false, OutputInterface::VERBOSITY_QUIET | OutputInterface::OUTPUT_RAW);
},
$application !== null ? $application->getVersion() : 'Unknown version',
static fn () => new DateTimeImmutable()
);
break;
case 'text':
$resultsWriter = new CliText(
$output,
static function (string $string) use ($output): void {
$output->write($string, false, OutputInterface::VERBOSITY_QUIET | OutputInterface::OUTPUT_RAW);
}
);
break;
default:
$resultsWriter = new CliText($output);
}

$output->writeln('The following ' . count($unknownSymbols) . ' unknown symbols were found:');
$table = new Table($output);
$table->setHeaders(['Unknown Symbol', 'Guessed Dependency']);
$guesser = new DependencyGuesser($options);
foreach ($unknownSymbols as $unknownSymbol) {
$guessedDependencies = [];
foreach ($guesser($unknownSymbol) as $guessedDependency) {
$guessedDependencies[] = $guessedDependency;
}

$table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
}

$table->render();
$resultsWriter->write(
array_map(
static function (string $unknownSymbol) use ($guesser): array {
$guessedDependencies = [];
foreach ($guesser($unknownSymbol) as $guessedDependency) {
$guessedDependencies[] = $guessedDependency;
}

return $guessedDependencies;
},
array_combine($unknownSymbols, $unknownSymbols)
),
);

return (int) (bool) $unknownSymbols;
}
Expand Down
55 changes: 55 additions & 0 deletions src/ComposerRequireChecker/Cli/ResultsWriter/CliJson.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace ComposerRequireChecker\Cli\ResultsWriter;

use DateTimeImmutable;

use function json_encode;

use const JSON_THROW_ON_ERROR;

final class CliJson implements ResultsWriter
{
/** @var callable(string): void */
private $writeCallable;
private string $applicationVersion;
/** @var callable(): DateTimeImmutable */
private $nowCallable;

/**
* @param callable(string): void $write
* @param callable(): DateTimeImmutable $now
*/
public function __construct(callable $write, string $applicationVersion, callable $now)
{
$this->writeCallable = $write;
$this->applicationVersion = $applicationVersion;
$this->nowCallable = $now;
}

/**
* {@inheritdoc}
*/
public function write(array $unknownSymbols): void
{
$write = $this->writeCallable;
$now = $this->nowCallable;

$write(
json_encode(
[
'_meta' => [
'composer-require-checker' => [
'version' => $this->applicationVersion,
],
'date' => $now()->format(DateTimeImmutable::ATOM),
],
'unknown-symbols' => $unknownSymbols,
],
JSON_THROW_ON_ERROR
)
);
}
}
57 changes: 57 additions & 0 deletions src/ComposerRequireChecker/Cli/ResultsWriter/CliText.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace ComposerRequireChecker\Cli\ResultsWriter;

use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;

use function count;
use function implode;

final class CliText implements ResultsWriter
{
private OutputInterface $output;
/** @var callable */
private $writeCallable;

public function __construct(OutputInterface $output, ?callable $write = null)
{
$this->output = $output;
if ($write === null) {
$write = static function (string $string) use ($output): void {
$output->write($string);
};
}

$this->writeCallable = $write;
}

/**
* {@inheritdoc}
*/
public function write(array $unknownSymbols): void
{
if (! $unknownSymbols) {
$this->output->writeln('There were no unknown symbols found.');

return;
}

$this->output->writeln('The following ' . count($unknownSymbols) . ' unknown symbols were found:');

$tableOutput = new BufferedOutput();
$table = new Table($tableOutput);
$table->setHeaders(['Unknown Symbol', 'Guessed Dependency']);
foreach ($unknownSymbols as $unknownSymbol => $guessedDependencies) {
$table->addRow([$unknownSymbol, implode("\n", $guessedDependencies)]);
}

$table->render();

$write = $this->writeCallable;
$write($tableOutput->fetch());
}
}
13 changes: 13 additions & 0 deletions src/ComposerRequireChecker/Cli/ResultsWriter/ResultsWriter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace ComposerRequireChecker\Cli\ResultsWriter;

interface ResultsWriter
{
/**
* @param array<array-key, list<string>> $unknownSymbols the unknown symbols found
*/
public function write(array $unknownSymbols): void;
}
58 changes: 58 additions & 0 deletions test/ComposerRequireCheckerTest/Cli/CheckCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

use function dirname;
use function file_put_contents;
use function json_decode;
use function unlink;
use function version_compare;

use const JSON_THROW_ON_ERROR;
use const PHP_VERSION;

final class CheckCommandTest extends TestCase
Expand Down Expand Up @@ -75,6 +77,62 @@ public function testUnknownSymbolsFound(): void
$this->assertStringContainsString('libxml_clear_errors', $display);
}

public function testInvalidOutputOptionValue(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Option "output" must be either of value "json", "text" or omitted altogether');

$this->commandTester->execute([
'composer-json' => dirname(__DIR__, 2) . '/fixtures/unknownSymbols/composer.json',
'--output' => '__invalid__',
]);
}

public function testUnknownSymbolsFoundJsonReport(): void
{
$this->commandTester->execute([
'composer-json' => dirname(__DIR__, 2) . '/fixtures/unknownSymbols/composer.json',
'--output' => 'json',
]);

$this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
$display = $this->commandTester->getDisplay();

/** @var array{'unknown-symbols': array<array-key, list<string>>} $actual */
$actual = json_decode($display, true, JSON_THROW_ON_ERROR);

$this->assertSame(
[
'Doctrine\Common\Collections\ArrayCollection' => [],
'Example\Library\Dependency' => [],
'FILTER_VALIDATE_URL' => ['ext-filter'],
'filter_var' => ['ext-filter'],
'Foo\Bar\Baz' => [],
'libxml_clear_errors' => ['ext-libxml'],
],
$actual['unknown-symbols']
);
}

public function testUnknownSymbolsFoundTextReport(): void
{
$this->commandTester->execute([
'composer-json' => dirname(__DIR__, 2) . '/fixtures/unknownSymbols/composer.json',
'--output' => 'text',
]);

$this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode());
$display = $this->commandTester->getDisplay();

$this->assertStringNotContainsString('The following 6 unknown symbols were found:', $display);
$this->assertStringContainsString('Doctrine\Common\Collections\ArrayCollection', $display);
$this->assertStringContainsString('Example\Library\Dependency', $display);
$this->assertStringContainsString('FILTER_VALIDATE_URL', $display);
$this->assertStringContainsString('filter_var', $display);
$this->assertStringContainsString('Foo\Bar\Baz', $display);
$this->assertStringContainsString('libxml_clear_errors', $display);
}

public function testSelfCheckShowsNoErrors(): void
{
$this->commandTester->execute([
Expand Down
Loading

0 comments on commit 537138b

Please sign in to comment.