diff --git a/doc/04-schema.md b/doc/04-schema.md index 20bb61ac8..5e65dee16 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -119,6 +119,10 @@ Examples: - redis - templating +> **Note**: Some special keywords trigger `composer require` without the +> `--dev` option to prompt users if they would like to add these packages to +> `require-dev` instead of `require`. These are: `dev`, `testing`, `static analysis`. + Optional. ### homepage diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 29c85a8f1..23e36bdfa 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -13,7 +13,10 @@ namespace Composer\Command; use Composer\DependencyResolver\Request; +use Composer\Package\CompletePackageInterface; +use Composer\Package\PackageInterface; use Composer\Util\Filesystem; +use Composer\Util\PackageSorter; use Seld\Signal\SignalHandler; use Symfony\Component\Console\Input\InputInterface; use Composer\Console\Input\InputArgument; @@ -218,9 +221,36 @@ EOT throw $e; } + $requirements = $this->formatRequirements($requirements); + + if (!$input->getOption('dev') && $io->isInteractive()) { + $devPackages = []; + $devTags = ['dev', 'testing', 'static analysis']; + $currentRequiresByKey = $this->getPackagesByRequireKey(); + foreach ($requirements as $name => $version) { + // skip packages which are already in the composer.json as those have already been decided + if (isset($currentRequiresByKey[$name])) { + continue; + } + + $pkg = PackageSorter::getMostCurrentVersion($this->getRepos()->findPackages($name)); + if ($pkg instanceof CompletePackageInterface && count(array_intersect($devTags, array_map('strtolower', $pkg->getKeywords()))) > 0) { + $devPackages[] = $name; + } + } + + if (count($devPackages) === count($requirements)) { + $plural = count($requirements) > 1 ? 's' : ''; + $plural2 = count($requirements) > 1 ? 'are' : 'is'; + $io->warning('The package'.$plural.' you required '.$plural2.' recommended to be placed in require-dev but you did not use --dev.'); + if ($io->askConfirmation('Do you want to re-run the command with --dev? [yes]? ')) { + $input->setOption('dev', true); + } + } + } + $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; - $requirements = $this->formatRequirements($requirements); // validate requirements format $versionParser = new VersionParser(); @@ -254,6 +284,7 @@ EOT return 0; } + $input->setOption('dev', true); list($requireKey, $removeKey) = array($removeKey, $requireKey); } } diff --git a/src/Composer/Util/PackageSorter.php b/src/Composer/Util/PackageSorter.php index 38b606e6a..6060c2781 100644 --- a/src/Composer/Util/PackageSorter.php +++ b/src/Composer/Util/PackageSorter.php @@ -17,6 +17,34 @@ use Composer\Package\RootPackageInterface; class PackageSorter { + /** + * Returns the most recent version of a set of packages + * + * This is ideally the default branch version, or failing that it will return the package with the highest version + * + * @template T of PackageInterface + * @param array $packages + * @return ($packages is non-empty-array ? T : T|null) + */ + public static function getMostCurrentVersion(array $packages): ?PackageInterface + { + return array_reduce($packages, function ($carry, $pkg) { + if ($carry === null) { + return $pkg; + } + + if ($pkg->isDefaultBranch()) { + return $pkg; + } + + if (!$carry->isDefaultBranch() && version_compare($carry->getVersion(), $pkg->getVersion(), '<')) { + return $pkg; + } + + return $carry; + }); + } + /** * Sorts packages by name *