From fe6be142b1a84a1e935b8953be18626336a2bf36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20TAMARELLE?= Date: Tue, 30 Nov 2021 01:03:23 +0100 Subject: [PATCH 01/15] Add completion on commands --- src/Composer/Command/ArchiveCommand.php | 13 +++ src/Composer/Command/BaseCommand.php | 100 +++++++++++++++- src/Composer/Command/CreateProjectCommand.php | 7 ++ src/Composer/Command/DependsCommand.php | 7 ++ src/Composer/Command/ExecCommand.php | 53 ++++++--- src/Composer/Command/GlobalCommand.php | 13 +++ src/Composer/Command/HomeCommand.php | 7 ++ src/Composer/Command/InitCommand.php | 10 ++ src/Composer/Command/InstallCommand.php | 7 ++ src/Composer/Command/OutdatedCommand.php | 7 ++ src/Composer/Command/ProhibitsCommand.php | 7 ++ src/Composer/Command/ReinstallCommand.php | 7 ++ src/Composer/Command/RemoveCommand.php | 7 ++ src/Composer/Command/RequireCommand.php | 7 ++ src/Composer/Command/RunScriptCommand.php | 9 ++ src/Composer/Command/SearchCommand.php | 9 ++ src/Composer/Command/ShowCommand.php | 14 ++- src/Composer/Command/SuggestsCommand.php | 7 ++ src/Composer/Command/UpdateCommand.php | 7 ++ .../Package/Version/VersionGuesser.php | 6 + src/Composer/Util/HttpDownloader.php | 38 ++++-- .../Test/CompletionFunctionalTest.php | 109 ++++++++++++++++++ 22 files changed, 423 insertions(+), 28 deletions(-) create mode 100644 tests/Composer/Test/CompletionFunctionalTest.php diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index fa450a6da7d1..2629db7e95c1 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -27,6 +27,8 @@ use Composer\Util\Loop; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -39,6 +41,17 @@ */ class ArchiveCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($this->completeAvailablePackage($input, $suggestions)) { + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['tar', 'tar.gz', 'tar.bz2', 'zip']); + } + } + /** * @return void */ diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index c4f2ace111fb..dc88c5e17908 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -20,10 +20,19 @@ use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\IO\IOInterface; use Composer\IO\NullIO; +use Composer\Package\Package; +use Composer\Package\PackageInterface; use Composer\Plugin\PreCommandRunEvent; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginEvents; +use Composer\Repository\CompositeRepository; +use Composer\Repository\InstalledRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Repository\RootPackageRepository; use Composer\Util\Platform; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; @@ -317,7 +326,96 @@ protected function getPlatformRequirementFilter(InputInterface $input): Platform } /** - * @param array $requirements + * Suggestion values for "prefer-install" option + */ + protected function completePreferInstall(CompletionInput $input, CompletionSuggestions $suggestions): bool + { + if ($input->mustSuggestOptionValuesFor('prefer-install')) { + $suggestions->suggestValues(['dist', 'source', 'auto']); + + return true; + } + + return false; + } + + /** + * Suggest package names from installed ones. + */ + protected function completeInstalledPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool + { + if (!$input->mustSuggestArgumentValuesFor('packages') && + !$input->mustSuggestArgumentValuesFor('package') && + !$input->mustSuggestOptionValuesFor('ignore') + ) { + return false; + } + + $composer = $this->getComposer(); + $installedRepos = [new RootPackageRepository(clone $composer->getPackage())]; + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = $locker->getLockedRepository(true); + } else { + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); + } + + $installedRepo = new InstalledRepository($installedRepos); + $suggestions->suggestValues(array_map(function (PackageInterface $package) { + return $package->getName(); + }, $installedRepo->getPackages())); + + return true; + } + + /** + * Suggest package names available on all configured repositories. + */ + protected function completeAvailablePackage(CompletionInput $input, CompletionSuggestions $suggestions): bool + { + if (!$input->mustSuggestArgumentValuesFor('packages') && + !$input->mustSuggestArgumentValuesFor('package') && + !$input->mustSuggestOptionValuesFor('require') && + !$input->mustSuggestOptionValuesFor('require-dev') + ) { + return false; + } + + $composer = $this->getComposer(); + $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + + $packages = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + + foreach (array_slice($packages, 0, 150) as $package) { + $suggestions->suggestValue($package['name']); + } + + return true; + } + + /** + * Suggests ext- packages from the ones available on the currently-running PHP + */ + protected function completePlatformPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool + { + if (!$input->mustSuggestOptionValuesFor('require') && + !$input->mustSuggestOptionValuesFor('require-dev') && + !str_starts_with($input->getCompletionValue(), 'ext-') + ) { + return false; + } + + $repos = new PlatformRepository([], $this->getComposer()->getConfig()->get('platform') ?? []); + $suggestions->suggestValues(array_map(function (PackageInterface $package) { + return $package->getName(); + }, $repos->getPackages())); + + return true; + } + + /** + * @param array $requirements * * @return array */ diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 1e98f836df96..341caaaa8fce 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -33,6 +33,8 @@ use Composer\Repository\RepositorySet; use Composer\Script\ScriptEvents; use Composer\Util\Silencer; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -55,6 +57,11 @@ */ class CreateProjectCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); + } + /** * @var SuggestedPackagesReporter */ diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index 0dbbb03f2cf7..d5c65ed46438 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -22,6 +24,11 @@ */ class DependsCommand extends BaseDependencyCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions); + } + /** * Configure command metadata. * diff --git a/src/Composer/Command/ExecCommand.php b/src/Composer/Command/ExecCommand.php index 049f6b687a05..1afdb6e8d7b5 100644 --- a/src/Composer/Command/ExecCommand.php +++ b/src/Composer/Command/ExecCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -22,6 +24,13 @@ */ class ExecCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('binary')) { + $suggestions->suggestValues($this->getBinaries(false)); + } + } + /** * @return void */ @@ -52,14 +61,11 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->requireComposer(); - $binDir = $composer->getConfig()->get('bin-dir'); - if ($input->getOption('list') || null === $input->getArgument('binary')) { - $bins = glob($binDir . '/*'); - $bins = array_merge($bins, array_map(function ($e) { - return "$e (local)"; - }, $composer->getPackage()->getBinaries())); + if ($input->getOption('list') || !$input->getArgument('binary')) { + $bins = $this->getBinaries(true); + if (count($bins) > 0) { + $binDir = $composer->getConfig()->get('bin-dir'); - if (!$bins) { throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)"); } @@ -70,13 +76,6 @@ protected function execute(InputInterface $input, OutputInterface $output) ); foreach ($bins as $bin) { - // skip .bat copies - if (isset($previousBin) && $bin === $previousBin.'.bat') { - continue; - } - - $previousBin = $bin; - $bin = basename($bin); $this->getIO()->write( <<- $bin @@ -105,4 +104,30 @@ protected function execute(InputInterface $input, OutputInterface $output) return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args')); } + + private function getBinaries(bool $forDisplay): array + { + $composer = $this->getComposer(); + $binDir = $composer->getConfig()->get('bin-dir'); + $bins = glob($binDir . '/*'); + $localBins = $composer->getPackage()->getBinaries(); + if ($forDisplay) { + $localBins = array_map(function ($e) { + return "$e (local)"; + }, $localBins); + } + + $binaries = []; + foreach (array_merge($bins, $localBins) as $bin) { + // skip .bat copies + if (isset($previousBin) && $bin === $previousBin.'.bat') { + continue; + } + + $previousBin = $bin; + $binaries[] = basename($bin); + } + + return $binaries; + } } diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index 3ec1254271f0..ea9b496e3d1a 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -16,6 +16,9 @@ use Composer\Pcre\Preg; use Composer\Util\Filesystem; use Composer\Util\Platform; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\StringInput; @@ -26,6 +29,16 @@ */ class GlobalCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $application = $this->getApplication(); + if ($input->mustSuggestArgumentValuesFor('command-name')) { + $suggestions->suggestValues(array_filter(array_map(function (Command $command) { + return $command->isHidden() ? null : $command->getName(); + }, $application->all()))); + } + } + /** * @return void */ diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index dad4e3a7540f..db715617cfc9 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -18,6 +18,8 @@ use Composer\Repository\RepositoryFactory; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; @@ -28,6 +30,11 @@ */ class HomeCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions); + } + /** * @inheritDoc * diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index f923f692913d..8af6719c491b 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -23,6 +23,8 @@ use Composer\Repository\RepositoryFactory; use Composer\Util\Filesystem; use Composer\Util\Silencer; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -42,6 +44,14 @@ class InitCommand extends BaseCommand /** @var array */ private $gitConfig; + /** @var RepositorySet[] */ + private $repositorySets; + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeAvailablePackage($input, $suggestions); + } + /** * @inheritDoc * diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 13b6ff400628..cbc51042dd31 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -16,6 +16,8 @@ use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\HttpDownloader; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -29,6 +31,11 @@ */ class InstallCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completePreferInstall($input, $suggestions) || $this->completeInstalledPackage($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php index def190c04d7b..120291b6a8d5 100644 --- a/src/Composer/Command/OutdatedCommand.php +++ b/src/Composer/Command/OutdatedCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\ArrayInput; @@ -23,6 +25,11 @@ */ class OutdatedCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/ProhibitsCommand.php b/src/Composer/Command/ProhibitsCommand.php index 6da462084f7b..45908ec40b7f 100644 --- a/src/Composer/Command/ProhibitsCommand.php +++ b/src/Composer/Command/ProhibitsCommand.php @@ -12,6 +12,8 @@ namespace Composer\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -22,6 +24,11 @@ */ class ProhibitsCommand extends BaseDependencyCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeAvailablePackage($input, $suggestions); + } + /** * Configure command metadata. * diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index 8150bfa22736..c845fc3f596b 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -22,6 +22,8 @@ use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; use Composer\Util\Platform; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -32,6 +34,11 @@ */ class ReinstallCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 9d1fa4965027..d558e4c0c54a 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -20,6 +20,8 @@ use Composer\Plugin\PluginEvents; use Composer\Json\JsonFile; use Composer\Factory; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -32,6 +34,11 @@ */ class RemoveCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 230e449c6ef4..ff8d399762d1 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -14,6 +14,8 @@ use Composer\DependencyResolver\Request; use Composer\Util\Filesystem; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -58,6 +60,11 @@ class RequireCommand extends BaseCommand /** @var bool */ private $dependencyResolutionCompleted = false; + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completePlatformPackage($input, $suggestions) || $this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/RunScriptCommand.php b/src/Composer/Command/RunScriptCommand.php index c626d9ccf04f..2b035cab2ee5 100644 --- a/src/Composer/Command/RunScriptCommand.php +++ b/src/Composer/Command/RunScriptCommand.php @@ -16,6 +16,8 @@ use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; use Composer\Util\Platform; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; @@ -44,6 +46,13 @@ class RunScriptCommand extends BaseCommand ScriptEvents::POST_AUTOLOAD_DUMP, ); + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('script')) { + $suggestions->suggestValues(array_keys($this->getComposer()->getPackage()->getScripts())); + } + } + /** * @return void */ diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index f701e40d91d4..f7dfa2001724 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -14,6 +14,8 @@ use Composer\Factory; use Composer\Json\JsonFile; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; @@ -30,6 +32,13 @@ */ class SearchCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['json', 'text']); + } + } + /** * @return void */ diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 0194662b7c8e..c128295c47f4 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -20,7 +20,6 @@ use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\AliasPackage; -use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; @@ -41,6 +40,8 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Input\InputArgument; @@ -64,6 +65,17 @@ class ShowCommand extends BaseCommand /** @var ?RepositorySet */ private $repositorySet; + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($this->completeInstalledPackage($input, $suggestions)) { + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['json', 'text']); + } + } + /** * @return void */ diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 23776abb5ba2..173337691f7b 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -16,6 +16,8 @@ use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; use Composer\Installer\SuggestedPackagesReporter; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -23,6 +25,11 @@ class SuggestsCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 8bd3a9bfe26f..c89f1a58aa8e 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -24,6 +24,8 @@ use Composer\Util\HttpDownloader; use Composer\Semver\Constraint\MultiConstraint; use Composer\Package\Link; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -37,6 +39,11 @@ */ class UpdateCommand extends BaseCommand { + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); + } + /** * @return void */ diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index bc2610a63bed..96384b128690 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -74,6 +74,12 @@ public function guessVersion(array $packageConfig, string $path): ?array return null; } + // bypass version guessing in bash completions as it takes time to create + // new processes and the root version is usually not that important + if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] === '_complete') { + return null; + } + $versionData = $this->guessGitVersion($packageConfig, $path); if (null !== $versionData && null !== $versionData['version']) { return $this->postprocess($versionData); diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 581e17226a26..4fb6b9f76e4e 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -44,8 +44,12 @@ class HttpDownloader private $config; /** @var array */ private $jobs = array(); + /** @var bool */ + private $disableTls; /** @var mixed[] */ private $options = array(); + /** @var mixed[]|null */ + private $tlsDefaultOptions = null; /** @var int */ private $runningJobs = 0; /** @var int */ @@ -73,22 +77,19 @@ public function __construct(IOInterface $io, Config $config, array $options = ar $this->disabled = (bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK'); - // Setup TLS options - // The cafile option can be set via config.json - if ($disableTls === false) { - $this->options = StreamContextFactory::getTlsDefaults($options, $io); + if ($disableTls === true) { + // make sure the tlsDefaultOptions are not loaded later + $this->tlsDefaultOptions = []; } - // handle the other externally set options normally. - $this->options = array_replace_recursive($this->options, $options); + $this->disableTls = $disableTls; + $this->options = $options; $this->config = $config; if (self::isCurlEnabled()) { $this->curl = new CurlDownloader($io, $config, $options, $disableTls); } - $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls); - if (is_numeric($maxJobs = Platform::getEnv('COMPOSER_MAX_PARALLEL_HTTP'))) { $this->maxJobs = max(1, min(50, (int) $maxJobs)); } @@ -171,7 +172,13 @@ public function addCopy(string $url, string $to, array $options = array()) */ public function getOptions() { - return $this->options; + if ($this->tlsDefaultOptions === null) { + // Setup TLS options + // The cafile option can be set via config.json + $this->tlsDefaultOptions = StreamContextFactory::getTlsDefaults($this->options, $this->io); + } + + return array_replace_recursive($this->tlsDefaultOptions, $this->options); } /** @@ -191,7 +198,7 @@ public function setOptions(array $options) */ private function addJob(array $request, bool $sync = false): array { - $request['options'] = array_replace_recursive($this->options, $request['options']); + $request['options'] = array_replace_recursive($this->getOptions(), $request['options']); /** @var Job */ $job = array( @@ -211,8 +218,6 @@ private function addJob(array $request, bool $sync = false): array $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); } - $rfs = $this->rfs; - if ($this->canUseCurl($job)) { $resolver = function ($resolve, $reject) use (&$job): void { $job['status'] = HttpDownloader::STATUS_QUEUED; @@ -285,6 +290,15 @@ private function addJob(array $request, bool $sync = false): array return array($job, $promise); } + private function getRFS(): RemoteFilesystem + { + if (null === $this->rfs) { + $this->rfs = new RemoteFilesystem($this->io, $this->config, $this->options, $this->disableTls); + } + + return $this->rfs; + } + /** * @param int $id * @return void diff --git a/tests/Composer/Test/CompletionFunctionalTest.php b/tests/Composer/Test/CompletionFunctionalTest.php new file mode 100644 index 000000000000..a2f1ce8c0dcf --- /dev/null +++ b/tests/Composer/Test/CompletionFunctionalTest.php @@ -0,0 +1,109 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test; + +use Composer\Console\Application; +use Symfony\Component\Console\Tester\CommandCompletionTester; + +/** + * Validate autocompletion for all commands. + * + * @author Jérôme Tamarelle + */ +class CompletionFunctionalTest extends TestCase +{ + public function getCommandSuggestions(): iterable + { + $randomProject = '104corp/cache'; + $installedPackages = ['composer/semver', 'psr/log']; + $preferInstall = ['dist', 'source', 'auto']; + + yield ['archive ', [$randomProject]]; + yield ['archive symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']]; + yield ['archive --format ', ['tar', 'zip']]; + + yield ['create-project ', [$randomProject]]; + yield ['create-project symfony/skeleton --prefer-install ', $preferInstall]; + + yield ['depends ', $installedPackages]; + yield ['why ', $installedPackages]; + + yield ['exec ', ['composer', 'compile']]; + + yield ['browse ', $installedPackages]; + yield ['home -H ', $installedPackages]; + + yield ['init --require ', [$randomProject]]; + yield ['init --require-dev foo/bar --require-dev ', [$randomProject]]; + + yield ['install --prefer-install ', $preferInstall]; + yield ['install ', $installedPackages]; + + yield ['outdated ', $installedPackages]; + + yield ['prohibits ', [$randomProject]]; + yield ['why-not symfony/http-ker', ['symfony/http-kernel']]; + + yield ['reinstall --prefer-install ', $preferInstall]; + yield ['reinstall ', $installedPackages]; + + yield ['remove ', $installedPackages]; + + yield ['require --prefer-install ', $preferInstall]; + yield ['require ', [$randomProject]]; + yield ['require --dev symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']]; + + yield ['run-script ', ['compile', 'test', 'phpstan']]; + yield ['run-script test ', null]; + + yield ['search --format ', ['text', 'json']]; + + yield ['show --format ', ['text', 'json']]; + yield ['info ', $installedPackages]; + + yield ['suggests ', $installedPackages]; + + yield ['update --prefer-install ', $preferInstall]; + yield ['update ', $installedPackages]; + } + + /** + * @dataProvider getCommandSuggestions + * + * @param string $input The command that is typed + * @param string[]|null $expectedSuggestions Sample expected suggestions. Null if nothing is expected. + */ + public function testComplete(string $input, ?array $expectedSuggestions): void + { + $input = explode(' ', $input); + $commandName = array_shift($input); + $command = $this->getApplication()->get($commandName); + + $tester = new CommandCompletionTester($command); + $suggestions = $tester->complete($input); + + if (null === $expectedSuggestions) { + $this->assertEmpty($suggestions); + + return; + } + + $diff = array_diff($expectedSuggestions, $suggestions); + $this->assertEmpty($diff, sprintf('Suggestions must contain "%s". Got "%s".', implode('", "', $diff), implode('", "', $suggestions))); + } + + private function getApplication(): Application + { + return new Application(); + } +} From ed7d8219addbc9d1abe7ae6224a035d57530dea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Thu, 14 Apr 2022 22:24:33 +0200 Subject: [PATCH 02/15] Backport bash completion suggestion definition from symfony/console 6.1 --- src/Composer/Command/ArchiveCommand.php | 21 +-- src/Composer/Command/BaseCommand.php | 124 ++++-------------- src/Composer/Command/CompletionTrait.php | 102 ++++++++++++++ src/Composer/Command/CreateProjectCommand.php | 15 +-- src/Composer/Command/DependsCommand.php | 13 +- src/Composer/Command/ExecCommand.php | 23 ++-- src/Composer/Command/FundCommand.php | 4 +- src/Composer/Command/GlobalCommand.php | 2 +- src/Composer/Command/HomeCommand.php | 13 +- src/Composer/Command/InitCommand.php | 17 +-- src/Composer/Command/InstallCommand.php | 15 +-- src/Composer/Command/OutdatedCommand.php | 15 +-- src/Composer/Command/ProhibitsCommand.php | 13 +- src/Composer/Command/ReinstallCommand.php | 15 +-- src/Composer/Command/RemoveCommand.php | 13 +- src/Composer/Command/RequireCommand.php | 16 +-- src/Composer/Command/RunScriptCommand.php | 17 +-- src/Composer/Command/ScriptAliasCommand.php | 4 +- src/Composer/Command/SearchCommand.php | 15 +-- src/Composer/Command/SelfUpdateCommand.php | 4 +- src/Composer/Command/ShowCommand.php | 25 +--- src/Composer/Command/StatusCommand.php | 2 +- src/Composer/Command/SuggestsCommand.php | 13 +- src/Composer/Command/UpdateCommand.php | 15 +-- src/Composer/Console/Input/InputArgument.php | 61 +++++++++ src/Composer/Console/Input/InputOption.php | 65 +++++++++ src/Composer/Factory.php | 14 +- .../Package/Version/VersionGuesser.php | 3 +- src/Composer/Util/HttpDownloader.php | 38 ++---- src/Composer/Util/Platform.php | 8 ++ 30 files changed, 376 insertions(+), 329 deletions(-) create mode 100644 src/Composer/Command/CompletionTrait.php create mode 100644 src/Composer/Console/Input/InputArgument.php create mode 100644 src/Composer/Console/Input/InputOption.php diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index 2629db7e95c1..1114ed02904c 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -27,11 +27,9 @@ use Composer\Util\Loop; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -41,16 +39,9 @@ */ class ArchiveCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($this->completeAvailablePackage($input, $suggestions)) { - return; - } + use CompletionTrait; - if ($input->mustSuggestOptionValuesFor('format')) { - $suggestions->suggestValues(['tar', 'tar.gz', 'tar.bz2', 'zip']); - } - } + private const FORMATS = ['tar', 'tar.gz', 'tar.bz2', 'zip']; /** * @return void @@ -61,9 +52,9 @@ protected function configure(): void ->setName('archive') ->setDescription('Creates an archive of this composer package.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project'), + new InputArgument('package', InputArgument::OPTIONAL, 'The package to archive instead of the current project', null, $this->suggestAvailablePackage()), new InputArgument('version', InputArgument::OPTIONAL, 'A version constraint to find the package to archive'), - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the resulting archive: tar, tar.gz, tar.bz2 or zip (default tar)', null, self::FORMATS), new InputOption('dir', null, InputOption::VALUE_REQUIRED, 'Write the archive to this directory'), new InputOption('file', null, InputOption::VALUE_REQUIRED, 'Write the archive with the given file name.' .' Note that the format will be appended.'), diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index dc88c5e17908..7ab897d5d054 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -15,21 +15,16 @@ use Composer\Composer; use Composer\Config; use Composer\Console\Application; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\IO\IOInterface; use Composer\IO\NullIO; -use Composer\Package\Package; -use Composer\Package\PackageInterface; use Composer\Plugin\PreCommandRunEvent; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginEvents; -use Composer\Repository\CompositeRepository; -use Composer\Repository\InstalledRepository; -use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\RootPackageRepository; use Composer\Util\Platform; use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; @@ -191,6 +186,30 @@ public function setIO(IOInterface $io) $this->io = $io; } + /** + * @inheritdoc + * + * Backport suggested values definition from symfony/console 6.1+ + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $definition = $this->getDefinition(); + $name = (string) $input->getCompletionName(); + if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() + && $definition->hasOption($name) + && ($option = $definition->getOption($name)) instanceof InputOption + ) { + $option->complete($input, $suggestions); + } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() + && $definition->hasArgument($name) + && ($argument = $definition->getArgument($name)) instanceof InputArgument + ) { + $argument->complete($input, $suggestions); + } else { + parent::complete($input, $suggestions); + } + } + /** * @inheritDoc * @@ -326,96 +345,7 @@ protected function getPlatformRequirementFilter(InputInterface $input): Platform } /** - * Suggestion values for "prefer-install" option - */ - protected function completePreferInstall(CompletionInput $input, CompletionSuggestions $suggestions): bool - { - if ($input->mustSuggestOptionValuesFor('prefer-install')) { - $suggestions->suggestValues(['dist', 'source', 'auto']); - - return true; - } - - return false; - } - - /** - * Suggest package names from installed ones. - */ - protected function completeInstalledPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool - { - if (!$input->mustSuggestArgumentValuesFor('packages') && - !$input->mustSuggestArgumentValuesFor('package') && - !$input->mustSuggestOptionValuesFor('ignore') - ) { - return false; - } - - $composer = $this->getComposer(); - $installedRepos = [new RootPackageRepository(clone $composer->getPackage())]; - - $locker = $composer->getLocker(); - if ($locker->isLocked()) { - $installedRepos[] = $locker->getLockedRepository(true); - } else { - $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); - } - - $installedRepo = new InstalledRepository($installedRepos); - $suggestions->suggestValues(array_map(function (PackageInterface $package) { - return $package->getName(); - }, $installedRepo->getPackages())); - - return true; - } - - /** - * Suggest package names available on all configured repositories. - */ - protected function completeAvailablePackage(CompletionInput $input, CompletionSuggestions $suggestions): bool - { - if (!$input->mustSuggestArgumentValuesFor('packages') && - !$input->mustSuggestArgumentValuesFor('package') && - !$input->mustSuggestOptionValuesFor('require') && - !$input->mustSuggestOptionValuesFor('require-dev') - ) { - return false; - } - - $composer = $this->getComposer(); - $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); - - $packages = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); - - foreach (array_slice($packages, 0, 150) as $package) { - $suggestions->suggestValue($package['name']); - } - - return true; - } - - /** - * Suggests ext- packages from the ones available on the currently-running PHP - */ - protected function completePlatformPackage(CompletionInput $input, CompletionSuggestions $suggestions): bool - { - if (!$input->mustSuggestOptionValuesFor('require') && - !$input->mustSuggestOptionValuesFor('require-dev') && - !str_starts_with($input->getCompletionValue(), 'ext-') - ) { - return false; - } - - $repos = new PlatformRepository([], $this->getComposer()->getConfig()->get('platform') ?? []); - $suggestions->suggestValues(array_map(function (PackageInterface $package) { - return $package->getName(); - }, $repos->getPackages())); - - return true; - } - - /** - * @param array $requirements + * @param array $requirements * * @return array */ diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php new file mode 100644 index 000000000000..c16c3a8f5cac --- /dev/null +++ b/src/Composer/Command/CompletionTrait.php @@ -0,0 +1,102 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\Composer; +use Composer\Package\PackageInterface; +use Composer\Repository\CompositeRepository; +use Composer\Repository\InstalledRepository; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Repository\RootPackageRepository; +use Symfony\Component\Console\Completion\CompletionInput; + +/** + * Adds completion to arguments and options. + * + * @internal + */ +trait CompletionTrait +{ + /** + * @see BaseCommand::requireComposer() + */ + abstract public function requireComposer(bool $disablePlugins = null, bool $disableScripts = null): Composer; + + /** + * Suggestion values for "prefer-install" option + */ + private function suggestPreferInstall(): array + { + return ['dist', 'source', 'auto']; + } + + /** + * Suggest package names from installed. + */ + private function suggestInstalledPackage(): \Closure + { + return function (): array { + $composer = $this->requireComposer(); + $installedRepos = [new RootPackageRepository(clone $composer->getPackage())]; + + $locker = $composer->getLocker(); + if ($locker->isLocked()) { + $installedRepos[] = $locker->getLockedRepository(true); + } else { + $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); + } + + $installedRepo = new InstalledRepository($installedRepos); + + return array_map(function (PackageInterface $package) { + return $package->getName(); + }, $installedRepo->getPackages()); + }; + } + + /** + * Suggest package names available on all configured repositories. + * @todo rework to list packages from cache + */ + private function suggestAvailablePackage(): \Closure + { + return function (CompletionInput $input) { + $composer = $this->requireComposer(); + $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); + + $packages = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + + return array_column(array_slice($packages, 0, 150), 'name'); + }; + } + + /** + * Suggest package names available on all configured repositories or + * ext- packages from the ones available on the currently-running PHP + */ + private function suggestAvailablePackageOrExtension(): \Closure + { + return function (CompletionInput $input) { + if (!str_starts_with($input->getCompletionValue(), 'ext-')) { + return $this->suggestAvailablePackage()($input); + } + + $repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform') ?? []); + + return array_map(function (PackageInterface $package) { + return $package->getName(); + }, $repos->getPackages()); + }; + } +} diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 341caaaa8fce..23356c267c1c 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -33,11 +33,9 @@ use Composer\Repository\RepositorySet; use Composer\Script\ScriptEvents; use Composer\Util\Silencer; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; @@ -57,10 +55,7 @@ */ class CreateProjectCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); - } + use CompletionTrait; /** * @var SuggestedPackagesReporter @@ -76,13 +71,13 @@ protected function configure(): void ->setName('create-project') ->setDescription('Creates new project from a package into given directory.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed'), + new InputArgument('package', InputArgument::OPTIONAL, 'Package name to be installed', null, $this->suggestAvailablePackage()), new InputArgument('directory', InputArgument::OPTIONAL, 'Directory where the files should be created'), new InputArgument('version', InputArgument::OPTIONAL, 'Version, will default to latest'), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum-stability allowed (unless a version is specified).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), - new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), + new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories to look the package up, either by URL or using JSON arrays'), new InputOption('repository-url', null, InputOption::VALUE_REQUIRED, 'DEPRECATED: Use --repository instead.'), new InputOption('add-repository', null, InputOption::VALUE_NONE, 'Add the custom repository in the composer.json. If a lock file is present it will be deleted and an update will be run instead of install.'), diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index d5c65ed46438..53ba7115f804 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -12,22 +12,17 @@ namespace Composer\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; /** * @author Niels Keurentjes */ class DependsCommand extends BaseDependencyCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * Configure command metadata. @@ -41,7 +36,7 @@ protected function configure(): void ->setAliases(array('why')) ->setDescription('Shows which packages cause the given package to be installed.') ->setDefinition(array( - new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), + new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage()), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), )) diff --git a/src/Composer/Command/ExecCommand.php b/src/Composer/Command/ExecCommand.php index 1afdb6e8d7b5..49df0a83244f 100644 --- a/src/Composer/Command/ExecCommand.php +++ b/src/Composer/Command/ExecCommand.php @@ -12,25 +12,16 @@ namespace Composer\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; /** * @author Davey Shafik */ class ExecCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('binary')) { - $suggestions->suggestValues($this->getBinaries(false)); - } - } - /** * @return void */ @@ -41,7 +32,9 @@ protected function configure() ->setDescription('Executes a vendored binary/script.') ->setDefinition(array( new InputOption('list', 'l', InputOption::VALUE_NONE), - new InputArgument('binary', InputArgument::OPTIONAL, 'The binary to run, e.g. phpunit'), + new InputArgument('binary', InputArgument::OPTIONAL, 'The binary to run, e.g. phpunit', null, function () { + return $this->getBinaries(false); + }), new InputArgument( 'args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, @@ -61,9 +54,9 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { $composer = $this->requireComposer(); - if ($input->getOption('list') || !$input->getArgument('binary')) { + if ($input->getOption('list') || null === $input->getArgument('binary')) { $bins = $this->getBinaries(true); - if (count($bins) > 0) { + if ([] === $bins) { $binDir = $composer->getConfig()->get('bin-dir'); throw new \RuntimeException("No binaries found in composer.json or in bin-dir ($binDir)"); @@ -107,7 +100,7 @@ protected function execute(InputInterface $input, OutputInterface $output) private function getBinaries(bool $forDisplay): array { - $composer = $this->getComposer(); + $composer = $this->requireComposer(); $binDir = $composer->getConfig()->get('bin-dir'); $bins = glob($binDir . '/*'); $localBins = $composer->getPackage()->getBinaries(); diff --git a/src/Composer/Command/FundCommand.php b/src/Composer/Command/FundCommand.php index 057fea5d6eab..15960d294523 100644 --- a/src/Composer/Command/FundCommand.php +++ b/src/Composer/Command/FundCommand.php @@ -21,7 +21,7 @@ use Composer\Semver\Constraint\MatchAllConstraint; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -38,7 +38,7 @@ protected function configure(): void $this->setName('fund') ->setDescription('Discover how to help fund the maintenance of your dependencies.') ->setDefinition(array( - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['text', 'json']), )) ; } diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index ea9b496e3d1a..d74bf2b14279 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -20,7 +20,7 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Composer/Command/HomeCommand.php b/src/Composer/Command/HomeCommand.php index db715617cfc9..5e792af6398c 100644 --- a/src/Composer/Command/HomeCommand.php +++ b/src/Composer/Command/HomeCommand.php @@ -18,10 +18,8 @@ use Composer\Repository\RepositoryFactory; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -30,10 +28,7 @@ */ class HomeCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * @inheritDoc @@ -47,7 +42,7 @@ protected function configure(): void ->setAliases(array('home')) ->setDescription('Opens the package\'s repository URL or homepage in your browser.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY, 'Package(s) to browse to.'), + new InputArgument('packages', InputArgument::IS_ARRAY, 'Package(s) to browse to.', null, $this->suggestInstalledPackage()), new InputOption('homepage', 'H', InputOption::VALUE_NONE, 'Open the homepage instead of the repository URL.'), new InputOption('show', 's', InputOption::VALUE_NONE, 'Only show the homepage or repository URL.'), )) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 8af6719c491b..6677da1019ef 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -23,11 +23,9 @@ use Composer\Repository\RepositoryFactory; use Composer\Util\Filesystem; use Composer\Util\Silencer; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; @@ -39,19 +37,12 @@ */ class InitCommand extends BaseCommand { + use CompletionTrait; use PackageDiscoveryTrait; /** @var array */ private $gitConfig; - /** @var RepositorySet[] */ - private $repositorySets; - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeAvailablePackage($input, $suggestions); - } - /** * @inheritDoc * @@ -68,8 +59,8 @@ protected function configure() new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), - new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), - new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), + new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), + new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index cbc51042dd31..35c7f664e26c 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -16,11 +16,9 @@ use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\HttpDownloader; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** @@ -31,10 +29,7 @@ */ class InstallCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completePreferInstall($input, $suggestions) || $this->completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -48,7 +43,7 @@ protected function configure() ->setDefinition(array( new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), - new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), + new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), @@ -63,7 +58,7 @@ protected function configure() new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.', null, $this->suggestInstalledPackage()), )) ->setHelp( <<completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -39,7 +34,7 @@ protected function configure(): void ->setName('outdated') ->setDescription('Shows a list of installed packages that have updates available, including their latest version.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), + new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestInstalledPackage()), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show only packages that are outdated (this is the default, but present here for compat with `show`'), new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all installed packages with their latest versions'), new InputOption('locked', null, InputOption::VALUE_NONE, 'Shows updates for packages from the lock file, regardless of what is currently in vendor dir'), @@ -48,7 +43,7 @@ protected function configure(): void new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'), new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), - new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), + new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage()), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), diff --git a/src/Composer/Command/ProhibitsCommand.php b/src/Composer/Command/ProhibitsCommand.php index 45908ec40b7f..d536a314161a 100644 --- a/src/Composer/Command/ProhibitsCommand.php +++ b/src/Composer/Command/ProhibitsCommand.php @@ -12,22 +12,17 @@ namespace Composer\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; /** * @author Niels Keurentjes */ class ProhibitsCommand extends BaseDependencyCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeAvailablePackage($input, $suggestions); - } + use CompletionTrait; /** * Configure command metadata. @@ -41,7 +36,7 @@ protected function configure(): void ->setAliases(array('why-not')) ->setDescription('Shows which packages prevent the given package from being installed.') ->setDefinition(array( - new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect'), + new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestAvailablePackage()), new InputArgument(self::ARGUMENT_CONSTRAINT, InputArgument::REQUIRED, 'Version constraint, which version you expected to be installed'), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index c845fc3f596b..40a376134bdf 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -22,11 +22,9 @@ use Composer\Plugin\PluginEvents; use Composer\Script\ScriptEvents; use Composer\Util\Platform; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** @@ -34,10 +32,7 @@ */ class ReinstallCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -50,7 +45,7 @@ protected function configure(): void ->setDefinition(array( new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), - new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), + new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump'), @@ -59,7 +54,7 @@ protected function configure(): void new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.', null, $this->suggestInstalledPackage()), )) ->setHelp( <<completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -48,7 +43,7 @@ protected function configure() ->setName('remove') ->setDescription('Removes a package from the require or require-dev.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage()), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index ff8d399762d1..d8fead4f91cc 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -14,11 +14,9 @@ use Composer\DependencyResolver\Request; use Composer\Util\Filesystem; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Factory; use Composer\Installer; @@ -41,6 +39,7 @@ */ class RequireCommand extends BaseCommand { + use CompletionTrait; use PackageDiscoveryTrait; /** @var bool */ @@ -60,11 +59,6 @@ class RequireCommand extends BaseCommand /** @var bool */ private $dependencyResolutionCompleted = false; - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completePlatformPackage($input, $suggestions) || $this->completeAvailablePackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); - } - /** * @return void */ @@ -74,12 +68,12 @@ protected function configure() ->setName('require') ->setDescription('Adds required packages to your composer.json and installs them.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageOrExtension()), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), - new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), + new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('fixed', null, InputOption::VALUE_NONE, 'Write fixed version to the composer.json.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), diff --git a/src/Composer/Command/RunScriptCommand.php b/src/Composer/Command/RunScriptCommand.php index 2b035cab2ee5..cdc262b1af3b 100644 --- a/src/Composer/Command/RunScriptCommand.php +++ b/src/Composer/Command/RunScriptCommand.php @@ -16,11 +16,9 @@ use Composer\Script\ScriptEvents; use Composer\Util\ProcessExecutor; use Composer\Util\Platform; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** @@ -46,13 +44,6 @@ class RunScriptCommand extends BaseCommand ScriptEvents::POST_AUTOLOAD_DUMP, ); - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestArgumentValuesFor('script')) { - $suggestions->suggestValues(array_keys($this->getComposer()->getPackage()->getScripts())); - } - } - /** * @return void */ @@ -63,7 +54,9 @@ protected function configure(): void ->setAliases(array('run')) ->setDescription('Runs the scripts defined in composer.json.') ->setDefinition(array( - new InputArgument('script', InputArgument::OPTIONAL, 'Script name to run.'), + new InputArgument('script', InputArgument::OPTIONAL, 'Script name to run.', null, function () { + return array_keys($this->requireComposer()->getPackage()->getScripts()); + }), new InputArgument('args', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, ''), new InputOption('timeout', null, InputOption::VALUE_REQUIRED, 'Sets script timeout in seconds, or 0 for never.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), diff --git a/src/Composer/Command/ScriptAliasCommand.php b/src/Composer/Command/ScriptAliasCommand.php index 4c34368ad237..603ea1059010 100644 --- a/src/Composer/Command/ScriptAliasCommand.php +++ b/src/Composer/Command/ScriptAliasCommand.php @@ -13,8 +13,8 @@ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; /** diff --git a/src/Composer/Command/SearchCommand.php b/src/Composer/Command/SearchCommand.php index f7dfa2001724..32ceaa9bc9eb 100644 --- a/src/Composer/Command/SearchCommand.php +++ b/src/Composer/Command/SearchCommand.php @@ -14,12 +14,10 @@ use Composer\Factory; use Composer\Json\JsonFile; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; @@ -32,13 +30,6 @@ */ class SearchCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestOptionValuesFor('format')) { - $suggestions->suggestValues(['json', 'text']); - } - } - /** * @return void */ @@ -51,7 +42,7 @@ protected function configure(): void new InputOption('only-name', 'N', InputOption::VALUE_NONE, 'Search only in package names'), new InputOption('only-vendor', 'O', InputOption::VALUE_NONE, 'Search only for vendor / organization names, returns only "vendor" as result'), new InputOption('type', 't', InputOption::VALUE_REQUIRED, 'Search for a specific package type'), - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputArgument('tokens', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'tokens to search for'), )) ->setHelp( diff --git a/src/Composer/Command/SelfUpdateCommand.php b/src/Composer/Command/SelfUpdateCommand.php index 5e025b486e68..f8291f133907 100644 --- a/src/Composer/Command/SelfUpdateCommand.php +++ b/src/Composer/Command/SelfUpdateCommand.php @@ -24,8 +24,8 @@ use Composer\Downloader\FilesystemException; use Composer\Downloader\TransportException; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputOption; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Finder\Finder; diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index c128295c47f4..dcce4f16a68a 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -40,13 +40,11 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; /** @@ -57,6 +55,8 @@ */ class ShowCommand extends BaseCommand { + use CompletionTrait; + /** @var VersionParser */ protected $versionParser; /** @var string[] */ @@ -65,17 +65,6 @@ class ShowCommand extends BaseCommand /** @var ?RepositorySet */ private $repositorySet; - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($this->completeInstalledPackage($input, $suggestions)) { - return; - } - - if ($input->mustSuggestOptionValuesFor('format')) { - $suggestions->suggestValues(['json', 'text']); - } - } - /** * @return void */ @@ -86,7 +75,7 @@ protected function configure() ->setAliases(array('info')) ->setDescription('Shows information about packages.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.'), + new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestInstalledPackage()), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), @@ -99,12 +88,12 @@ protected function configure() new InputOption('tree', 't', InputOption::VALUE_NONE, 'List the dependencies as a tree'), new InputOption('latest', 'l', InputOption::VALUE_NONE, 'Show the latest version'), new InputOption('outdated', 'o', InputOption::VALUE_NONE, 'Show the latest version but only for packages that are outdated'), - new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.'), + new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage()), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'), new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'), new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages). Use with the --outdated option'), diff --git a/src/Composer/Command/StatusCommand.php b/src/Composer/Command/StatusCommand.php index c749294d97a0..0f8e2b530a9e 100644 --- a/src/Composer/Command/StatusCommand.php +++ b/src/Composer/Command/StatusCommand.php @@ -13,7 +13,7 @@ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Composer\Downloader\ChangeReportInterface; use Composer\Downloader\DvcsDownloaderInterface; diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 173337691f7b..0dd1653b496e 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -16,19 +16,14 @@ use Composer\Repository\RootPackageRepository; use Composer\Repository\InstalledRepository; use Composer\Installer\SuggestedPackagesReporter; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; -use Symfony\Component\Console\Input\InputArgument; +use Composer\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; +use Composer\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SuggestsCommand extends BaseCommand { - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - $this->completeInstalledPackage($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -44,7 +39,7 @@ protected function configure(): void new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show suggestions from all dependencies, including transitive ones'), new InputOption('list', null, InputOption::VALUE_NONE, 'Show only list of suggested package names'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Exclude suggestions from require-dev packages'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that you want to list suggestions from.', null, $this->suggestInstalledPackage()), )) ->setHelp( <<completeInstalledPackage($input, $suggestions) || $this->completePreferInstall($input, $suggestions); - } + use CompletionTrait; /** * @return void @@ -54,11 +49,11 @@ protected function configure() ->setAliases(array('u', 'upgrade')) ->setDescription('Updates your dependencies to the latest version according to composer.json, and updates the composer.lock file.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Packages that should be updated, if not provided all packages are.', null, $this->suggestInstalledPackage()), new InputOption('with', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Temporary version constraint to add, e.g. foo/bar:1.0.0 or foo/bar=1.0.0'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), - new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).'), + new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), diff --git a/src/Composer/Console/Input/InputArgument.php b/src/Composer/Console/Input/InputArgument.php new file mode 100644 index 000000000000..88fe62b3cb40 --- /dev/null +++ b/src/Composer/Console/Input/InputArgument.php @@ -0,0 +1,61 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Console\Input; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputArgument as BaseInputArgument; + +/** + * Backport suggested values definition from symfony/console 6.1+ + * + * @author Jérôme Tamarelle + * + * @internal + */ +class InputArgument extends BaseInputArgument +{ + /** + * @var string[]|\Closure + */ + private $suggestedValues; + + /** + * @inheritdoc + * + * @param string[]|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function __construct(string $name, int $mode = null, string $description = '', $default = null, $suggestedValues = []) + { + parent::__construct($name, $mode, $description, $default, $suggestedValues); + + $this->suggestedValues = $suggestedValues; + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } +} diff --git a/src/Composer/Console/Input/InputOption.php b/src/Composer/Console/Input/InputOption.php new file mode 100644 index 000000000000..0d4ca2722aa8 --- /dev/null +++ b/src/Composer/Console/Input/InputOption.php @@ -0,0 +1,65 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Console\Input; + +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Input\InputOption as BaseInputOption; + +/** + * Backport suggested values definition from symfony/console 6.1+ + * + * @author Jérôme Tamarelle + * + * @internal + */ +class InputOption extends BaseInputOption +{ + /** + * @var string[]|\Closure + */ + private $suggestedValues; + + /** + * @inheritdoc + * + * @param string[]|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null, $suggestedValues = []) + { + parent::__construct($name, $shortcut, $mode, $description, $default, $suggestedValues); + + $this->suggestedValues = $suggestedValues; + + if ($suggestedValues && !$this->acceptValue()) { + throw new LogicException('Cannot set suggested values if the option does not accept a value.'); + } + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } +} diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 93253b0b35a3..af468ab803c4 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -309,12 +309,14 @@ public function createComposer(IOInterface $io, $localConfig = null, bool $disab throw new \InvalidArgumentException($message.PHP_EOL.$instructions); } - try { - $file->validateSchema(JsonFile::LAX_SCHEMA); - } catch (JsonValidationException $e) { - $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); - $message = $e->getMessage() . ':' . PHP_EOL . $errors; - throw new JsonValidationException($message); + if (!Platform::isInputCompletionProcess()) { + try { + $file->validateSchema(JsonFile::LAX_SCHEMA); + } catch (JsonValidationException $e) { + $errors = ' - ' . implode(PHP_EOL . ' - ', $e->getErrors()); + $message = $e->getMessage() . ':' . PHP_EOL . $errors; + throw new JsonValidationException($message); + } } $localConfig = $file->read(); diff --git a/src/Composer/Package/Version/VersionGuesser.php b/src/Composer/Package/Version/VersionGuesser.php index 96384b128690..d768f3247c49 100644 --- a/src/Composer/Package/Version/VersionGuesser.php +++ b/src/Composer/Package/Version/VersionGuesser.php @@ -19,6 +19,7 @@ use Composer\Semver\VersionParser as SemverVersionParser; use Composer\Util\Git as GitUtil; use Composer\Util\HttpDownloader; +use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\Svn as SvnUtil; use React\Promise\CancellablePromiseInterface; @@ -76,7 +77,7 @@ public function guessVersion(array $packageConfig, string $path): ?array // bypass version guessing in bash completions as it takes time to create // new processes and the root version is usually not that important - if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] === '_complete') { + if (Platform::isInputCompletionProcess()) { return null; } diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 4fb6b9f76e4e..581e17226a26 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -44,12 +44,8 @@ class HttpDownloader private $config; /** @var array */ private $jobs = array(); - /** @var bool */ - private $disableTls; /** @var mixed[] */ private $options = array(); - /** @var mixed[]|null */ - private $tlsDefaultOptions = null; /** @var int */ private $runningJobs = 0; /** @var int */ @@ -77,19 +73,22 @@ public function __construct(IOInterface $io, Config $config, array $options = ar $this->disabled = (bool) Platform::getEnv('COMPOSER_DISABLE_NETWORK'); - if ($disableTls === true) { - // make sure the tlsDefaultOptions are not loaded later - $this->tlsDefaultOptions = []; + // Setup TLS options + // The cafile option can be set via config.json + if ($disableTls === false) { + $this->options = StreamContextFactory::getTlsDefaults($options, $io); } - $this->disableTls = $disableTls; - $this->options = $options; + // handle the other externally set options normally. + $this->options = array_replace_recursive($this->options, $options); $this->config = $config; if (self::isCurlEnabled()) { $this->curl = new CurlDownloader($io, $config, $options, $disableTls); } + $this->rfs = new RemoteFilesystem($io, $config, $options, $disableTls); + if (is_numeric($maxJobs = Platform::getEnv('COMPOSER_MAX_PARALLEL_HTTP'))) { $this->maxJobs = max(1, min(50, (int) $maxJobs)); } @@ -172,13 +171,7 @@ public function addCopy(string $url, string $to, array $options = array()) */ public function getOptions() { - if ($this->tlsDefaultOptions === null) { - // Setup TLS options - // The cafile option can be set via config.json - $this->tlsDefaultOptions = StreamContextFactory::getTlsDefaults($this->options, $this->io); - } - - return array_replace_recursive($this->tlsDefaultOptions, $this->options); + return $this->options; } /** @@ -198,7 +191,7 @@ public function setOptions(array $options) */ private function addJob(array $request, bool $sync = false): array { - $request['options'] = array_replace_recursive($this->getOptions(), $request['options']); + $request['options'] = array_replace_recursive($this->options, $request['options']); /** @var Job */ $job = array( @@ -218,6 +211,8 @@ private function addJob(array $request, bool $sync = false): array $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); } + $rfs = $this->rfs; + if ($this->canUseCurl($job)) { $resolver = function ($resolve, $reject) use (&$job): void { $job['status'] = HttpDownloader::STATUS_QUEUED; @@ -290,15 +285,6 @@ private function addJob(array $request, bool $sync = false): array return array($job, $promise); } - private function getRFS(): RemoteFilesystem - { - if (null === $this->rfs) { - $this->rfs = new RemoteFilesystem($this->io, $this->config, $this->options, $this->disableTls); - } - - return $this->rfs; - } - /** * @param int $id * @return void diff --git a/src/Composer/Util/Platform.php b/src/Composer/Util/Platform.php index be37ba6214ee..83fe71bd4353 100644 --- a/src/Composer/Util/Platform.php +++ b/src/Composer/Util/Platform.php @@ -225,6 +225,14 @@ public static function isTty($fd = null): bool return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; } + /** + * @return bool Whether the current command is for bash completion + */ + public static function isInputCompletionProcess(): bool + { + return '_complete' === ($_SERVER['argv'][1] ?? null); + } + /** * @return void */ From 68fd80b630d75c6e441a9d77313cd92b0eb61f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Tamarelle?= Date: Fri, 15 Apr 2022 15:11:01 +0200 Subject: [PATCH 03/15] PHPStan annotations --- src/Composer/Command/CompletionTrait.php | 2 ++ src/Composer/Command/ExecCommand.php | 4 ++++ src/Composer/Console/Input/InputArgument.php | 18 ++++++++++++------ src/Composer/Console/Input/InputOption.php | 19 ++++++++++++------- .../Test/CompletionFunctionalTest.php | 3 +++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index c16c3a8f5cac..f49c484717d5 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -35,6 +35,8 @@ abstract public function requireComposer(bool $disablePlugins = null, bool $disa /** * Suggestion values for "prefer-install" option + * + * @return string[] */ private function suggestPreferInstall(): array { diff --git a/src/Composer/Command/ExecCommand.php b/src/Composer/Command/ExecCommand.php index 49df0a83244f..f30467bd8100 100644 --- a/src/Composer/Command/ExecCommand.php +++ b/src/Composer/Command/ExecCommand.php @@ -98,6 +98,10 @@ protected function execute(InputInterface $input, OutputInterface $output) return $dispatcher->dispatchScript('__exec_command', true, $input->getArgument('args')); } + /** + * @param bool $forDisplay + * @return string[] + */ private function getBinaries(bool $forDisplay): array { $composer = $this->requireComposer(); diff --git a/src/Composer/Console/Input/InputArgument.php b/src/Composer/Console/Input/InputArgument.php index 88fe62b3cb40..697556b3bc70 100644 --- a/src/Composer/Console/Input/InputArgument.php +++ b/src/Composer/Console/Input/InputArgument.php @@ -14,6 +14,8 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument as BaseInputArgument; @@ -27,18 +29,22 @@ class InputArgument extends BaseInputArgument { /** - * @var string[]|\Closure + * @var string[]|\Closure(CompletionInput,CompletionSuggestions):list */ private $suggestedValues; /** - * @inheritdoc - * + * @param string $name The argument name + * @param int|null $mode The argument mode: self::REQUIRED or self::OPTIONAL + * @param string $description A description text + * @param string|bool|int|float|string[]|null $default The default value (for self::OPTIONAL mode only) * @param string[]|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * + * @throws InvalidArgumentException When argument mode is not valid */ public function __construct(string $name, int $mode = null, string $description = '', $default = null, $suggestedValues = []) { - parent::__construct($name, $mode, $description, $default, $suggestedValues); + parent::__construct($name, $mode, $description, $default); $this->suggestedValues = $suggestedValues; } @@ -51,10 +57,10 @@ public function __construct(string $name, int $mode = null, string $description public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input))) { + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } - if ($values) { + if ([] !== $values) { $suggestions->suggestValues($values); } } diff --git a/src/Composer/Console/Input/InputOption.php b/src/Composer/Console/Input/InputOption.php index 0d4ca2722aa8..72807355b29f 100644 --- a/src/Composer/Console/Input/InputOption.php +++ b/src/Composer/Console/Input/InputOption.php @@ -14,6 +14,8 @@ use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputOption as BaseInputOption; @@ -27,22 +29,25 @@ class InputOption extends BaseInputOption { /** - * @var string[]|\Closure + * @var string[]|\Closure(CompletionInput,CompletionSuggestions):list */ private $suggestedValues; /** - * @inheritdoc + * @param string|string[]|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the VALUE_* constants + * @param string|bool|int|float|string[]|null $default The default value (must be null for self::VALUE_NONE) + * @param string[]|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completionnull for self::VALUE_NONE) * - * @param string[]|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * @throws InvalidArgumentException If option mode is invalid or incompatible */ public function __construct(string $name, $shortcut = null, int $mode = null, string $description = '', $default = null, $suggestedValues = []) { - parent::__construct($name, $shortcut, $mode, $description, $default, $suggestedValues); + parent::__construct($name, $shortcut, $mode, $description, $default); $this->suggestedValues = $suggestedValues; - if ($suggestedValues && !$this->acceptValue()) { + if ([] !== $suggestedValues && !$this->acceptValue()) { throw new LogicException('Cannot set suggested values if the option does not accept a value.'); } } @@ -55,10 +60,10 @@ public function __construct(string $name, $shortcut = null, int $mode = null, st public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input))) { + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } - if ($values) { + if ([] !== $values) { $suggestions->suggestValues($values); } } diff --git a/tests/Composer/Test/CompletionFunctionalTest.php b/tests/Composer/Test/CompletionFunctionalTest.php index a2f1ce8c0dcf..422abea79aeb 100644 --- a/tests/Composer/Test/CompletionFunctionalTest.php +++ b/tests/Composer/Test/CompletionFunctionalTest.php @@ -22,6 +22,9 @@ */ class CompletionFunctionalTest extends TestCase { + /** + * @return iterable> + */ public function getCommandSuggestions(): iterable { $randomProject = '104corp/cache'; From 3f848cdb9ae937d74169ffab8107bb2c21a76d35 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 16:05:27 +0200 Subject: [PATCH 04/15] Add TODOs --- src/Composer/Command/BaseCommand.php | 2 ++ src/Composer/Console/Input/InputArgument.php | 2 ++ src/Composer/Console/Input/InputOption.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Composer/Command/BaseCommand.php b/src/Composer/Command/BaseCommand.php index 7ab897d5d054..89c3996247c3 100644 --- a/src/Composer/Command/BaseCommand.php +++ b/src/Composer/Command/BaseCommand.php @@ -190,6 +190,8 @@ public function setIO(IOInterface $io) * @inheritdoc * * Backport suggested values definition from symfony/console 6.1+ + * + * TODO drop when PHP 8.1 / symfony 6.1+ can be required */ public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { diff --git a/src/Composer/Console/Input/InputArgument.php b/src/Composer/Console/Input/InputArgument.php index 697556b3bc70..b9ee541f6185 100644 --- a/src/Composer/Console/Input/InputArgument.php +++ b/src/Composer/Console/Input/InputArgument.php @@ -25,6 +25,8 @@ * @author Jérôme Tamarelle * * @internal + * + * TODO drop when PHP 8.1 / symfony 6.1+ can be required */ class InputArgument extends BaseInputArgument { diff --git a/src/Composer/Console/Input/InputOption.php b/src/Composer/Console/Input/InputOption.php index 72807355b29f..70654c0d87e8 100644 --- a/src/Composer/Console/Input/InputOption.php +++ b/src/Composer/Console/Input/InputOption.php @@ -25,6 +25,8 @@ * @author Jérôme Tamarelle * * @internal + * + * TODO drop when PHP 8.1 / symfony 6.1+ can be required */ class InputOption extends BaseInputOption { From ce45d15bb5dfec0baa444a56ec0a8aacf070f063 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 16:05:49 +0200 Subject: [PATCH 05/15] Skip schema validation when running completion for increased bootstrap performance --- src/Composer/Factory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index af468ab803c4..ee7e045868f9 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -704,6 +704,10 @@ private static function getUserDir(): string */ private static function validateJsonSchema(?IOInterface $io, $fileOrData, int $schema = JsonFile::LAX_SCHEMA, ?string $source = null): void { + if (Platform::isInputCompletionProcess()) { + return; + } + try { if ($fileOrData instanceof JsonFile) { $fileOrData->validateSchema($schema); From 1162629a172dcfb5c6bdede132dc1bf69b1b3b09 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 16:13:24 +0200 Subject: [PATCH 06/15] Try and add completion for global subcommands --- src/Composer/Command/GlobalCommand.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index d74bf2b14279..a99898e2835a 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -37,6 +37,14 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti return $command->isHidden() ? null : $command->getName(); }, $application->all()))); } + + if ($application->has($commandName = $input->getArgument('command-name'))) { + $input = $this->prepareSubcommandInput($input, true); + $input = CompletionInput::fromString($input->__toString(), 3); + $command = $this->getApplication()->find($commandName); + $input->bind($command->getDefinition()); + $command->complete($input, $suggestions); + } } /** @@ -100,6 +108,13 @@ public function run(InputInterface $input, OutputInterface $output): int return parent::run($input, $output); } + $input = $this->prepareSubcommandInput($input); + + return $this->getApplication()->run($input, $output); + } + + private function prepareSubcommandInput(InputInterface $input, bool $quiet = false): StringInput + { // The COMPOSER env var should not apply to the global execution scope if (Platform::getEnv('COMPOSER')) { Platform::clearEnv('COMPOSER'); @@ -122,13 +137,15 @@ public function run(InputInterface $input, OutputInterface $output): int } catch (\Exception $e) { throw new \RuntimeException('Could not switch to home directory "'.$home.'"', 0, $e); } - $this->getIO()->writeError('Changed current directory to '.$home.''); + if (!$quiet) { + $this->getIO()->writeError('Changed current directory to '.$home.''); + } // create new input without "global" command prefix $input = new StringInput(Preg::replace('{\bg(?:l(?:o(?:b(?:a(?:l)?)?)?)?)?\b}', '', $input->__toString(), 1)); $this->getApplication()->resetComposer(); - return $this->getApplication()->run($input, $output); + return $input; } /** From 3b2745a00d025fc1ec0e830f1552fe40ce57ba79 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 22:51:11 +0200 Subject: [PATCH 07/15] Improve package suggestions, show only vendors by default when showing all available packages, add support for -p/-a in show command --- src/Composer/Command/CompletionTrait.php | 103 +++++++++++++++++++---- src/Composer/Command/DependsCommand.php | 2 +- src/Composer/Command/InitCommand.php | 4 +- src/Composer/Command/RemoveCommand.php | 2 +- src/Composer/Command/RequireCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 18 +++- 6 files changed, 108 insertions(+), 23 deletions(-) diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index f49c484717d5..e7773035ddf1 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -13,7 +13,9 @@ namespace Composer\Command; use Composer\Composer; +use Composer\Package\BasePackage; use Composer\Package\PackageInterface; +use Composer\Pcre\Preg; use Composer\Repository\CompositeRepository; use Composer\Repository\InstalledRepository; use Composer\Repository\PlatformRepository; @@ -46,9 +48,9 @@ private function suggestPreferInstall(): array /** * Suggest package names from installed. */ - private function suggestInstalledPackage(): \Closure + private function suggestInstalledPackage(bool $includePlatformPackages = false): \Closure { - return function (): array { + return function (CompletionInput $input) use ($includePlatformPackages): array { $composer = $this->requireComposer(); $installedRepos = [new RootPackageRepository(clone $composer->getPackage())]; @@ -59,46 +61,113 @@ private function suggestInstalledPackage(): \Closure $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } + $platformHint = []; + if ($includePlatformPackages) { + if ($locker->isLocked()) { + $platformRepo = new PlatformRepository(array(), $locker->getPlatformOverrides()); + } else { + $platformRepo = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + } + if ($input->getCompletionValue() === '') { + // to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes + $hintsToFind = ['ext-' => 0, 'lib-' => 0, 'php' => 99, 'composer' => 99]; + foreach ($platformRepo->getPackages() as $pkg) { + foreach ($hintsToFind as $hintPrefix => $hintCount) { + if (str_starts_with($pkg->getName(), $hintPrefix)) { + if ($hintCount === 0 || $hintCount >= 99) { + $platformHint[] = $pkg->getName(); + $hintsToFind[$hintPrefix]++; + } elseif ($hintCount === 1) { + unset($hintsToFind[$hintPrefix]); + $platformHint[] = substr($pkg->getName(), 0, max(strlen($pkg->getName()) - 3, strlen($hintPrefix) + 1)).'...'; + } + continue 2; + } + } + } + } else { + $installedRepos[] = $platformRepo; + } + } + $installedRepo = new InstalledRepository($installedRepos); - return array_map(function (PackageInterface $package) { - return $package->getName(); - }, $installedRepo->getPackages()); + return array_merge( + array_map(function (PackageInterface $package) { + return $package->getName(); + }, $installedRepo->getPackages()), + $platformHint + ); }; } /** * Suggest package names available on all configured repositories. - * @todo rework to list packages from cache */ - private function suggestAvailablePackage(): \Closure + private function suggestAvailablePackage(int $max = 99): \Closure { - return function (CompletionInput $input) { + return function (CompletionInput $input) use ($max): array { + if ($max < 1) { + return []; + } + $composer = $this->requireComposer(); $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); - $packages = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + $results = []; + if (!str_contains($input->getCompletionValue(), '/')) { + $results = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_VENDOR); + $vendors = true; + } + + // if we get a single vendor, we expand it into its contents already + if (\count($results) <= 1) { + $results = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); + $vendors = false; + } - return array_column(array_slice($packages, 0, 150), 'name'); + $results = array_column(array_slice($results, 0, $max), 'name'); + if ($vendors) { + $results = array_map(function (string $name): string { + return $name.'/'; + }, $results); + } + + return $results; }; } /** * Suggest package names available on all configured repositories or - * ext- packages from the ones available on the currently-running PHP + * platform packages from the ones available on the currently-running PHP */ - private function suggestAvailablePackageOrExtension(): \Closure + private function suggestAvailablePackageInclPlatform(): \Closure { - return function (CompletionInput $input) { - if (!str_starts_with($input->getCompletionValue(), 'ext-')) { - return $this->suggestAvailablePackage()($input); + return function (CompletionInput $input): array { + if (Preg::isMatch('{^(ext|lib|php)(-|$)|^com}', $input->getCompletionValue())) { + $matches = $this->suggestPlatformPackage()($input); + } else { + $matches = []; } + return array_merge($matches, $this->suggestAvailablePackage(99 - \count($matches))($input)); + }; + } + + /** + * Suggest platform packages from the ones available on the currently-running PHP + */ + private function suggestPlatformPackage(): \Closure + { + return function (CompletionInput $input): array { $repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform') ?? []); - return array_map(function (PackageInterface $package) { + $pattern = BasePackage::packageNameToRegexp($input->getCompletionValue().'*'); + return array_filter(array_map(function (PackageInterface $package) { return $package->getName(); - }, $repos->getPackages()); + }, $repos->getPackages()), function (string $name) use ($pattern): bool { + return Preg::isMatch($pattern, $name); + }); }; } } diff --git a/src/Composer/Command/DependsCommand.php b/src/Composer/Command/DependsCommand.php index 53ba7115f804..1b353807aad4 100644 --- a/src/Composer/Command/DependsCommand.php +++ b/src/Composer/Command/DependsCommand.php @@ -36,7 +36,7 @@ protected function configure(): void ->setAliases(array('why')) ->setDescription('Shows which packages cause the given package to be installed.') ->setDefinition(array( - new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage()), + new InputArgument(self::ARGUMENT_PACKAGE, InputArgument::REQUIRED, 'Package to inspect', null, $this->suggestInstalledPackage(true)), new InputOption(self::OPTION_RECURSIVE, 'r', InputOption::VALUE_NONE, 'Recursively resolves up to the root package'), new InputOption(self::OPTION_TREE, 't', InputOption::VALUE_NONE, 'Prints the results as a nested tree'), )) diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 6677da1019ef..29382606fbf3 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -59,8 +59,8 @@ protected function configure() new InputOption('author', null, InputOption::VALUE_REQUIRED, 'Author name of package'), new InputOption('type', null, InputOption::VALUE_OPTIONAL, 'Type of package (e.g. library, project, metapackage, composer-plugin)'), new InputOption('homepage', null, InputOption::VALUE_REQUIRED, 'Homepage of package'), - new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), - new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackage()), + new InputOption('require', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), + new InputOption('require-dev', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Package to require for development with a version constraint, e.g. foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('stability', 's', InputOption::VALUE_REQUIRED, 'Minimum stability (empty or one of: '.implode(', ', array_keys(BasePackage::$stabilities)).')'), new InputOption('license', 'l', InputOption::VALUE_REQUIRED, 'License of package'), new InputOption('repository', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Add custom repositories, either by URL or using JSON arrays'), diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 86a11193f246..6db97660242d 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -43,7 +43,7 @@ protected function configure() ->setName('remove') ->setDescription('Removes a package from the require or require-dev.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage()), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'Packages that should be removed.', null, $this->suggestInstalledPackage(true)), new InputOption('dev', null, InputOption::VALUE_NONE, 'Removes a package from the require-dev section.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index d8fead4f91cc..3feb98e6c77d 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -68,7 +68,7 @@ protected function configure() ->setName('require') ->setDescription('Adds required packages to your composer.json and installs them.') ->setDefinition(array( - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageOrExtension()), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Optional package name can also include a version constraint, e.g. foo/bar or foo/bar:1.0.0 or foo/bar=1.0.0 or "foo/bar 1.0.0"', null, $this->suggestAvailablePackageInclPlatform()), new InputOption('dev', null, InputOption::VALUE_NONE, 'Add requirement to require-dev.'), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index dcce4f16a68a..d96a409fe1e7 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -40,6 +40,7 @@ use Composer\Semver\Semver; use Composer\Spdx\SpdxLicenses; use Composer\Util\PackageInfo; +use Symfony\Component\Console\Completion\CompletionInput; use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Composer\Console\Input\InputArgument; @@ -75,7 +76,7 @@ protected function configure() ->setAliases(array('info')) ->setDescription('Shows information about packages.') ->setDefinition(array( - new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestInstalledPackage()), + new InputArgument('package', InputArgument::OPTIONAL, 'Package to inspect. Or a name including a wildcard (*) to filter lists of packages instead.', null, $this->suggestPackageBasedOnMode()), new InputArgument('version', InputArgument::OPTIONAL, 'Version or version constraint to inspect'), new InputOption('all', null, InputOption::VALUE_NONE, 'List all packages'), new InputOption('locked', null, InputOption::VALUE_NONE, 'List all locked packages'), @@ -109,6 +110,21 @@ protected function configure() ; } + protected function suggestPackageBasedOnMode(): \Closure + { + return function (CompletionInput $input) { + if ($input->getOption('available') || $input->getOption('all')) { + return $this->suggestAvailablePackageInclPlatform()($input); + } + + if ($input->getOption('platform')) { + return $this->suggestPlatformPackage()($input); + } + + return $this->suggestInstalledPackage()($input); + }; + } + protected function execute(InputInterface $input, OutputInterface $output) { $this->versionParser = new VersionParser; From 0089a69c57621fa79dc7d4948a364dde3791736e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 22:51:38 +0200 Subject: [PATCH 08/15] Avoid outputting network errors or others while loading suggestions --- src/Composer/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index ba140a1496ea..c45dec711863 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -466,7 +466,7 @@ public function getComposer(bool $required = true, ?bool $disablePlugins = null, if (null === $this->composer) { try { - $this->composer = Factory::create($this->io, null, $disablePlugins, $disableScripts); + $this->composer = Factory::create(Platform::isInputCompletionProcess() ? new NullIO() : $this->io, null, $disablePlugins, $disableScripts); } catch (\InvalidArgumentException $e) { if ($required) { $this->io->writeError($e->getMessage()); From 6aa7e15373e1d0bd2bb060e09f360e07b6004fd6 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 23:11:22 +0200 Subject: [PATCH 09/15] Fixes to global completion, working in some cases now --- src/Composer/Command/GlobalCommand.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index a99898e2835a..902ed4cb224e 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -36,12 +36,16 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti $suggestions->suggestValues(array_filter(array_map(function (Command $command) { return $command->isHidden() ? null : $command->getName(); }, $application->all()))); + + return; } if ($application->has($commandName = $input->getArgument('command-name'))) { $input = $this->prepareSubcommandInput($input, true); - $input = CompletionInput::fromString($input->__toString(), 3); - $command = $this->getApplication()->find($commandName); + $input = CompletionInput::fromString($input->__toString(), 2); + $command = $application->find($commandName); + $command->mergeApplicationDefinition(); + $input->bind($command->getDefinition()); $command->complete($input, $suggestions); } From 55dc80862ea81748df77153fdf14f861759c68b3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 12 May 2022 23:22:34 +0200 Subject: [PATCH 10/15] Improve sorting of vendor results for available packages --- src/Composer/Command/CompletionTrait.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index e7773035ddf1..8fa7632d5f38 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -126,14 +126,31 @@ private function suggestAvailablePackage(int $max = 99): \Closure $vendors = false; } - $results = array_column(array_slice($results, 0, $max), 'name'); + $results = array_column($results, 'name'); + if ($vendors) { $results = array_map(function (string $name): string { return $name.'/'; }, $results); + + // sort shorter results first to avoid auto-expanding the completion to a longer string than needed + usort($results, function (string $a, string $b) { + return \strlen($a) - \strlen($b); + }); + + $pinned = []; + + // ensure if the input is an exact match that it is always in the result set + $completionInput = $input->getCompletionValue().'/'; + if (in_array($completionInput, $results, true)) { + $pinned[] = $completionInput; + array_splice($results, array_search($completionInput, $results, true), 1); + } + + return array_merge($pinned, array_slice($results, 0, $max - \count($pinned))); } - return $results; + return array_slice($results, 0, $max); }; } From b52053893c35aaaee0ce07a155d8c00717e06d1f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 May 2022 09:52:02 +0200 Subject: [PATCH 11/15] Fix type errors and tests --- src/Composer/Command/CompletionTrait.php | 23 +++++++---- src/Composer/Command/CreateProjectCommand.php | 6 +-- src/Composer/Command/GlobalCommand.php | 4 ++ .../Command/PackageDiscoveryTrait.php | 38 ++++++------------- src/Composer/Command/ShowCommand.php | 2 +- src/Composer/Command/SuggestsCommand.php | 2 +- src/Composer/Console/Input/InputArgument.php | 2 +- src/Composer/Console/Input/InputOption.php | 2 +- .../Test/CompletionFunctionalTest.php | 16 ++++---- 9 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/Composer/Command/CompletionTrait.php b/src/Composer/Command/CompletionTrait.php index 8fa7632d5f38..5134aac3a1e8 100644 --- a/src/Composer/Command/CompletionTrait.php +++ b/src/Composer/Command/CompletionTrait.php @@ -66,7 +66,7 @@ private function suggestInstalledPackage(bool $includePlatformPackages = false): if ($locker->isLocked()) { $platformRepo = new PlatformRepository(array(), $locker->getPlatformOverrides()); } else { - $platformRepo = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + $platformRepo = new PlatformRepository(array(), $composer->getConfig()->get('platform')); } if ($input->getCompletionValue() === '') { // to reduce noise, when no text is yet entered we list only two entries for ext- and lib- prefixes @@ -115,36 +115,43 @@ private function suggestAvailablePackage(int $max = 99): \Closure $repos = new CompositeRepository($composer->getRepositoryManager()->getRepositories()); $results = []; + $showVendors = false; if (!str_contains($input->getCompletionValue(), '/')) { $results = $repos->search('^' . preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_VENDOR); - $vendors = true; + $showVendors = true; } // if we get a single vendor, we expand it into its contents already if (\count($results) <= 1) { $results = $repos->search('^'.preg_quote($input->getCompletionValue()), RepositoryInterface::SEARCH_NAME); - $vendors = false; + $showVendors = false; } $results = array_column($results, 'name'); - if ($vendors) { + if ($showVendors) { $results = array_map(function (string $name): string { return $name.'/'; }, $results); // sort shorter results first to avoid auto-expanding the completion to a longer string than needed usort($results, function (string $a, string $b) { - return \strlen($a) - \strlen($b); + $lenA = \strlen($a); + $lenB = \strlen($b); + if ($lenA === $lenB) { + return $a <=> $b; + } + + return $lenA - $lenB; }); $pinned = []; // ensure if the input is an exact match that it is always in the result set $completionInput = $input->getCompletionValue().'/'; - if (in_array($completionInput, $results, true)) { + if (false !== ($exactIndex = array_search($completionInput, $results, true))) { $pinned[] = $completionInput; - array_splice($results, array_search($completionInput, $results, true), 1); + array_splice($results, $exactIndex, 1); } return array_merge($pinned, array_slice($results, 0, $max - \count($pinned))); @@ -177,7 +184,7 @@ private function suggestAvailablePackageInclPlatform(): \Closure private function suggestPlatformPackage(): \Closure { return function (CompletionInput $input): array { - $repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform') ?? []); + $repos = new PlatformRepository([], $this->requireComposer()->getConfig()->get('platform')); $pattern = BasePackage::packageNameToRegexp($input->getCompletionValue().'*'); return array_filter(array_map(function (PackageInterface $package) { diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 23356c267c1c..e04c0fdcf335 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -159,7 +159,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $preferSource, $preferDist, !$input->getOption('no-dev'), - $input->getOption('repository') ?: $input->getOption('repository-url'), + \count($input->getOption('repository')) > 0 ? $input->getOption('repository') : $input->getOption('repository-url'), $input->getOption('no-plugins'), $input->getOption('no-scripts'), $input->getOption('no-progress'), @@ -197,7 +197,7 @@ public function installProject(IOInterface $io, Config $config, InputInterface $ $repositories = (array) $repositories; } - $platformRequirementFilter = $platformRequirementFilter ?: PlatformRequirementFilterFactory::ignoreNothing(); + $platformRequirementFilter = $platformRequirementFilter ?? PlatformRequirementFilterFactory::ignoreNothing(); // we need to manually load the configuration to pass the auth credentials to the io interface! $io->loadConfiguration($config); @@ -424,7 +424,7 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p } } - $platformOverrides = $config->get('platform') ?: array(); + $platformOverrides = $config->get('platform'); $platformRepo = new PlatformRepository(array(), $platformOverrides); // find the latest version if there are multiple diff --git a/src/Composer/Command/GlobalCommand.php b/src/Composer/Command/GlobalCommand.php index 902ed4cb224e..e9195ca1e72a 100644 --- a/src/Composer/Command/GlobalCommand.php +++ b/src/Composer/Command/GlobalCommand.php @@ -119,6 +119,10 @@ public function run(InputInterface $input, OutputInterface $output): int private function prepareSubcommandInput(InputInterface $input, bool $quiet = false): StringInput { + if (!method_exists($input, '__toString')) { + throw new \LogicException('Expected an Input instance that is stringable, got '.get_class($input)); + } + // The COMPOSER env var should not apply to the global execution scope if (Platform::getEnv('COMPOSER')) { Platform::clearEnv('COMPOSER'); diff --git a/src/Composer/Command/PackageDiscoveryTrait.php b/src/Composer/Command/PackageDiscoveryTrait.php index 99fb0f39b707..c8ab0e9bfdbf 100644 --- a/src/Composer/Command/PackageDiscoveryTrait.php +++ b/src/Composer/Command/PackageDiscoveryTrait.php @@ -99,7 +99,7 @@ final protected function determineRequirements(InputInterface $input, OutputInte foreach ($requires as $requirement) { if (!isset($requirement['version'])) { // determine the best version automatically - list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, null, null, $fixed); + list($name, $version) = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed); $requirement['version'] = $version; // replace package name from packagist.org @@ -268,7 +268,7 @@ final protected function determineRequirements(InputInterface $input, OutputInte * @throws \InvalidArgumentException * @return array{string, string} name version */ - private function findBestVersionAndNameForPackage(InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', ?string $requiredVersion = null, ?string $minimumStability = null, bool $fixed = false): array + private function findBestVersionAndNameForPackage(InputInterface $input, string $name, ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $fixed = false): array { // handle ignore-platform-reqs flag if present if ($input->hasOption('ignore-platform-reqs') && $input->hasOption('ignore-platform-req')) { @@ -278,17 +278,17 @@ private function findBestVersionAndNameForPackage(InputInterface $input, string } // find the latest version allowed in this repo set - $repoSet = $this->getRepositorySet($input, $minimumStability); + $repoSet = $this->getRepositorySet($input); $versionSelector = new VersionSelector($repoSet, $platformRepo); - $effectiveMinimumStability = $minimumStability ?? $this->getMinimumStability($input); + $effectiveMinimumStability = $this->getMinimumStability($input); - $package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter); + $package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter); if (false === $package) { // platform packages can not be found in the pool in versions other than the local platform's has // so if platform reqs are ignored we just take the user's word for it if ($platformRequirementFilter->isIgnored($name)) { - return array($name, $requiredVersion ?: '*'); + return array($name, '*'); } // Check if it is a virtual package provided by others @@ -308,17 +308,16 @@ private function findBestVersionAndNameForPackage(InputInterface $input, string } // Check whether the package requirements were the problem - if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && false !== ($candidate = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreAll()))) { + if (!($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter) && false !== ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll()))) { throw new \InvalidArgumentException(sprintf( - 'Package %s%s has requirements incompatible with your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo), - $name, - is_string($requiredVersion) ? ' at version '.$requiredVersion : '' + 'Package %s has requirements incompatible with your PHP version, PHP extensions and Composer version' . $this->getPlatformExceptionDetails($candidate, $platformRepo), + $name )); } // Check whether the minimum stability was the problem but the package exists - if (false !== ($package = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { + if (false !== ($package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { // we must first verify if a valid package would be found in a lower priority repository - if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) { + if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter, RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) { throw new \InvalidArgumentException( 'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your minimum-stability and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.' ); @@ -330,21 +329,6 @@ private function findBestVersionAndNameForPackage(InputInterface $input, string $effectiveMinimumStability )); } - // Check whether the required version was the problem - if (is_string($requiredVersion) && false !== ($package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter))) { - // we must first verify if a valid package would be found in a lower priority repository - if (false !== ($allReposPackage = $versionSelector->findBestCandidate($name, $requiredVersion, $preferredStability, PlatformRequirementFilterFactory::ignoreNothing(), RepositorySet::ALLOW_SHADOWED_REPOSITORIES))) { - throw new \InvalidArgumentException( - 'Package '.$name.' exists in '.$allReposPackage->getRepository()->getRepoName().' and '.$package->getRepository()->getRepoName().' which has a higher repository priority. The packages from the higher priority repository do not match your constraint and are therefore not installable. That repository is canonical so the lower priority repo\'s packages are not installable. See https://getcomposer.org/repoprio for details and assistance.' - ); - } - - throw new \InvalidArgumentException(sprintf( - 'Could not find package %s in a version matching "%s" and a stability matching "'.$effectiveMinimumStability.'".', - $name, - $requiredVersion - )); - } // Check whether the PHP version was the problem for all versions if (!$platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter && false !== ($candidate = $versionSelector->findBestCandidate($name, null, $preferredStability, PlatformRequirementFilterFactory::ignoreAll(), RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES))) { $additional = ''; diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index d96a409fe1e7..1fbb444b9ad6 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -187,7 +187,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // init repos $platformOverrides = array(); if ($composer) { - $platformOverrides = $composer->getConfig()->get('platform') ?: array(); + $platformOverrides = $composer->getConfig()->get('platform'); } $platformRepo = new PlatformRepository(array(), $platformOverrides); $lockedRepo = null; diff --git a/src/Composer/Command/SuggestsCommand.php b/src/Composer/Command/SuggestsCommand.php index 0dd1653b496e..0e28b6089da7 100644 --- a/src/Composer/Command/SuggestsCommand.php +++ b/src/Composer/Command/SuggestsCommand.php @@ -65,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $installedRepos[] = new PlatformRepository(array(), $locker->getPlatformOverrides()); $installedRepos[] = $locker->getLockedRepository(!$input->getOption('no-dev')); } else { - $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform') ?: array()); + $installedRepos[] = new PlatformRepository(array(), $composer->getConfig()->get('platform')); $installedRepos[] = $composer->getRepositoryManager()->getLocalRepository(); } diff --git a/src/Composer/Console/Input/InputArgument.php b/src/Composer/Console/Input/InputArgument.php index b9ee541f6185..69c6dfb02c23 100644 --- a/src/Composer/Console/Input/InputArgument.php +++ b/src/Composer/Console/Input/InputArgument.php @@ -59,7 +59,7 @@ public function __construct(string $name, int $mode = null, string $description public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } if ([] !== $values) { diff --git a/src/Composer/Console/Input/InputOption.php b/src/Composer/Console/Input/InputOption.php index 70654c0d87e8..2d370ba5135e 100644 --- a/src/Composer/Console/Input/InputOption.php +++ b/src/Composer/Console/Input/InputOption.php @@ -62,7 +62,7 @@ public function __construct(string $name, $shortcut = null, int $mode = null, st public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void { $values = $this->suggestedValues; - if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { + if ($values instanceof \Closure && !\is_array($values = $values($input, $suggestions))) { // @phpstan-ignore-line throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->getName(), get_debug_type($values))); } if ([] !== $values) { diff --git a/tests/Composer/Test/CompletionFunctionalTest.php b/tests/Composer/Test/CompletionFunctionalTest.php index 422abea79aeb..36aa3fe8d52f 100644 --- a/tests/Composer/Test/CompletionFunctionalTest.php +++ b/tests/Composer/Test/CompletionFunctionalTest.php @@ -27,34 +27,34 @@ class CompletionFunctionalTest extends TestCase */ public function getCommandSuggestions(): iterable { - $randomProject = '104corp/cache'; + $randomVendor = 'a/'; $installedPackages = ['composer/semver', 'psr/log']; $preferInstall = ['dist', 'source', 'auto']; - yield ['archive ', [$randomProject]]; + yield ['archive ', [$randomVendor]]; yield ['archive symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']]; yield ['archive --format ', ['tar', 'zip']]; - yield ['create-project ', [$randomProject]]; + yield ['create-project ', [$randomVendor]]; yield ['create-project symfony/skeleton --prefer-install ', $preferInstall]; yield ['depends ', $installedPackages]; yield ['why ', $installedPackages]; - yield ['exec ', ['composer', 'compile']]; + yield ['exec ', ['composer', 'jsonlint', 'phpstan', 'phpstan.phar', 'simple-phpunit', 'validate-json']]; yield ['browse ', $installedPackages]; yield ['home -H ', $installedPackages]; - yield ['init --require ', [$randomProject]]; - yield ['init --require-dev foo/bar --require-dev ', [$randomProject]]; + yield ['init --require ', [$randomVendor]]; + yield ['init --require-dev foo/bar --require-dev ', [$randomVendor]]; yield ['install --prefer-install ', $preferInstall]; yield ['install ', $installedPackages]; yield ['outdated ', $installedPackages]; - yield ['prohibits ', [$randomProject]]; + yield ['prohibits ', [$randomVendor]]; yield ['why-not symfony/http-ker', ['symfony/http-kernel']]; yield ['reinstall --prefer-install ', $preferInstall]; @@ -63,7 +63,7 @@ public function getCommandSuggestions(): iterable yield ['remove ', $installedPackages]; yield ['require --prefer-install ', $preferInstall]; - yield ['require ', [$randomProject]]; + yield ['require ', [$randomVendor]]; yield ['require --dev symfony/http-', ['symfony/http-kernel', 'symfony/http-foundation']]; yield ['run-script ', ['compile', 'test', 'phpstan']]; From eac83aad5c05c0ca359e6b8381e6fcdeef22180b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 May 2022 10:35:39 +0200 Subject: [PATCH 12/15] Avoid running cache GC within completion processes --- src/Composer/Cache.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Composer/Cache.php b/src/Composer/Cache.php index c6e2585aaa31..27fc282179cb 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -248,6 +248,10 @@ public function gcIsNecessary() return false; } + if (Platform::isInputCompletionProcess()) { + return false; + } + return !random_int(0, 50); } From d99e059aa37a51376125f65c26b360d461df01a5 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 May 2022 13:58:02 +0200 Subject: [PATCH 13/15] Add bash completions to docs --- doc/03-cli.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/03-cli.md b/doc/03-cli.md index 9fd8256b610b..30d21c673b5d 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -13,6 +13,12 @@ php composer.phar dump ``` calls `composer dump-autoload`. +## Bash Completions + +To install bash completions you can run `composer completion bash > completion.bash` (put the file +in /etc/bash_completion.d/composer to make it load automatically in new terminals) and then +`source completion.bash` to enable it in the current terminal session. + ## Global Options The following options are available with every command: From 72f4e9ba63238946cd8053d9633335a0e0787157 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 13 May 2022 13:58:37 +0200 Subject: [PATCH 14/15] Remove package name completion from install command as it is not a recommended use case --- src/Composer/Command/InstallCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 35c7f664e26c..6e27a5815b3a 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -58,7 +58,7 @@ protected function configure() new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), - new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.', null, $this->suggestInstalledPackage()), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::OPTIONAL, 'Should not be provided, use composer require instead to add a given package to composer.json.'), )) ->setHelp( << Date: Fri, 13 May 2022 14:09:09 +0200 Subject: [PATCH 15/15] Add a couple missing --format completions --- src/Composer/Command/LicensesCommand.php | 4 ++-- src/Composer/Command/OutdatedCommand.php | 2 +- tests/Composer/Test/CompletionFunctionalTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index 748b5b85d4fd..52d0d0122959 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -12,6 +12,7 @@ namespace Composer\Command; +use Composer\Console\Input\InputOption; use Composer\Json\JsonFile; use Composer\Package\CompletePackageInterface; use Composer\Plugin\CommandEvent; @@ -22,7 +23,6 @@ use Symfony\Component\Console\Formatter\OutputFormatter; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; @@ -40,7 +40,7 @@ protected function configure(): void ->setName('licenses') ->setDescription('Shows information about licenses of dependencies.') ->setDefinition(array( - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text, json or summary', 'text'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text, json or summary', 'text', ['text', 'json', 'summary']), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), )) ->setHelp( diff --git a/src/Composer/Command/OutdatedCommand.php b/src/Composer/Command/OutdatedCommand.php index 8a8b8b0e682e..ffa9ba3af731 100644 --- a/src/Composer/Command/OutdatedCommand.php +++ b/src/Composer/Command/OutdatedCommand.php @@ -42,7 +42,7 @@ protected function configure(): void new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'), new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --outdated option.'), new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --outdated option.'), - new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text'), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']), new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it with the --outdated option if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage()), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'), new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages). Use with the --outdated option'), diff --git a/tests/Composer/Test/CompletionFunctionalTest.php b/tests/Composer/Test/CompletionFunctionalTest.php index 36aa3fe8d52f..32baa33ce315 100644 --- a/tests/Composer/Test/CompletionFunctionalTest.php +++ b/tests/Composer/Test/CompletionFunctionalTest.php @@ -50,7 +50,7 @@ public function getCommandSuggestions(): iterable yield ['init --require-dev foo/bar --require-dev ', [$randomVendor]]; yield ['install --prefer-install ', $preferInstall]; - yield ['install ', $installedPackages]; + yield ['install ', null]; yield ['outdated ', $installedPackages];