Skip to content

Commit c792026

Browse files
Fix resolving aliases when running composer req
1 parent 9cd3847 commit c792026

1 file changed

Lines changed: 52 additions & 2 deletions

File tree

src/Flex.php

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use Composer\Script\Event;
4141
use Composer\Script\ScriptEvents;
4242
use Composer\Semver\VersionParser;
43+
use Symfony\Component\Console\Exception\ExceptionInterface as ConsoleExceptionInterface;
4344
use Symfony\Component\Console\Input\ArgvInput;
4445
use Symfony\Component\Filesystem\Filesystem;
4546
use 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

Comments
 (0)