VersionParser::parseConstraint()   F
last analyzed

Complexity

Conditions 70
Paths 1820

Size

Total Lines 218
Code Lines 111

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 111
c 1
b 0
f 0
dl 0
loc 218
rs 0
cc 70
nc 1820
nop 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of composer/semver.
5
 *
6
 * (c) Composer <https://github.com/composer>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Composer\Semver;
13
14
use Composer\Semver\Constraint\ConstraintInterface;
15
use Composer\Semver\Constraint\MatchAllConstraint;
16
use Composer\Semver\Constraint\MultiConstraint;
17
use Composer\Semver\Constraint\Constraint;
18
19
/**
20
 * Version parser.
21
 *
22
 * @author Jordi Boggiano <[email protected]>
23
 */
24
class VersionParser
25
{
26
    /**
27
     * Regex to match pre-release data (sort of).
28
     *
29
     * Due to backwards compatibility:
30
     *   - Instead of enforcing hyphen, an underscore, dot or nothing at all are also accepted.
31
     *   - Only stabilities as recognized by Composer are allowed to precede a numerical identifier.
32
     *   - Numerical-only pre-release identifiers are not supported, see tests.
33
     *
34
     *                        |--------------|
35
     * [major].[minor].[patch] -[pre-release] +[build-metadata]
36
     *
37
     * @var string
38
     */
39
    private static $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)((?:[.-]?\d+)*+)?)?([.-]?dev)?';
40
41
    /** @var string */
42
    private static $stabilitiesRegex = 'stable|RC|beta|alpha|dev';
43
44
    /**
45
     * Returns the stability of a version.
46
     *
47
     * @param string $version
48
     *
49
     * @return string
50
     */
51
    public static function parseStability($version)
52
    {
53
        $version = (string) preg_replace('{#.+$}', '', $version);
54
55
        if (strpos($version, 'dev-') === 0 || '-dev' === substr($version, -4)) {
56
            return 'dev';
57
        }
58
59
        preg_match('{' . self::$modifierRegex . '(?:\+.*)?$}i', strtolower($version), $match);
60
61
        if (!empty($match[3])) {
62
            return 'dev';
63
        }
64
65
        if (!empty($match[1])) {
66
            if ('beta' === $match[1] || 'b' === $match[1]) {
67
                return 'beta';
68
            }
69
            if ('alpha' === $match[1] || 'a' === $match[1]) {
70
                return 'alpha';
71
            }
72
            if ('rc' === $match[1]) {
73
                return 'RC';
74
            }
75
        }
76
77
        return 'stable';
78
    }
79
80
    /**
81
     * @param string $stability
82
     *
83
     * @return string
84
     */
85
    public static function normalizeStability($stability)
86
    {
87
        $stability = strtolower($stability);
88
89
        return $stability === 'rc' ? 'RC' : $stability;
90
    }
91
92
    /**
93
     * Normalizes a version string to be able to perform comparisons on it.
94
     *
95
     * @param string $version
96
     * @param string $fullVersion optional complete version string to give more context
97
     *
98
     * @throws \UnexpectedValueException
99
     *
100
     * @return string
101
     */
102
    public function normalize($version, $fullVersion = null)
103
    {
104
        $version = trim($version);
105
        $origVersion = $version;
106
        if (null === $fullVersion) {
107
            $fullVersion = $version;
108
        }
109
110
        // strip off aliasing
111
        if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $version, $match)) {
112
            $version = $match[1];
113
        }
114
115
        // strip off stability flag
116
        if (preg_match('{@(?:' . self::$stabilitiesRegex . ')$}i', $version, $match)) {
117
            $version = substr($version, 0, strlen($version) - strlen($match[0]));
118
        }
119
120
        // normalize master/trunk/default branches to dev-name for BC with 1.x as these used to be valid constraints
121
        if (\in_array($version, array('master', 'trunk', 'default'), true)) {
122
            $version = 'dev-' . $version;
123
        }
124
125
        // if requirement is branch-like, use full name
126
        if (stripos($version, 'dev-') === 0) {
127
            return 'dev-' . substr($version, 4);
128
        }
129
130
        // strip off build metadata
131
        if (preg_match('{^([^,\s+]++)\+[^\s]++$}', $version, $match)) {
132
            $version = $match[1];
133
        }
