VersionParser   F
last analyzed

Complexity

Total Complexity 136

Size/Duplication

Total Lines 325
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 136
eloc 221
dl 0
loc 325
rs 2
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
F parseConstraint() 0 132 70
A normalizeDefaultBranch() 0 6 4
B parseStability() 0 22 10
B manipulateVersionString() 0 17 7
A normalizeStability() 0 4 2
A expandStability() 0 15 6
B parseConstraints() 0 33 8
A parseNumericAliasPrefix() 0 6 2
A normalizeBranch() 0 11 4
F normalize() 0 57 23

How to fix   Complexity   

Complex Class

Complex classes like VersionParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use VersionParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace HumbugBox451\Composer\Semver;
4
5
use HumbugBox451\Composer\Semver\Constraint\ConstraintInterface;
6
use HumbugBox451\Composer\Semver\Constraint\MatchAllConstraint;
7
use HumbugBox451\Composer\Semver\Constraint\MultiConstraint;
8
use HumbugBox451\Composer\Semver\Constraint\Constraint;
9
/** @internal */
10
class VersionParser
11
{
12
    private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\\d+)*+)?)?([.-]?dev)?';
13
    private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev';
14
    /**
15
    @phpstan-return
16
    */
17
    public static function parseStability($version)
18
    {
19
        $version = (string) \preg_replace('{#.+$}', '', (string) $version);
20
        if (\strpos($version, 'dev-') === 0 || '-dev' === \substr($version, -4)) {
21
            return 'dev';
22
        }
23
        \preg_match('{' . self::$modifierRegex . '(?:\\+.*)?$}i', \strtolower($version), $match);
24
        if (!empty($match[3])) {
25
            return 'dev';
26
        }
27
        if (!empty($match[1])) {
28
            if ('beta' === $match[1] || 'b' === $match[1]) {
29
                return 'beta';
30
            }
31
            if ('alpha' === $match[1] || 'a' === $match[1]) {
32
                return 'alpha';
33
            }
34
            if ('rc' === $match[1]) {
35
                return 'RC';
36
            }
37
        }
38
        return 'stable';
39
    }
40
    public static function normalizeStability($stability)
41
    {
42
        $stability = \strtolower((string) $stability);
43
        return $stability === 'rc' ? 'RC' : $stability;
44
    }
45
    public function normalize($version, $fullVersion = null)
46
    {
47
        $version = \trim((string) $version);
48
        $origVersion = $version;
49
        if (null === $fullVersion) {
50
            $fullVersion = $version;
51
        }
52
        if (\preg_match('{^([^,\\s]++) ++as ++([^,\\s]++)$}', $version, $match)) {
53
            $version = $match[1];
54
        }
55
        if (\preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) {
56
            $version = \substr($version, 0, \strlen($version) - \strlen($match[0]));
57
        }
58
        if (\in_array($version, array('master', 'trunk', 'default'), \true)) {
59
            $version = 'dev-' . $version;
60
        }
61
        if (\stripos($version, 'dev-') === 0) {
62
            return 'dev-' . \substr($version, 4);
63
        }
64
        if (\preg_match('{^([^,\\s+]++)\\+[^\\s]++$}', $version, $match)) {
65
            $version = $match[1];
66
        }
67
        if (\preg_match('{^v?(\\d{1,5}+)(\\.\\d++)?(\\.\\d++)?(\\.\\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) {
68
            $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0');
69
            $index = 5;
70
        } elseif (\preg_match('{^v?(\\d{4}(?:[.:-]?\\d{2}){1,6}(?:[.:-]?\\d{1,3}){0,2})' . self::$modifierRegex . '$}i', $version, $matches)) {
71
            $version = (string) \preg_replace('{\\D}', '.', $matches[1]);
72
            $index = 2;
73
        }
74
        if (isset($index)) {
75
            if (!empty($matches[$index])) {
76
                if ('stable' === $matches[$index]) {
77
                    return $version;
78
                }
79
                $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? \ltrim($matches[$index + 1], '.-') : '');
80
            }
81
            if (!empty($matches[$index + 2])) {
82
                $version .= '-dev';
83
            }
84
            return $version;
85
        }
86
        if (\preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
87
            try {
88
                $normalized = $this->normalizeBranch($match[1]);
89
                if (\strpos($normalized, 'dev-') === \false) {
90
                    return $normalized;
91
                }
92
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
93
            }
94
        }
95
        $extraMessage = '';
96
        if (\preg_match('{ +as +' . \preg_quote($version) . '(?:@(?:' . self::$stabilitiesRegex . '))?$}', $fullVersion)) {
97
            $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
98
        } elseif (\preg_match('{^' . \preg_quote($version) . '(?:@(?:' . self::$stabilitiesRegex . '))? +as +}', $fullVersion)) {
99
            $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
100
        }
101
        throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage);
