4040use Composer \Script \Event ;
4141use Composer \Script \ScriptEvents ;
4242use Composer \Semver \VersionParser ;
43+ use Symfony \Component \Console \Exception \ExceptionInterface as ConsoleExceptionInterface ;
4344use Symfony \Component \Console \Input \ArgvInput ;
4445use Symfony \Component \Filesystem \Filesystem ;
4546use Symfony \Flex \Event \UpdateEvent ;
@@ -161,9 +162,11 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
161162
162163 $ resolver = new PackageResolver ($ this ->downloader );
163164
165+ $ commandObj = null ;
164166 try {
165167 $ command = $ input ->getFirstArgument ();
166- $ command = $ command ? $ app ->find ($ command )->getName () : null ;
168+ $ commandObj = $ command ? $ app ->find ($ command ) : null ;
169+ $ command = $ commandObj ? $ commandObj ->getName () : null ;
167170 } catch (\InvalidArgumentException $ e ) {
168171 }
169172
@@ -178,8 +181,25 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
178181 }
179182
180183 if (isset (self ::$ aliasResolveCommands [$ command ])) {
184+ // When the command name is abbreviated (e.g. "req" for "require"), Composer
185+ // activates plugins early to look up potential script commands, before the
186+ // input has been bound to the command definition. In that case "packages"
187+ // isn't a known argument yet, so bind the command definition first.
188+ if (null !== $ commandObj && !$ input ->hasArgument ('packages ' )) {
189+ $ commandObj ->mergeApplicationDefinition ();
190+ try {
191+ $ input ->bind ($ commandObj ->getDefinition ());
192+ } catch (ConsoleExceptionInterface $ e ) {
193+ }
194+ }
181195 if ($ input ->hasArgument ('packages ' )) {
182- $ input ->setArgument ('packages ' , $ resolver ->resolve ($ input ->getArgument ('packages ' ), self ::$ aliasResolveCommands [$ command ]));
196+ $ packages = $ input ->getArgument ('packages ' );
197+ $ resolved = $ resolver ->resolve ($ packages , self ::$ aliasResolveCommands [$ command ]);
198+ $ input ->setArgument ('packages ' , $ resolved );
199+ // The command will bind its definition again at execution time, which
200+ // re-parses the raw tokens. Rewrite them too so the resolved package
201+ // names survive that rebinding.
202+ $ this ->rewritePackageTokens ($ input , $ packages , $ resolved );
183203 }
184204 }
185205
@@ -206,6 +226,36 @@ class_exists(__NAMESPACE__.str_replace('/', '\\', substr($file, \strlen(__DIR__)
206226 }
207227 }
208228
229+ /**
230+ * Rewrites the raw input tokens so resolved package names survive a later rebinding
231+ * of the input to the command definition (which re-parses the tokens from scratch).
232+ */
233+ private function rewritePackageTokens (ArgvInput $ input , array $ original , array $ resolved ): void
234+ {
235+ if ($ original === $ resolved ) {
236+ return ;
237+ }
238+
239+ try {
240+ $ property = new \ReflectionProperty (ArgvInput::class, 'tokens ' );
241+ } catch (\ReflectionException $ e ) {
242+ return ;
243+ }
244+ $ tokens = $ property ->getValue ($ input );
245+
246+ // Drop the tokens matching the original package arguments (each one once, keeping
247+ // options and the command name in place), then append the resolved ones at the end.
248+ // Argument order relative to options is irrelevant when the tokens are re-parsed.
249+ $ remaining = $ original ;
250+ foreach ($ tokens as $ i => $ token ) {
251+ if (false !== $ pos = array_search ($ token , $ remaining , true )) {
252+ unset($ tokens [$ i ], $ remaining [$ pos ]);
253+ }
254+ }
255+
256+ $ property ->setValue ($ input , array_merge (array_values ($ tokens ), $ resolved ));
257+ }
258+
209259 /**
210260 * @return void
211261 */
0 commit comments