Lines 92.59% 25 / 27
Functions and Methods 60.00% 3 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
StewartApplication 92.59% 25 / 27 60.00% 3 / 5 17.12 0.00% 0 / 1
 __construct 100.00% 2 / 2 100.00% 1 / 1 1
 getCommandName 66.66% 2 / 3 0.00% 0 / 1 2.15
 findCommandNameAfterLeadingOptions 90.90% 10 / 11 0.00% 0 / 1 7.04
 findLeadingOption 100.00% 7 / 7 100.00% 1 / 1 6
 buildLeadingOptionDefinition 100.00% 4 / 4 100.00% 1 / 1 1
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Runtime\Console;
6
7use Symfony\Component\Console\Application;
8use Symfony\Component\Console\Input\ArgvInput;
9use Symfony\Component\Console\Input\InputDefinition;
10use Symfony\Component\Console\Input\InputInterface;
11use Symfony\Component\Console\Input\InputOption;
12
13final class StewartApplication extends Application
14{
15    private const string END_OF_OPTIONS = '--';
16
17    public function __construct(string $version)
18    {
19        parent::__construct('Stewart', $version);
20        $this->setDefaultCommand(RunCommand::NAME);
21    }
22
23    protected function getCommandName(InputInterface $input): ?string
24    {
25        if (!$input instanceof ArgvInput) {
26            return parent::getCommandName($input);
27        }
28
29        // Options before the command name belong to the default command, so their values are not command names.
30        return $this->findCommandNameAfterLeadingOptions($input->getRawTokens(), $this->buildLeadingOptionDefinition());
31    }
32
33    /** @param list<string> $tokens */
34    private function findCommandNameAfterLeadingOptions(array $tokens, InputDefinition $leadingOptions): ?string
35    {
36        $skipNext = false;
37
38        foreach ($tokens as $index => $token) {
39            if ($skipNext) {
40                $skipNext = false;
41
42                continue;
43            }
44
45            if ($token === self::END_OF_OPTIONS) {
46                return $tokens[$index + 1] ?? null;
47            }
48
49            if ($token === '' || $token === '-' || $token[0] !== '-') {
50                return $token;
51            }
52
53            $skipNext = $this->findLeadingOption($token, $leadingOptions)?->acceptValue() ?? false;
54        }
55
56        return null;
57    }
58
59    private function findLeadingOption(string $token, InputDefinition $leadingOptions): ?InputOption
60    {
61        if (str_contains($token, '=')) {
62            return null;
63        }
64
65        if (str_starts_with($token, self::END_OF_OPTIONS)) {
66            $name = substr($token, \strlen(self::END_OF_OPTIONS));
67
68            return $leadingOptions->hasOption($name) ? $leadingOptions->getOption($name) : null;
69        }
70
71        $shortcut = substr($token, 1);
72
73        return \strlen($shortcut) === 1 && $leadingOptions->hasShortcut($shortcut) ? $leadingOptions->getOptionForShortcut($shortcut) : null;
74    }
75
76    private function buildLeadingOptionDefinition(): InputDefinition
77    {
78        return new InputDefinition([
79            ...$this->getDefinition()->getOptions(),
80            ...$this->get(RunCommand::NAME)->getNativeDefinition()->getOptions(),
81        ]);
82    }
83}