134
135
        // match classical versioning
136
        if (preg_match('{^v?(\d{1,5})(\.\d++)?(\.\d++)?(\.\d++)?' . self::$modifierRegex . '$}i', $version, $matches)) {
137
            $version = $matches[1]
138
                . (!empty($matches[2]) ? $matches[2] : '.0')
139
                . (!empty($matches[3]) ? $matches[3] : '.0')
140
                . (!empty($matches[4]) ? $matches[4] : '.0');
141
            $index = 5;
142
        // match date(time) based versioning
143
        } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) {
144
            $version = preg_replace('{\D}', '.', $matches[1]);
145
            $index = 2;
146
        }
147
148
        // add version modifiers if a version was matched
149
        if (isset($index)) {
150
            if (!empty($matches[$index])) {
151
                if ('stable' === $matches[$index]) {
152
                    return $version;
153
                }
154
                $version .= '-' . $this->expandStability($matches[$index]) . (isset($matches[$index + 1]) && '' !== $matches[$index + 1] ? ltrim($matches[$index + 1], '.-') : '');
155
            }
156
157
            if (!empty($matches[$index + 2])) {
158
                $version .= '-dev';
159
            }
160
161
            return $version;
162
        }
163
164
        // match dev branches
165
        if (preg_match('{(.*?)[.-]?dev$}i', $version, $match)) {
166
            try {
167
                $normalized = $this->normalizeBranch($match[1]);
168
                // a branch ending with -dev is only valid if it is numeric
169
                // if it gets prefixed with dev- it means the branch name should
170
                // have had a dev- prefix already when passed to normalize
171
                if (strpos($normalized, 'dev-') === false) {
172
                    return $normalized;
173
                }
174
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
175
            }
176
        }
177
178
        $extraMessage = '';
179
        if (preg_match('{ +as +' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))?$}', $fullVersion)) {
180
            $extraMessage = ' in "' . $fullVersion . '", the alias must be an exact version';
181
        } elseif (preg_match('{^' . preg_quote($version) . '(?:@(?:'.self::$stabilitiesRegex.'))? +as +}', $fullVersion)) {
182
            $extraMessage = ' in "' . $fullVersion . '", the alias source must be an exact version, if it is a branch name you should prefix it with dev-';
183
        }
184
185
        throw new \UnexpectedValueException('Invalid version string "' . $origVersion . '"' . $extraMessage);
186
    }
187
188
    /**
189
     * Extract numeric prefix from alias, if it is in numeric format, suitable for version comparison.
190
     *
191
     * @param string $branch Branch name (e.g. 2.1.x-dev)
192
     *
193
     * @return string|false Numeric prefix if present (e.g. 2.1.) or false
194
     */
195
    public function parseNumericAliasPrefix($branch)
196
    {
197
        if (preg_match('{^(?P<version>(\d++\\.)*\d++)(?:\.x)?-dev$}i', $branch, $matches)) {
198
            return $matches['version'] . '.';
199
        }
200
201
        return false;
202
    }
203
204
    /**
205
     * Normalizes a branch name to be able to perform comparisons on it.
206
     *
207
     * @param string $name
208
     *
209
     * @return string
210
     */
211
    public function normalizeBranch($name)
212
    {
213
        $name = trim($name);
214
215
        if (preg_match('{^v?(\d++)(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?(\.(?:\d++|[xX*]))?$}i', $name, $matches)) {
216
            $version = '';
217
            for ($i = 1; $i < 5; ++$i) {
218
                $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
219
            }
220
221
            return str_replace('x', '9999999', $version) . '-dev';
222
        }
223
224
        return 'dev-' . $name;
225
    }
226
227
    /**
228
     * Normalizes a default branch name (i.e. master on git) to 9999999-dev.
229
     *
230
     * @param string $name
231
     *
232
     * @return string
233
     */
234
    public function normalizeDefaultBranch($name)
235
    {
236
        if ($name === 'dev-master' || $name === 'dev-default' || $name === 'dev-trunk') {
237
            return '9999999-dev';
238
        }
239
240
        return $name;
241
    }
