From aefa46dfbabea3497437480061496ba54773bf46 Mon Sep 17 00:00:00 2001 From: Travis Carden Date: Fri, 27 Oct 2023 05:36:59 -0400 Subject: [PATCH] Add support for "scripts-aliases" in composer.json (#11666) --- doc/articles/scripts.md | 16 ++++++++++++ res/composer-schema.json | 7 +++++ src/Composer/Command/ScriptAliasCommand.php | 15 ++++++++++- src/Composer/Config/JsonConfigSource.php | 2 +- src/Composer/Console/Application.php | 4 ++- src/Composer/Util/ConfigValidator.php | 11 ++++++++ .../Test/Command/RunScriptCommandTest.php | 26 +++++++++++++++++++ .../Test/Util/ConfigValidatorTest.php | 11 ++++++++ .../Fixtures/composer_scripts-aliases.json | 10 +++++++ 9 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 tests/Composer/Test/Util/Fixtures/composer_scripts-aliases.json diff --git a/doc/articles/scripts.md b/doc/articles/scripts.md index f83db4072..b7339cbc5 100644 --- a/doc/articles/scripts.md +++ b/doc/articles/scripts.md @@ -441,3 +441,19 @@ The descriptions are used in `composer list` or `composer run -l` commands to describe what the scripts do when the command is run. > **Note:** You can only set custom descriptions of custom commands. + +## Custom aliases. + +You can set custom script aliases with the following in your `composer.json`: + +```json +{ + "scripts-aliases": { + "phpstan": ["stan", "analyze"] + } +} +``` + +The aliases provide alternate command names. + +> **Note:** You can only set custom aliases of custom commands. diff --git a/res/composer-schema.json b/res/composer-schema.json index 47c087b3e..a77becb4b 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -694,6 +694,13 @@ "additionalProperties": { "type": "string" } + }, + "scripts-aliases": { + "type": ["object"], + "description": "Aliases for custom commands.", + "additionalProperties": { + "type": "array" + } } }, "definitions": { diff --git a/src/Composer/Command/ScriptAliasCommand.php b/src/Composer/Command/ScriptAliasCommand.php index b773f235d..f2de686fe 100644 --- a/src/Composer/Command/ScriptAliasCommand.php +++ b/src/Composer/Command/ScriptAliasCommand.php @@ -27,11 +27,23 @@ class ScriptAliasCommand extends BaseCommand private $script; /** @var string */ private $description; + /** @var string[] */ + private $aliases; - public function __construct(string $script, ?string $description) + /** + * @param string[] $aliases + */ + public function __construct(string $script, ?string $description, array $aliases = []) { $this->script = $script; $this->description = $description ?? 'Runs the '.$script.' script as defined in composer.json'; + $this->aliases = $aliases; + + foreach ($this->aliases as $alias) { + if (!is_string($alias)) { + throw new \InvalidArgumentException('"scripts-aliases" element array values should contain only strings'); + } + } $this->ignoreValidationErrors(); @@ -43,6 +55,7 @@ class ScriptAliasCommand extends BaseCommand $this ->setName($this->script) ->setDescription($this->description) + ->setAliases($this->aliases) ->setDefinition([ new InputOption('dev', null, InputOption::VALUE_NONE, 'Sets the dev mode.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables the dev mode.'), diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index bff50d869..db3d36dc4 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -249,7 +249,7 @@ class JsonConfigSource implements ConfigSourceInterface $this->arrayUnshiftRef($args, $config); $fallback(...$args); // avoid ending up with arrays for keys that should be objects - foreach (['require', 'require-dev', 'conflict', 'provide', 'replace', 'suggest', 'config', 'autoload', 'autoload-dev', 'scripts', 'scripts-descriptions', 'support'] as $prop) { + foreach (['require', 'require-dev', 'conflict', 'provide', 'replace', 'suggest', 'config', 'autoload', 'autoload-dev', 'scripts', 'scripts-descriptions', 'scripts-aliases', 'support'] as $prop) { if (isset($config[$prop]) && $config[$prop] === []) { $config[$prop] = new \stdClass; } diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index da77636d1..a74a6c69d 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -365,7 +365,9 @@ class Application extends BaseApplication $description = $composer['scripts-descriptions'][$script]; } - $this->add(new Command\ScriptAliasCommand($script, $description)); + $aliases = $composer['scripts-aliases'][$script] ?? []; + + $this->add(new Command\ScriptAliasCommand($script, $description, $aliases)); } } } diff --git a/src/Composer/Util/ConfigValidator.php b/src/Composer/Util/ConfigValidator.php index 44c2c3a2c..ac57199ee 100644 --- a/src/Composer/Util/ConfigValidator.php +++ b/src/Composer/Util/ConfigValidator.php @@ -196,6 +196,17 @@ class ConfigValidator } } + // report scripts-aliases for non-existent scripts + $scriptAliases = $manifest['scripts-aliases'] ?? []; + foreach ($scriptAliases as $scriptName => $scriptAlias) { + if (!array_key_exists($scriptName, $scripts)) { + $warnings[] = sprintf( + 'Aliases for non-existent script "%s" found in "scripts-aliases"', + $scriptName + ); + } + } + // check for empty psr-0/psr-4 namespace prefixes if (isset($manifest['autoload']['psr-0'][''])) { $warnings[] = "Defining autoload.psr-0 with an empty namespace prefix is a bad idea for performance"; diff --git a/tests/Composer/Test/Command/RunScriptCommandTest.php b/tests/Composer/Test/Command/RunScriptCommandTest.php index 88e6f390b..dc724ae8d 100644 --- a/tests/Composer/Test/Command/RunScriptCommandTest.php +++ b/tests/Composer/Test/Command/RunScriptCommandTest.php @@ -109,6 +109,32 @@ class RunScriptCommandTest extends TestCase $this->assertStringContainsString('Run the codestyle fixer', $output, 'The custom description for the fix-cs script should be printed'); } + public function testCanDefineAliases(): void + { + $expectedAliases = ['one', 'two', 'three']; + + $this->initTempComposer([ + 'scripts' => [ + 'test' => '@php test', + ], + 'scripts-aliases' => [ + 'test' => $expectedAliases, + ], + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'test', '--help' => true, '--format' => 'json']); + + $appTester->assertCommandIsSuccessful(); + + $output = $appTester->getDisplay(); + $array = json_decode($output, true); + $actualAliases = $array['usage']; + array_shift($actualAliases); + + $this->assertSame($expectedAliases, $actualAliases, 'The custom aliases for the test command should be printed'); + } + public function testExecutionOfCustomSymfonyCommand(): void { $this->initTempComposer([ diff --git a/tests/Composer/Test/Util/ConfigValidatorTest.php b/tests/Composer/Test/Util/ConfigValidatorTest.php index a43fc6358..c6d2bbfde 100644 --- a/tests/Composer/Test/Util/ConfigValidatorTest.php +++ b/tests/Composer/Test/Util/ConfigValidatorTest.php @@ -46,6 +46,17 @@ class ConfigValidatorTest extends TestCase ); } + public function testConfigValidatorWarnsOnScriptAliasForNonexistentScript(): void + { + $configValidator = new ConfigValidator(new NullIO()); + [, , $warnings] = $configValidator->validate(__DIR__ . '/Fixtures/composer_scripts-aliases.json'); + + $this->assertContains( + 'Aliases for non-existent script "phpcsxxx" found in "scripts-aliases"', + $warnings + ); + } + public function testConfigValidatorWarnsOnUnnecessaryProvideReplace(): void { $configValidator = new ConfigValidator(new NullIO()); diff --git a/tests/Composer/Test/Util/Fixtures/composer_scripts-aliases.json b/tests/Composer/Test/Util/Fixtures/composer_scripts-aliases.json new file mode 100644 index 000000000..21b552d8f --- /dev/null +++ b/tests/Composer/Test/Util/Fixtures/composer_scripts-aliases.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "test": "phpunit", + "phpcs": "phpcs" + }, + "scripts-aliases": { + "test": ["t"], + "phpcsxxx": ["x"] + } +}