102
    }
103
    public function parseNumericAliasPrefix($branch)
104
    {
105
        if (\preg_match('{^(?P<version>(\\d++\\.)*\\d++)(?:\\.x)?-dev$}i', (string) $branch, $matches)) {
106
            return $matches['version'] . '.';
107
        }
108
        return \false;
109
    }
110
    public function normalizeBranch($name)
111
    {
112
        $name = \trim((string) $name);
113
        if (\preg_match('{^v?(\\d++)(\\.(?:\\d++|[xX*]))?(\\.(?:\\d++|[xX*]))?(\\.(?:\\d++|[xX*]))?$}i', $name, $matches)) {
114
            $version = '';
115
            for ($i = 1; $i < 5; ++$i) {
116
                $version .= isset($matches[$i]) ? \str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
117
            }
118
            return \str_replace('x', '9999999', $version) . '-dev';
119
        }
120
        return 'dev-' . $name;
121
    }
122
    public function normalizeDefaultBranch($name)
123
    {
124
        if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') {
125
            return '9999999-dev';
126
        }
127
        return (string) $name;
128
    }
129
    public function parseConstraints($constraints)
130
    {
131
        $prettyConstraint = (string) $constraints;
132
        $orConstraints = \preg_split('{\\s*\\|\\|?\\s*}', \trim((string) $constraints));
133
        if (\false === $orConstraints) {
134
            throw new \RuntimeException('Failed to preg_split string: ' . $constraints);
135
        }
136
        $orGroups = array();
137
        foreach ($orConstraints as $orConstraint) {
138
            $andConstraints = \preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $orConstraint);
139
            if (\false === $andConstraints) {
140
                throw new \RuntimeException('Failed to preg_split string: ' . $orConstraint);
141
            }
142
            if (\count($andConstraints) > 1) {
143
                $constraintObjects = array();
144
                foreach ($andConstraints as $andConstraint) {
145
                    foreach ($this->parseConstraint($andConstraint) as $parsedAndConstraint) {
146
                        $constraintObjects[] = $parsedAndConstraint;
147
                    }
148
                }
149
            } else {
150
                $constraintObjects = $this->parseConstraint($andConstraints[0]);
151
            }
152
            if (1 === \count($constraintObjects)) {
153
                $constraint = $constraintObjects[0];
154
            } else {
155
                $constraint = new MultiConstraint($constraintObjects);
156
            }
157
            $orGroups[] = $constraint;
158
        }
159
        $parsedConstraint = MultiConstraint::create($orGroups, \false);
160
        $parsedConstraint->setPrettyString($prettyConstraint);
161
        return $parsedConstraint;
162
    }
163
    /**
164
    @phpstan-return
165
    */
166
    private function parseConstraint($constraint)
167
    {
168
        if (\preg_match('{^([^,\\s]++) ++as ++([^,\\s]++)$}', $constraint, $match)) {
169
            $constraint = $match[1];
170
        }
171
        if (\preg_match('{^([^,\\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) {
172
            $constraint = '' !== $match[1] ? $match[1] : '*';
173
            if ($match[2] !== 'stable') {
174
                $stabilityModifier = $match[2];
175
            }
176
        }
177
        if (\preg_match('{^(dev-[^,\\s@]+?|[^,\\s@]+?\\.x-dev)#.+$}i', $constraint, $match)) {
178
            $constraint = $match[1];
179
        }
180
        if (\preg_match('{^(v)?[xX*](\\.[xX*])*$}i', $constraint, $match)) {
181
            if (!empty($match[1]) || !empty($match[2])) {
182
                return array(new Constraint('>=', '0.0.0.0-dev'));
183
            }
184
            return array(new MatchAllConstraint());
185
        }
186
        $versionRegex = 'v?(\\d++)(?:\\.(\\d++))?(?:\\.(\\d++))?(?:\\.(\\d++))?(?:' . self::$modifierRegex . '|\\.([xX*][.-]?dev))(?:\\+[^\\s]+)?';
187
        if (\preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
188
            if (\strpos($constraint, '~>') === 0) {
189
                throw new \UnexpectedValueException('Could not parse version constraint ' . $constraint . ': ' . 'Invalid operator "~>", you probably meant to use the "~" operator');
190
            }
191
            if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) {
192
                $position = 4;
193
            } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
194
                $position = 3;
195
            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
196
                $position = 2;
197
            } else {
198
                $position = 1;
199
            }
200
            if (!empty($matches[8])) {
201
                $position++;
202
            }
203
            $stabilitySuffix = '';
204
            if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
205
                $stabilitySuffix .= '-dev';
206
            }