242
243
    /**
244
     * Parses a constraint string into MultiConstraint and/or Constraint objects.
245
     *
246
     * @param string $constraints
247
     *
248
     * @return ConstraintInterface
249
     */
250
    public function parseConstraints($constraints)
251
    {
252
        $prettyConstraint = $constraints;
253
254
        $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
255
        if (false === $orConstraints) {
256
            throw new \RuntimeException('Failed to preg_split string: '.$constraints);
257
        }
258
        $orGroups = array();
259
260
        foreach ($orConstraints as $constraints) {
0 ignored issues
show
introduced by
$constraints is overwriting one of the parameters of this function.
Loading history...
261
            $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
262
            if (false === $andConstraints) {
263
                throw new \RuntimeException('Failed to preg_split string: '.$constraints);
264
            }
265
            if (\count($andConstraints) > 1) {
266
                $constraintObjects = array();
267
                foreach ($andConstraints as $constraint) {
268
                    foreach ($this->parseConstraint($constraint) as $parsedConstraint) {
269
                        $constraintObjects[] = $parsedConstraint;
270
                    }
271
                }
272
            } else {
273
                $constraintObjects = $this->parseConstraint($andConstraints[0]);
274
            }
275
276
            if (1 === \count($constraintObjects)) {
277
                $constraint = $constraintObjects[0];
278
            } else {
279
                $constraint = new MultiConstraint($constraintObjects);
280
            }
281
282
            $orGroups[] = $constraint;
283
        }
284
285
        $constraint = MultiConstraint::create($orGroups, false);
286
287
        $constraint->setPrettyString($prettyConstraint);
288
289
        return $constraint;
290
    }
291
292
    /**
293
     * @param string $constraint
294
     *
295
     * @throws \UnexpectedValueException
296
     *
297
     * @return array
298
     *
299
     * @phpstan-return non-empty-array<ConstraintInterface>
300
     */
301
    private function parseConstraint($constraint)
302
    {
303
        // strip off aliasing
304
        if (preg_match('{^([^,\s]++) ++as ++([^,\s]++)$}', $constraint, $match)) {
305
            $constraint = $match[1];
306
        }
307
308
        // strip @stability flags, and keep it for later use
309
        if (preg_match('{^([^,\s]*?)@(' . self::$stabilitiesRegex . ')$}i', $constraint, $match)) {
310
            $constraint = '' !== $match[1] ? $match[1] : '*';
311
            if ($match[2] !== 'stable') {
312
                $stabilityModifier = $match[2];
313
            }
314
        }
315
316
        // get rid of #refs as those are used by composer only
317
        if (preg_match('{^(dev-[^,\s@]+?|[^,\s@]+?\.x-dev)#.+$}i', $constraint, $match)) {
318
            $constraint = $match[1];
319
        }
320
321
        if (preg_match('{^(v)?[xX*](\.[xX*])*$}i', $constraint, $match)) {
322
            if (!empty($match[1]) || !empty($match[2])) {
323
                return array(new Constraint('>=', '0.0.0.0-dev'));
324
            }
325
326
            return array(new MatchAllConstraint());
327
        }
328
329
        $versionRegex = 'v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.(\d++))?(?:' . self::$modifierRegex . '|\.([xX*][.-]?dev))(?:\+[^\s]+)?';
330
331
        // Tilde Range
332
        //
333
        // Like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
334
        // version, to ensure that unstable instances of the current version are allowed. However, if a stability
335
        // suffix is added to the constraint, then a >= match on the current version is used instead.
336
        if (preg_match('{^~>?' . $versionRegex . '$}i', $constraint, $matches)) {
337
            if (strpos($constraint, '~>') === 0) {
338
                throw new \UnexpectedValueException(
339
                    'Could not parse version constraint ' . $constraint . ': ' .
340
                    'Invalid operator "~>", you probably meant to use the "~" operator'
341
                );
342
            }
343
344
            // Work out which position in the version we are operating at
345
            if (isset($matches[4]) && '' !== $matches[4] && null !== $matches[4]) {
346
                $position = 4;
347
            } elseif (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
348
                $position = 3;
349
            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
350
                $position = 2;
351
            } else {
352
                $position = 1;
353
            }
354
355
            // when matching 2.x-dev or 3.0.x-dev we have to shift the second or third number, despite no second/third number matching above
356
            if (!empty($matches[8])) {
357
                $position++;
358
            }
359
360
            // Calculate the stability suffix
361
            $stabilitySuffix = '';
362
            if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
363
                $stabilitySuffix .= '-dev';
364
            }
365
366
            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
367
            $lowerBound = new Constraint('>=', $lowVersion);
368
369
            // For upper bound, we increment the position of one more significance,
370
            // but highPosition = 0 would be illegal
371
            $highPosition = max(1, $position - 1);
372
            $highVersion = $this->manipulateVersionString($matches, $highPosition, 1) . '-dev';
373
            $upperBound = new Constraint('<', $highVersion);
374
375
            return array(
376
                $lowerBound,
377
                $upperBound,
378
            );
379
        }
