From 1054a6944b4daa2b5428adb8cfb45514dc1f543a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20FIDRY?= Date: Wed, 1 Nov 2023 12:28:36 +0100 Subject: [PATCH] feat: Add an inspect command This adds a command akin to `box process` which allows to output the scopped content of a singular file. The command takes similar input as the `add-prefix` regarding the configuration and behaves identically except it will not try to execute the `init` command. The scopped content can also be used as raw input by passing `--quiet`. Closes #771. --- src/Console/Application.php | 6 + src/Console/Command/InspectCommand.php | 252 ++++++++++++++++++ .../AddInspectCommandIntegrationTest.php | 132 +++++++++ 3 files changed, 390 insertions(+) create mode 100644 src/Console/Command/InspectCommand.php create mode 100644 tests/Console/Command/AddInspectCommandIntegrationTest.php diff --git a/src/Console/Application.php b/src/Console/Application.php index 95614fdb7..f652c6f20 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -17,6 +17,7 @@ use Fidry\Console\Application\Application as FidryApplication; use Humbug\PhpScoper\Console\Command\AddPrefixCommand; use Humbug\PhpScoper\Console\Command\InitCommand; +use Humbug\PhpScoper\Console\Command\InspectCommand; use Humbug\PhpScoper\Console\Command\InspectSymbolCommand; use Humbug\PhpScoper\Container; use Symfony\Component\Console\Helper\FormatterHelper; @@ -103,6 +104,11 @@ public function getCommands(): array $this, $this->container->getConfigurationFactory(), ), + new InspectCommand( + $this->container->getFileSystem(), + $this->container->getScoperFactory(), + $this->container->getConfigurationFactory(), + ), new InspectSymbolCommand( $this->container->getFileSystem(), $this->container->getConfigurationFactory(), diff --git a/src/Console/Command/InspectCommand.php b/src/Console/Command/InspectCommand.php new file mode 100644 index 000000000..562a358d8 --- /dev/null +++ b/src/Console/Command/InspectCommand.php @@ -0,0 +1,252 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Humbug\PhpScoper\Console\Command; + +use Fidry\Console\Command\Command; +use Fidry\Console\Command\CommandAware; +use Fidry\Console\Command\CommandAwareness; +use Fidry\Console\Command\Configuration as CommandConfiguration; +use Fidry\Console\ExitCode; +use Fidry\Console\Input\IO; +use Humbug\PhpScoper\Configuration\Configuration; +use Humbug\PhpScoper\Configuration\ConfigurationFactory; +use Humbug\PhpScoper\Console\ConfigLoader; +use Humbug\PhpScoper\Scoper\ScoperFactory; +use Humbug\PhpScoper\Symbol\SymbolsRegistry; +use InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Filesystem\Path; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; +use function array_key_exists; +use function Safe\getcwd; +use function sprintf; +use const DIRECTORY_SEPARATOR; + +/** + * @private + */ +final class InspectCommand implements Command, CommandAware +{ + use CommandAwareness; + + private const FILE_PATH_ARG = 'file-path'; + private const PREFIX_OPT = 'prefix'; + private const CONFIG_FILE_OPT = 'config'; + private const NO_CONFIG_OPT = 'no-config'; + + public function __construct( + private readonly Filesystem $fileSystem, + private readonly ScoperFactory $scoperFactory, + private readonly ConfigurationFactory $configFactory, + ) { + } + + public function getConfiguration(): CommandConfiguration + { + return new CommandConfiguration( + 'inspect', + 'Outputs the processed file content based on the configuration.', + '', + [ + new InputArgument( + self::FILE_PATH_ARG, + InputArgument::REQUIRED, + 'The file path to process.', + ), + ], + [ + ChangeableDirectory::createOption(), + new InputOption( + self::PREFIX_OPT, + 'p', + InputOption::VALUE_REQUIRED, + 'The namespace prefix to add.', + '', + ), + new InputOption( + self::CONFIG_FILE_OPT, + 'c', + InputOption::VALUE_REQUIRED, + sprintf( + 'Configuration file. Will use "%s" if found by default.', + ConfigurationFactory::DEFAULT_FILE_NAME, + ), + ), + new InputOption( + self::NO_CONFIG_OPT, + null, + InputOption::VALUE_NONE, + 'Do not look for a configuration file.', + ), + ], + ); + } + + public function execute(IO $io): int + { + $io->newLine(); + + ChangeableDirectory::changeWorkingDirectory($io); + + // Only get current working directory _after_ we changed to the desired + // working directory + $cwd = getcwd(); + + $filePath = $this->getFilePath($io, $cwd); + $config = $this->retrieveConfig($io, [$filePath], $cwd); + + if (array_key_exists($filePath, $config->getExcludedFilesWithContents())) { + $io->writeln('The file was ignored as part of the excluded files.'); + + return ExitCode::SUCCESS; + } + + $symbolsRegistry = new SymbolsRegistry(); + $fileContents = $config->getFilesWithContents()[$filePath][1]; + + $scoppedContents = $this->scopeFile($config, $symbolsRegistry, $filePath, $fileContents); + + $this->printScoppedContents($io, $scoppedContents, $symbolsRegistry); + + return ExitCode::SUCCESS; + } + + /** + * @param list $paths + */ + private function retrieveConfig(IO $io, array $paths, string $cwd): Configuration + { + $configLoader = new ConfigLoader( + $this->getCommandRegistry(), + $this->fileSystem, + $this->configFactory, + ); + + return $configLoader->loadConfig( + $io, + $io->getOption(self::PREFIX_OPT)->asString(), + $io->getOption(self::NO_CONFIG_OPT)->asBoolean(), + $this->getConfigFilePath($io, $cwd), + ConfigurationFactory::DEFAULT_FILE_NAME, + true, + $paths, + $cwd, + ); + } + + /** + * @return non-empty-string|null + */ + private function getConfigFilePath(IO $io, string $cwd): ?string + { + $configFilePath = (string) $io->getOption(self::CONFIG_FILE_OPT)->asNullableString(); + + return '' === $configFilePath ? null : $this->canonicalizePath($configFilePath, $cwd); + } + + /** + * @return non-empty-string + */ + private function getFilePath(IO $io, string $cwd): string + { + return $this->canonicalizePath( + $io->getArgument(self::FILE_PATH_ARG)->asNonEmptyString(), + $cwd, + ); + } + + /** + * @return non-empty-string Absolute canonical path + */ + private function canonicalizePath(string $path, string $cwd): string + { + $canonicalPath = Path::canonicalize( + $this->fileSystem->isAbsolutePath($path) + ? $path + : $cwd.DIRECTORY_SEPARATOR.$path, + ); + + if ('' === $canonicalPath) { + throw new InvalidArgumentException('Cannot canonicalize empty path and empty working directory'); + } + + return $canonicalPath; + } + + private function scopeFile( + Configuration $config, + SymbolsRegistry $symbolsRegistry, + string $filePath, + string $fileContents, + ): string { + $scoper = $this->scoperFactory->createScoper( + $config, + $symbolsRegistry, + ); + + return $scoper->scope( + $filePath, + $fileContents, + ); + } + + private function printScoppedContents( + IO $io, + string $scoppedContents, + SymbolsRegistry $symbolsRegistry, + ): void { + if ($io->isQuiet()) { + $io->writeln($scoppedContents, OutputInterface::VERBOSITY_QUIET); + } else { + $io->writeln([ + 'Scopped contents:', + '', + '"""', + $scoppedContents, + '"""', + ]); + + $io->writeln([ + '', + 'Symbols Registry:', + '', + '"""', + self::exportSymbolsRegistry($symbolsRegistry, $io), + '"""', + ]); + } + } + + private static function exportSymbolsRegistry(SymbolsRegistry $symbolsRegistry, IO $io): string + { + $cloner = new VarCloner(); + $cloner->setMaxItems(-1); + $cloner->setMaxString(-1); + + $cliDumper = new CliDumper(); + if ($io->isDecorated()) { + $cliDumper->setColors(true); + } + + return (string) $cliDumper->dump( + $cloner->cloneVar($symbolsRegistry), + true, + ); + } +} diff --git a/tests/Console/Command/AddInspectCommandIntegrationTest.php b/tests/Console/Command/AddInspectCommandIntegrationTest.php new file mode 100644 index 000000000..af0a3df64 --- /dev/null +++ b/tests/Console/Command/AddInspectCommandIntegrationTest.php @@ -0,0 +1,132 @@ +, + * Pádraic Brady + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Console\Command; + +use Fidry\Console\ExitCode; +use Fidry\Console\Test\AppTester; +use Fidry\Console\Test\OutputAssertions; +use Humbug\PhpScoper\Console\Application; +use Humbug\PhpScoper\Container; +use PHPUnit\Framework\TestCase; +use function Safe\preg_replace; + +/** + * @coversNothing + * + * @group integration + * + * @internal + */ +class AddInspectCommandIntegrationTest extends TestCase +{ + private const FIXTURE_PATH = __DIR__.'/../../../fixtures/set002/original'; + + private AppTester $appTester; + + protected function setUp(): void + { + parent::setUp(); + + $application = new Application( + new Container(), + 'TestVersion', + '28/01/2020', + false, + false, + ); + + $this->appTester = AppTester::fromConsoleApp($application); + } + + public function test_it_shows_the_scopped_content_of_the_file_given(): void + { + $input = [ + 'inspect', + '--prefix' => 'MyPrefix', + 'file-path' => self::FIXTURE_PATH.'/file.php', + '--no-interaction' => null, + '--no-config' => null, + ]; + + $this->appTester->run($input); + + OutputAssertions::assertSameOutput( + <<<'PHP' + + Scopped contents: + + """ + appTester, + self::normalizeSymbolsRegistryReference(...), + ); + } + + public function test_it_shows_the_raw_scopped_content_of_the_file_given_in_quiet_mode(): void + { + $input = [ + 'inspect', + '--prefix' => 'MyPrefix', + 'file-path' => self::FIXTURE_PATH.'/file.php', + '--no-interaction' => null, + '--no-config' => null, + '--quiet' => null, + ]; + + $this->appTester->run($input); + + OutputAssertions::assertSameOutput( + <<<'PHP' + appTester, + ); + } + + private static function normalizeSymbolsRegistryReference(string $output): string + { + return preg_replace( + '/ \{#\d{3,}/', + ' {#140', + $output, + ); + } +}