207
            $lowVersion = $this->normalize(\substr($constraint . $stabilitySuffix, 1));
208
            $lowerBound = new Constraint('>=', $lowVersion);
209
            $highPosition = \max(1, $position - 1);
210
            $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
211
            $upperBound = new Constraint('<', $highVersion);
212
            return array($lowerBound, $upperBound);
213
        }
214
        if (\preg_match('{^\\^' . $versionRegex . '($)}i', $constraint, $matches)) {
215
            if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) {
216
                $position = 1;
217
            } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) {
218
                $position = 2;
219
            } else {
220
                $position = 3;
221
            }
222
            $stabilitySuffix = '';
223
            if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
224
                $stabilitySuffix .= '-dev';
225
            }
226
            $lowVersion = $this->normalize(\substr($constraint . $stabilitySuffix, 1));
227
            $lowerBound = new Constraint('>=', $lowVersion);
228
            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
229
            $upperBound = new Constraint('<', $highVersion);
230
            return array($lowerBound, $upperBound);
231
        }
232
        if (\preg_match('{^v?(\\d++)(?:\\.(\\d++))?(?:\\.(\\d++))?(?:\\.[xX*])++$}', $constraint, $matches)) {
233
            if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
234
                $position = 3;
235
            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
236
                $position = 2;
237
            } else {
238
                $position = 1;
239
            }
240
            $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev';
241
            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
242
            if ($lowVersion === '0.0.0.0-dev') {
243
                return array(new Constraint('<', $highVersion));
244
            }
245
            return array(new Constraint('>=', $lowVersion), new Constraint('<', $highVersion));
246
        }
247
        if (\preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
248
            $lowStabilitySuffix = '';
249
            if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) {
250
                $lowStabilitySuffix = '-dev';
251
            }
252
            $lowVersion = $this->normalize($matches['from']);
253
            $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix);
254
            $empty = function ($x) {
255
                return $x === 0 || $x === '0' ? \false : empty($x);
256
            };
257
            if (!$empty($matches[12]) && !$empty($matches[13]) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) {
258
                $highVersion = $this->normalize($matches['to']);
259
                $upperBound = new Constraint('<=', $highVersion);
260
            } else {
261
                $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]);
262
                $this->normalize($matches['to']);
263
                $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev';
264
                $upperBound = new Constraint('<', $highVersion);
265
            }
266
            return array($lowerBound, $upperBound);
267
        }
268
        if (\preg_match('{^(<>|!=|>=?|<=?|==?)?\\s*(.*)}', $constraint, $matches)) {
269
            try {
270
                try {
271
                    $version = $this->normalize($matches[2]);
272
                } catch (\UnexpectedValueException $e) {
273
                    if (\substr($matches[2], -4) === '-dev' && \preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) {
274
                        $version = $this->normalize('dev-' . \substr($matches[2], 0, -4));
275
                    } else {
276
                        throw $e;
277
                    }
278
                }
279
                $op = $matches[1] ?: '=';
280
                if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') {
281
                    $version .= '-' . $stabilityModifier;
282
                } elseif ('<' === $op || '>=' === $op) {
283
                    if (!\preg_match('/-' . self::$modifierRegex . '$/', \strtolower($matches[2]))) {
284
                        if (\strpos($matches[2], 'dev-') !== 0) {
285
                            $version .= '-dev';
286
                        }
287
                    }
288
                }
289
                return array(new Constraint($matches[1] ?: '=', $version));
290
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
291
            }
292
        }
293
        $message = 'Could not parse version constraint ' . $constraint;
294
        if (isset($e)) {
295
            $message .= ': ' . $e->getMessage();
296
        }
297
        throw new \UnexpectedValueException($message);
298
    }
299
    /**
300
    @phpstan-param
301
    */
302
    private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0')
303
    {
304
        for ($i = 4; $i > 0; --$i) {
305
            if ($i > $position) {
306
                $matches[$i] = $pad;
307
            } elseif ($i === $position && $increment) {
308
                $matches[$i] += $increment;
309
                if ($matches[$i] < 0) {
310
                    $matches[$i] = $pad;
311
                    --$position;
312
                    if ($i === 1) {
313
                        return null;
314
                    }
315
                }
316
            }
317
        }
318
        return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
319
    }
320
    private function expandStability($stability)
321
    {
322
        $stability = \strtolower($stability);
323
        switch ($stability) {
324
            case 'a':
325
                return 'alpha';
326
            case 'b':
327
                return 'beta';
328
            case 'p':
329
            case 'pl':
330
                return 'patch';
331
            case 'rc':
332
                return 'RC';
333
            default:
334
                return $stability;
335
        }
336
    }
337
}
338