380
381
        // Caret Range
382
        //
383
        // Allows changes that do not modify the left-most non-zero digit in the [major, minor, patch] tuple.
384
        // In other words, this allows patch and minor updates for versions 1.0.0 and above, patch updates for
385
        // versions 0.X >=0.1.0, and no updates for versions 0.0.X
386
        if (preg_match('{^\^' . $versionRegex . '($)}i', $constraint, $matches)) {
387
            // Work out which position in the version we are operating at
388
            if ('0' !== $matches[1] || '' === $matches[2] || null === $matches[2]) {
389
                $position = 1;
390
            } elseif ('0' !== $matches[2] || '' === $matches[3] || null === $matches[3]) {
391
                $position = 2;
392
            } else {
393
                $position = 3;
394
            }
395
396
            // Calculate the stability suffix
397
            $stabilitySuffix = '';
398
            if (empty($matches[5]) && empty($matches[7]) && empty($matches[8])) {
399
                $stabilitySuffix .= '-dev';
400
            }
401
402
            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
403
            $lowerBound = new Constraint('>=', $lowVersion);
404
405
            // For upper bound, we increment the position of one more significance,
406
            // but highPosition = 0 would be illegal
407
            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
408
            $upperBound = new Constraint('<', $highVersion);
409
410
            return array(
411
                $lowerBound,
412
                $upperBound,
413
            );
414
        }
415
416
        // X Range
417
        //
418
        // Any of X, x, or * may be used to "stand in" for one of the numeric values in the [major, minor, patch] tuple.
419
        // A partial version range is treated as an X-Range, so the special character is in fact optional.
420
        if (preg_match('{^v?(\d++)(?:\.(\d++))?(?:\.(\d++))?(?:\.[xX*])++$}', $constraint, $matches)) {
421
            if (isset($matches[3]) && '' !== $matches[3] && null !== $matches[3]) {
422
                $position = 3;
423
            } elseif (isset($matches[2]) && '' !== $matches[2] && null !== $matches[2]) {
424
                $position = 2;
425
            } else {
426
                $position = 1;
427
            }
428
429
            $lowVersion = $this->manipulateVersionString($matches, $position) . '-dev';
430
            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
431
432
            if ($lowVersion === '0.0.0.0-dev') {
433
                return array(new Constraint('<', $highVersion));
434
            }
435
436
            return array(
437
                new Constraint('>=', $lowVersion),
438
                new Constraint('<', $highVersion),
439
            );
440
        }
441
442
        // Hyphen Range
443
        //
444
        // Specifies an inclusive set. If a partial version is provided as the first version in the inclusive range,
445
        // then the missing pieces are replaced with zeroes. If a partial version is provided as the second version in
446
        // the inclusive range, then all versions that start with the supplied parts of the tuple are accepted, but
447
        // nothing that would be greater than the provided tuple parts.
