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) { |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|