OLD | NEW |
---|---|
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * Handles version numbers, following the [Semantic Versioning][semver] spec. | 6 * Handles version numbers, following the [Semantic Versioning][semver] spec. |
7 * | 7 * |
8 * [semver]: http://semver.org/ | 8 * [semver]: http://semver.org/ |
9 */ | 9 */ |
10 #library('version'); | 10 #library('version'); |
11 | 11 |
12 #import('utils.dart'); | 12 #import('utils.dart'); |
13 | 13 |
14 /** A parsed semantic version number. */ | 14 /** A parsed semantic version number. */ |
15 class Version implements Comparable, Hashable, VersionConstraint { | 15 class Version implements Comparable, Hashable, VersionConstraint { |
16 static get none() => new Version(0, 0, 0); | 16 /** No released version: i.e. "0.0.0". */ |
17 static Version get none() => new Version(0, 0, 0); | |
17 | 18 |
18 static final _PARSE_REGEX = const RegExp( | 19 static final _PARSE_REGEX = const RegExp( |
19 @'^' // Start at beginning. | 20 @'^' // Start at beginning. |
20 @'(\d+).(\d+).(\d+)' // Version number. | 21 @'(\d+).(\d+).(\d+)' // Version number. |
21 @'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. | 22 @'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. |
22 @'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. | 23 @'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. |
23 @'$'); // Consume entire string. | 24 @'$'); // Consume entire string. |
24 | 25 |
25 /** The major version number: "1" in "1.2.3". */ | 26 /** The major version number: "1" in "1.2.3". */ |
26 final int major; | 27 final int major; |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
64 | 65 |
65 String preRelease = match[5]; | 66 String preRelease = match[5]; |
66 String build = match[8]; | 67 String build = match[8]; |
67 | 68 |
68 return new Version(major, minor, patch, preRelease, build); | 69 return new Version(major, minor, patch, preRelease, build); |
69 } catch (BadNumberFormatException ex) { | 70 } catch (BadNumberFormatException ex) { |
70 throw new FormatException('Could not parse "$text".'); | 71 throw new FormatException('Could not parse "$text".'); |
71 } | 72 } |
72 } | 73 } |
73 | 74 |
74 bool operator ==(Version other) { | 75 bool operator ==(other) { |
75 if (other is! Version) return false; | 76 if (other is! Version) return false; |
76 return compareTo(other) == 0; | 77 return compareTo(other) == 0; |
77 } | 78 } |
78 | 79 |
79 bool operator <(Version other) => compareTo(other) < 0; | 80 bool operator <(Version other) => compareTo(other) < 0; |
80 bool operator >(Version other) => compareTo(other) > 0; | 81 bool operator >(Version other) => compareTo(other) > 0; |
81 bool operator <=(Version other) => compareTo(other) <= 0; | 82 bool operator <=(Version other) => compareTo(other) <= 0; |
82 bool operator >=(Version other) => compareTo(other) >= 0; | 83 bool operator >=(Version other) => compareTo(other) >= 0; |
83 | 84 |
85 bool get isEmpty() => false; | |
86 | |
84 /** Tests if [other] matches this version exactly. */ | 87 /** Tests if [other] matches this version exactly. */ |
85 bool allows(Version other) => this == other; | 88 bool allows(Version other) => this == other; |
86 | 89 |
90 VersionConstraint intersect(VersionConstraint other) { | |
91 if (other.isEmpty) return other; | |
92 | |
93 // Intersect a version and a range. | |
94 if (other is VersionRange) return other.intersect(this); | |
95 | |
96 // Intersecting two versions only works if they are the same. | |
97 if (other is Version) return this == other ? this : _emptyVersion; | |
98 | |
99 throw new IllegalArgumentException( | |
100 'Unknown VersionConstraint type $other.'); | |
101 } | |
102 | |
87 int compareTo(Version other) { | 103 int compareTo(Version other) { |
88 if (major != other.major) return major.compareTo(other.major); | 104 if (major != other.major) return major.compareTo(other.major); |
89 if (minor != other.minor) return minor.compareTo(other.minor); | 105 if (minor != other.minor) return minor.compareTo(other.minor); |
90 if (patch != other.patch) return patch.compareTo(other.patch); | 106 if (patch != other.patch) return patch.compareTo(other.patch); |
91 | 107 |
92 if (preRelease != other.preRelease) { | 108 if (preRelease != other.preRelease) { |
93 // Pre-releases always come before no pre-release string. | 109 // Pre-releases always come before no pre-release string. |
94 if (preRelease == null) return 1; | 110 if (preRelease == null) return 1; |
95 if (other.preRelease == null) return -1; | 111 if (other.preRelease == null) return -1; |
96 | 112 |
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
172 }); | 188 }); |
173 } | 189 } |
174 } | 190 } |
175 | 191 |
176 /** | 192 /** |
177 * A [VersionConstraint] is a predicate that can determine whether a given | 193 * A [VersionConstraint] is a predicate that can determine whether a given |
178 * version is valid or not. For example, a ">= 2.0.0" constraint allows any | 194 * version is valid or not. For example, a ">= 2.0.0" constraint allows any |
179 * version that is "2.0.0" or greater. Version objects themselves implement | 195 * version that is "2.0.0" or greater. Version objects themselves implement |
180 * this to match a specific version. | 196 * this to match a specific version. |
181 */ | 197 */ |
182 interface VersionConstraint { | 198 interface VersionConstraint default _VersionConstraintFactory { |
199 /** | |
200 * A [VersionConstraint] that allows no versions: i.e. the empty set. | |
201 */ | |
202 VersionConstraint.empty(); | |
203 | |
204 /** | |
205 * Parses a version constraint. This string is a space-separated series of | |
206 * version parts. Each part can be one of: | |
207 * | |
208 * * A version string like `1.2.3`. In other words, anything that can be | |
209 * parsed by [Version.parse()]. | |
210 * * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version | |
211 * string. There cannot be a space between the operator and the version. | |
212 * | |
213 * Examples: | |
214 * | |
215 * 1.2.3-alpha | |
216 * <=5.1.4 | |
217 * >2.0.4 <=2.4.6 | |
218 */ | |
219 VersionConstraint.parse(String text); | |
220 | |
221 /** | |
222 * Creates a new version constraint that is the intersection of [constraints]. | |
223 * It will only allow versions that all of those constraints allow. If | |
224 * constraints is empty, then it returns a VersionConstraint that allows all | |
225 * versions. | |
226 */ | |
227 VersionConstraint.intersect(Collection<VersionConstraint> constraints); | |
228 | |
229 /** | |
230 * Returns `true` if this constraint allows no versions. | |
231 */ | |
232 bool get isEmpty(); | |
233 | |
234 /** | |
235 * Returns `true` if this constraint allows [version]. | |
236 */ | |
183 bool allows(Version version); | 237 bool allows(Version version); |
238 | |
239 /** | |
240 * Creates a new [VersionConstraint] that only allows [Version]s allowed by | |
241 * both this and [other]. | |
242 */ | |
243 VersionConstraint intersect(VersionConstraint other); | |
184 } | 244 } |
185 | 245 |
186 /** | 246 /** |
187 * Constrains versions to a fall within a given range. If there is a minimum, | 247 * Constrains versions to a fall within a given range. If there is a minimum, |
188 * then this only allows versions that are at that minimum or greater. If there | 248 * then this only allows versions that are at that minimum or greater. If there |
189 * is a maximum, then only versions less than that are allowed. In other words, | 249 * is a maximum, then only versions less than that are allowed. In other words, |
190 * this allows `>= min, < max`. | 250 * this allows `>= min, < max`. |
191 */ | 251 */ |
192 class VersionRange implements VersionConstraint { | 252 class VersionRange implements VersionConstraint { |
193 final Version min; | 253 final Version min; |
194 final Version max; | 254 final Version max; |
255 final bool includeMin; | |
256 final bool includeMax; | |
195 | 257 |
196 VersionRange([this.min, this.max]) { | 258 VersionRange([this.min, this.max, |
259 this.includeMin = false, this.includeMax = false]) { | |
197 if (min != null && max != null && min > max) { | 260 if (min != null && max != null && min > max) { |
198 throw new IllegalArgumentException( | 261 throw new IllegalArgumentException( |
199 'Maximum version ("$max") must be less than minimum ("$min").'); | 262 'Minimum version ("$min") must be less than maximum ("$max").'); |
200 } | 263 } |
201 } | 264 } |
202 | 265 |
266 bool operator ==(other) { | |
267 if (other is! VersionRange) return false; | |
268 | |
269 return min == other.min && | |
270 max == other.max && | |
271 includeMin == other.includeMin && | |
272 includeMax == other.includeMax; | |
273 } | |
274 | |
275 bool get isEmpty() => false; | |
276 | |
203 /** Tests if [other] matches falls within this version range. */ | 277 /** Tests if [other] matches falls within this version range. */ |
204 bool allows(Version other) { | 278 bool allows(Version other) { |
205 if (min != null && other < min) return false; | 279 if (min != null && other < min) return false; |
206 if (max != null && other >= max) return false; | 280 if (min != null && !includeMin && other == min) return false; |
281 if (max != null && other > max) return false; | |
282 if (max != null && !includeMax && other == max) return false; | |
207 return true; | 283 return true; |
208 } | 284 } |
285 | |
286 VersionConstraint intersect(VersionConstraint other) { | |
287 if (other.isEmpty) return other; | |
288 | |
289 // A range and a Version just yields the version if it's in the range. | |
290 if (other is Version) return allows(other) ? other : _emptyVersion; | |
291 | |
292 if (other is VersionRange) { | |
293 // Intersect the two ranges. | |
294 var intersectMin = min; | |
295 var intersectIncludeMin = includeMin; | |
296 var intersectMax = max; | |
297 var intersectIncludeMax = includeMax; | |
298 | |
299 if (other.min == null) { | |
300 // Do nothing. | |
301 } else if (intersectMin == null || intersectMin < other.min) { | |
302 intersectMin = other.min; | |
303 intersectIncludeMin = other.includeMin; | |
304 } else if (intersectMin == other.min && !other.includeMin) { | |
305 // The edges are the same, but one is exclusive, make it exclusive. | |
306 intersectIncludeMin = false; | |
307 } | |
308 | |
309 if (other.max == null) { | |
310 // Do nothing. | |
311 } else if (intersectMax == null || intersectMax > other.max) { | |
312 intersectMax = other.max; | |
313 intersectIncludeMax = other.includeMax; | |
314 } else if (intersectMax == other.max && !other.includeMax) { | |
315 // The edges are the same, but one is exclusive, make it exclusive. | |
316 intersectIncludeMax = false; | |
317 } | |
318 | |
319 if (intersectMin == null && intersectMax == null) { | |
320 // Open range. | |
321 return new VersionRange(); | |
322 } | |
323 | |
324 // If the range is just a single version. | |
nweiz
2012/06/18 18:29:19
There are other cases where it could just be a sin
Bob Nystrom
2012/06/20 01:40:04
That can still have other versions, like: 0.0.1+ho
nweiz
2012/06/20 21:08:48
Good point.
| |
325 if (intersectMin == intersectMax) { | |
326 // If both ends are inclusive, allow that version. | |
327 if (intersectIncludeMin && intersectIncludeMax) return intersectMin; | |
328 | |
329 // Otherwise, no versions. | |
330 return _emptyVersion; | |
331 } | |
332 | |
333 if (intersectMin != null && intersectMax != null && | |
334 intersectMin > intersectMax) { | |
335 // Non-overlapping ranges, so empty. | |
336 return _emptyVersion; | |
337 } | |
338 | |
339 // If we got here, there is an actual range. | |
340 return new VersionRange(intersectMin, intersectMax, | |
341 intersectIncludeMin, intersectIncludeMax); | |
342 } | |
343 | |
344 throw new IllegalArgumentException( | |
345 'Unknown VersionConstraint type $other.'); | |
346 } | |
347 | |
348 String toString() { | |
349 var buffer = new StringBuffer(); | |
350 | |
351 if (min != null) { | |
352 buffer.add(includeMin ? '>=' : '>'); | |
353 buffer.add(min); | |
354 } | |
355 | |
356 if (max != null) { | |
357 if (min != null) buffer.add(' '); | |
358 buffer.add(includeMax ? '<=' : '<'); | |
359 buffer.add(max); | |
360 } | |
361 | |
362 if (min == null && max == null) buffer.add('any'); | |
nweiz
2012/06/18 18:29:19
We should only print this if we parse "any" as all
Bob Nystrom
2012/06/20 01:40:04
Done. Added parse support for it. That should make
| |
363 return buffer.toString(); | |
364 } | |
209 } | 365 } |
366 | |
367 class _EmptyVersion implements VersionConstraint { | |
368 const _EmptyVersion(); | |
369 | |
370 bool get isEmpty() => true; | |
371 bool allows(Version other) => false; | |
372 VersionConstraint intersect(VersionConstraint other) => this; | |
373 String toString() => '<empty>'; | |
374 } | |
375 | |
376 final _emptyVersion = const _EmptyVersion(); | |
nweiz
2012/06/18 18:29:19
Why is this necessary? Can't const automatically c
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
377 | |
378 class _VersionConstraintFactory { | |
379 factory VersionConstraint.empty() => _emptyVersion; | |
380 | |
381 factory VersionConstraint.parse(String text) { | |
382 if (text.trim() == '') { | |
383 throw new FormatException('Cannot parse an empty string.'); | |
384 } | |
385 | |
386 // Split it into space-separated parts. | |
387 var constraints = <VersionConstraint>[]; | |
388 for (var part in text.split(' ')) { | |
389 constraints.add(parseSingleConstraint(part)); | |
390 } | |
391 | |
392 return new VersionConstraint.intersect(constraints); | |
393 } | |
394 | |
395 factory VersionConstraint.intersect(Collection<VersionConstraint> constraints) { | |
nweiz
2012/06/18 18:29:19
Line length.
Bob Nystrom
2012/06/20 01:40:04
Done.
| |
396 var constraint = new VersionRange(); | |
397 for (var other in constraints) { | |
398 constraint = constraint.intersect(other); | |
399 } | |
400 return constraint; | |
401 } | |
402 | |
403 static VersionConstraint parseSingleConstraint(String text) { | |
404 // TODO(rnystrom): Consider other syntaxes for version constraints. This | |
405 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is | |
nweiz
2012/06/18 18:29:19
I'm coming around on quoting constraints in genera
Bob Nystrom
2012/06/20 01:40:04
Agreed, this is too finicky, but is it OK to be si
nweiz
2012/06/20 21:08:48
Yes, definitely.
| |
406 // unfortunately meaningful in YAML, requiring it to be quoted in a | |
407 // pubspec. | |
408 if (text.startsWith('<=')) { | |
nweiz
2012/06/18 18:29:19
I think you could save some repetition here by par
Bob Nystrom
2012/06/20 01:40:04
Done. Good call.
| |
409 // A maximum version (inclusive). | |
410 var version = new Version.parse(text.substring('<='.length)); | |
411 return new VersionRange(max: version, includeMax: true); | |
412 } | |
413 | |
414 if (text.startsWith('<')) { | |
415 // A maximum version (exclusive). | |
416 var version = new Version.parse(text.substring('<'.length)); | |
417 return new VersionRange(max: version, includeMax: false); | |
418 } | |
419 | |
420 if (text.startsWith('>=')) { | |
421 // A minimum version (inclusive). | |
422 var version = new Version.parse(text.substring('>='.length)); | |
423 return new VersionRange(min: version, includeMin: true); | |
424 } | |
425 | |
426 if (text.startsWith('>')) { | |
427 // A minimum version (exclusive). | |
428 var version = new Version.parse(text.substring('>'.length)); | |
429 return new VersionRange(min: version, includeMin: false); | |
430 } | |
431 | |
432 // Otherwise, it must be an explicit version. | |
433 return new Version.parse(text); | |
434 } | |
435 } | |
OLD | NEW |