448
        if (preg_match('{^(?P<from>' . $versionRegex . ') +- +(?P<to>' . $versionRegex . ')($)}i', $constraint, $matches)) {
449
            // Calculate the stability suffix
450
            $lowStabilitySuffix = '';
451
            if (empty($matches[6]) && empty($matches[8]) && empty($matches[9])) {
452
                $lowStabilitySuffix = '-dev';
453
            }
454
455
            $lowVersion = $this->normalize($matches['from']);
456
            $lowerBound = new Constraint('>=', $lowVersion . $lowStabilitySuffix);
457
458
            $empty = function ($x) {
459
                return ($x === 0 || $x === '0') ? false : empty($x);
460
            };
461
462
            if ((!$empty($matches[12]) && !$empty($matches[13])) || !empty($matches[15]) || !empty($matches[17]) || !empty($matches[18])) {
463
                $highVersion = $this->normalize($matches['to']);
464
                $upperBound = new Constraint('<=', $highVersion);
465
            } else {
466
                $highMatch = array('', $matches[11], $matches[12], $matches[13], $matches[14]);
467
468
                // validate to version
469
                $this->normalize($matches['to']);
470
471
                $highVersion = $this->manipulateVersionString($highMatch, $empty($matches[12]) ? 1 : 2, 1) . '-dev';
472
                $upperBound = new Constraint('<', $highVersion);
473
            }
474
475
            return array(
476
                $lowerBound,
477
                $upperBound,
478
            );
479
        }
480
481
        // Basic Comparators
482
        if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
483
            try {
484
                try {
485
                    $version = $this->normalize($matches[2]);
486
                } catch (\UnexpectedValueException $e) {
487
                    // recover from an invalid constraint like foobar-dev which should be dev-foobar
488
                    // except if the constraint uses a known operator, in which case it must be a parse error
489
                    if (substr($matches[2], -4) === '-dev' && preg_match('{^[0-9a-zA-Z-./]+$}', $matches[2])) {
490
                        $version = $this->normalize('dev-'.substr($matches[2], 0, -4));
491
                    } else {
492
                        throw $e;
493
                    }
494
                }
495
496
                $op = $matches[1] ?: '=';
497
498
                if ($op !== '==' && $op !== '=' && !empty($stabilityModifier) && self::parseStability($version) === 'stable') {
499
                    $version .= '-' . $stabilityModifier;
500
                } elseif ('<' === $op || '>=' === $op) {
501
                    if (!preg_match('/-' . self::$modifierRegex . '$/', strtolower($matches[2]))) {
502
                        if (strpos($matches[2], 'dev-') !== 0) {
503
                            $version .= '-dev';
504
                        }
505
                    }
506
                }
507
508
                return array(new Constraint($matches[1] ?: '=', $version));
509
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
510
            }
511
        }
512
513
        $message = 'Could not parse version constraint ' . $constraint;
514
        if (isset($e)) {
515
            $message .= ': ' . $e->getMessage();
516
        }
517
518
        throw new \UnexpectedValueException($message);
519
    }
520
521
    /**
522
     * Increment, decrement, or simply pad a version number.
523
     *
524
     * Support function for {@link parseConstraint()}
525
     *
526
     * @param array  $matches   Array with version parts in array indexes 1,2,3,4
527
     * @param int    $position  1,2,3,4 - which segment of the version to increment/decrement
528
     * @param int    $increment
529
     * @param string $pad       The string to pad version parts after $position
530
     *
531
     * @return string|null The new version
532
     *
533
     * @phpstan-param string[] $matches
534
     */
535
    private function manipulateVersionString(array $matches, $position, $increment = 0, $pad = '0')
536
    {
537
        for ($i = 4; $i > 0; --$i) {
538
            if ($i > $position) {
539
                $matches[$i] = $pad;
540
            } elseif ($i === $position && $increment) {
541
                $matches[$i] += $increment;
542
                // If $matches[$i] was 0, carry the decrement
543
                if ($matches[$i] < 0) {
544
                    $matches[$i] = $pad;
545
                    --$position;
546
547
                    // Return null on a carry overflow
548
                    if ($i === 1) {
549
                        return null;
550
                    }
551
                }
552
            }
553
        }
554
555
        return $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
556
    }
557
558
    /**
559
     * Expand shorthand stability string to long version.
560
     *
561
     * @param string $stability
562
     *
563
     * @return string
564
     */
565
    private function expandStability($stability)
566
    {
567
        $stability = strtolower($stability);
568
569
        switch ($stability) {
570
            case 'a':
571
                return 'alpha';
572
            case 'b':
573
                return 'beta';
574
            case 'p':
575
            case 'pl':
576
                return 'patch';
577
            case 'rc':
578
                return 'RC';
579
            default:
580
                return $stability;
581
        }
582
    }
583
}
584