Test Failed
Pull Request — master (#872)
by Maxim
10:54 queued 03:52
created

Parser::parse()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 5
dl 0
loc 11
rs 10
c 1
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Console\Configurator\Signature;
6
7
use InvalidArgumentException;
8
use Spiral\Console\Configurator\CommandDefinition;
9
use Symfony\Component\Console\Input\InputArgument;
10
use Symfony\Component\Console\Input\InputOption;
11
12
/**
13
 * Console signature parser.
14
 * @internal
15
 */
16
final class Parser
17
{
18
    /**
19
     * Parse the given console command definition into an array.
20
     *
21
     * @throws InvalidArgumentException
22
     */
23
    public function parse(string $signature): CommandDefinition
24
    {
25
        $name = $this->parseName($signature);
26
27
        if (\preg_match_all('/\{\s*(.*?)\s*\}/', $signature, $matches)) {
28
            if (\count($matches[1])) {
29
                return new CommandDefinition($name, ...$this->parseParameters($matches[1]));
0 ignored issues
show
Bug introduced by
$this->parseParameters($matches[1]) is expanded, but the parameter $arguments of Spiral\Console\Configura...finition::__construct() does not expect variable arguments. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

29
                return new CommandDefinition($name, /** @scrutinizer ignore-type */ ...$this->parseParameters($matches[1]));
Loading history...
30
            }
31
        }
32
33
        return new CommandDefinition($name);
34
    }
35
36
    /**
37
     * Extract the name of the command from the expression.
38
     *
39
     * @throws InvalidArgumentException
40
     */
41
    private function parseName(string $signature): string
42
    {
43
        if (!\preg_match('/\S+/', $signature, $matches)) {
44
            throw new InvalidArgumentException('Unable to determine command name from signature.');
45
        }
46
47
        return $matches[0];
48
    }
49
50
    /**
51
     * Extract all the parameters from the tokens.
52
     *
53
     * @return array{0: InputArgument[], 1: InputOption[]}
54
     */
55
    private function parseParameters(array $tokens): array
56
    {
57
        $arguments = [];
58
        $options = [];
59
60
        foreach ($tokens as $token) {
61
            if (\preg_match('/-{2,}(.*)/', $token, $matches)) {
62
                $options[] = $this->parseOption($matches[1]);
63
            } else {
64
                $arguments[] = $this->parseArgument($token);
65
            }
66
        }
67
68
        return [$arguments, $options];
69
    }
70
71
    /**
72
     * Parse an argument expression.
73
     *
74
     * @noRector \Rector\Php56\Rector\FunctionLike\AddDefaultValueForUndefinedVariableRector
75
     */
76
    private function parseArgument(string $token): InputArgument
77
    {
78
        [$token, $description] = $this->extractDescription($token);
79
80
        return match (true) {
81
            \str_ends_with($token, '[]?') => new InputArgument(
82
                \rtrim($token, '[]?'),
83
                InputArgument::IS_ARRAY,
84
                $description
85
            ),
86
            \str_ends_with($token, '[]') => new InputArgument(
87
                \rtrim($token, '[]'),
88
                InputArgument::IS_ARRAY | InputArgument::REQUIRED,
89
                $description
90
            ),
91
            \str_ends_with($token, '?') => new InputArgument(
92
                \rtrim($token, '?'),
93
                InputArgument::OPTIONAL,
94
                $description
95
            ),
96
            (bool)\preg_match('/(.+)\[\]\=(.+)/', $token, $matches) => new InputArgument(
97
                $matches[1],
98
                InputArgument::IS_ARRAY,
99
                $description,
100
                \preg_split('/,\s?/', $matches[2])
101
            ),
102
            (bool)\preg_match('/(.+)\=(.+)?/', $token, $matches) => new InputArgument(
103
                $matches[1],
104
                InputArgument::OPTIONAL,
105
                $description,
106
                $matches[2] ?? null
107
            ),
108
            default => new InputArgument(
109
                $token,
110
                InputArgument::REQUIRED,
111
                $description
112
            ),
113
        };
114
    }
115
116
    /**
117
     * Parse an option expression.
118
     */
119
    private function parseOption(string $token): InputOption
120
    {
121
        [$token, $description] = $this->extractDescription($token);
122
123
        $matches = \preg_split('/\s*\|\s*/', $token, 2);
124
        $shortcut = null;
125
126
        if (isset($matches[1])) {
127
            [$shortcut, $token] = $matches;
128
        }
129
130
        return match (true) {
131
            \str_ends_with($token, '[]=') => new InputOption(
132
                \rtrim($token, '[]='),
133
                $shortcut,
134
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
135
                $description
136
            ),
137
            \str_ends_with($token, '=') => new InputOption(
138
                \rtrim($token, '='),
139
                $shortcut,
140
                InputOption::VALUE_OPTIONAL,
141
                $description
142
            ),
143
            (bool)\preg_match('/(.+)\[\]\=(.+)/', $token, $matches) => new InputOption(
144
                $matches[1],
145
                $shortcut,
146
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
147
                $description,
148
                \preg_split('/,\s?/', $matches[2])
149
            ),
150
            (bool)\preg_match('/(.+)\=(.+)/', $token, $matches) => new InputOption(
151
                $matches[1],
152
                $shortcut,
153
                InputOption::VALUE_OPTIONAL,
154
                $description,
155
                $matches[2]
156
            ),
157
            default => new InputOption(
158
                $token,
159
                $shortcut,
160
                InputOption::VALUE_NONE,
161
                $description
162
            ),
163
        };
164
    }
165
166
    /**
167
     * Parse the token into its token and description segments.
168
     *
169
     * @return array{
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
170
     *     0: non-empty-string,
171
     *     1: string
172
     * }
173
     */
174
    private function extractDescription(string $token): array
175
    {
176
        $token = \trim($token);
177
178
        $parts = \array_map('trim', \explode(':', $token, 2));
179
180
        return \count($parts) === 2 ? $parts : [$token, ''];
181
    }
182
}
183