Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(351)

Side by Side Diff: sdk/lib/_internal/pub/lib/src/version.dart

Issue 602253002: Use pub_semver package in pub. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 /// Handles version numbers, following the [Semantic Versioning][semver] spec.
6 ///
7 /// [semver]: http://semver.org/
8 library pub.version;
9
10 import 'dart:math';
11
12 import 'package:collection/equality.dart';
13
14 /// Regex that matches a version number at the beginning of a string.
15 final _START_VERSION = new RegExp(
16 r'^' // Start at beginning.
17 r'(\d+).(\d+).(\d+)' // Version number.
18 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release.
19 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build.
20
21 /// Like [_START_VERSION] but matches the entire string.
22 final _COMPLETE_VERSION = new RegExp("${_START_VERSION.pattern}\$");
23
24 /// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
25 /// a string.
26 final _START_COMPARISON = new RegExp(r"^[<>]=?");
27
28 /// The equality operator to use for comparing version components.
29 final _equality = const IterableEquality();
30
31 /// A parsed semantic version number.
32 class Version implements Comparable<Version>, VersionConstraint {
33 /// No released version: i.e. "0.0.0".
34 static Version get none => new Version(0, 0, 0);
35
36 /// Compares [a] and [b] to see which takes priority over the other.
37 ///
38 /// Returns `1` if [a] takes priority over [b] and `-1` if vice versa. If
39 /// [a] and [b] are equivalent, returns `0`.
40 ///
41 /// Unlike [compareTo], which *orders* versions, this determines which
42 /// version a user is likely to prefer. In particular, it prioritizes
43 /// pre-release versions lower than stable versions, regardless of their
44 /// version numbers.
45 ///
46 /// When used to sort a list, orders in ascending priority so that the
47 /// highest priority version is *last* in the result.
48 static int prioritize(Version a, Version b) {
49 // Sort all prerelease versions after all normal versions. This way
50 // the solver will prefer stable packages over unstable ones.
51 if (a.isPreRelease && !b.isPreRelease) return -1;
52 if (!a.isPreRelease && b.isPreRelease) return 1;
53
54 return a.compareTo(b);
55 }
56
57 /// Like [proiritize], but lower version numbers are considered greater than
58 /// higher version numbers.
59 ///
60 /// This still considers prerelease versions to be lower than non-prerelease
61 /// versions.
62 static int antiPrioritize(Version a, Version b) {
63 if (a.isPreRelease && !b.isPreRelease) return -1;
64 if (!a.isPreRelease && b.isPreRelease) return 1;
65
66 return b.compareTo(a);
67 }
68
69 /// The major version number: "1" in "1.2.3".
70 final int major;
71
72 /// The minor version number: "2" in "1.2.3".
73 final int minor;
74
75 /// The patch version number: "3" in "1.2.3".
76 final int patch;
77
78 /// The pre-release identifier: "foo" in "1.2.3-foo".
79 ///
80 /// This is split into a list of components, each of which may be either a
81 /// string or a non-negative integer. It may also be empty, indicating that
82 /// this version has no pre-release identifier.
83 final List preRelease;
84
85 /// The build identifier: "foo" in "1.2.3+foo".
86 ///
87 /// This is split into a list of components, each of which may be either a
88 /// string or a non-negative integer. It may also be empty, indicating that
89 /// this version has no build identifier.
90 final List build;
91
92 /// The original string representation of the version number.
93 ///
94 /// This preserves textual artifacts like leading zeros that may be left out
95 /// of the parsed version.
96 final String _text;
97
98 Version._(this.major, this.minor, this.patch, String preRelease, String build,
99 this._text)
100 : preRelease = preRelease == null ? [] : _splitParts(preRelease),
101 build = build == null ? [] : _splitParts(build) {
102 if (major < 0) throw new ArgumentError(
103 'Major version must be non-negative.');
104 if (minor < 0) throw new ArgumentError(
105 'Minor version must be non-negative.');
106 if (patch < 0) throw new ArgumentError(
107 'Patch version must be non-negative.');
108 }
109
110 /// Creates a new [Version] object.
111 factory Version(int major, int minor, int patch, {String pre, String build}) {
112 var text = "$major.$minor.$patch";
113 if (pre != null) text += "-$pre";
114 if (build != null) text += "+$build";
115
116 return new Version._(major, minor, patch, pre, build, text);
117 }
118
119 /// Creates a new [Version] by parsing [text].
120 factory Version.parse(String text) {
121 final match = _COMPLETE_VERSION.firstMatch(text);
122 if (match == null) {
123 throw new FormatException('Could not parse "$text".');
124 }
125
126 try {
127 int major = int.parse(match[1]);
128 int minor = int.parse(match[2]);
129 int patch = int.parse(match[3]);
130
131 String preRelease = match[5];
132 String build = match[8];
133
134 return new Version._(major, minor, patch, preRelease, build, text);
135 } on FormatException catch (ex) {
136 throw new FormatException('Could not parse "$text".');
137 }
138 }
139
140 /// Returns the primary version out of a list of candidates.
141 ///
142 /// This is the highest-numbered stable (non-prerelease) version. If there
143 /// are no stable versions, it's just the highest-numbered version.
144 static Version primary(List<Version> versions) {
145 var primary;
146 for (var version in versions) {
147 if (primary == null || (!version.isPreRelease && primary.isPreRelease) ||
148 (version.isPreRelease == primary.isPreRelease && version > primary)) {
149 primary = version;
150 }
151 }
152 return primary;
153 }
154
155 /// Splits a string of dot-delimited identifiers into their component parts.
156 ///
157 /// Identifiers that are numeric are converted to numbers.
158 static List _splitParts(String text) {
159 return text.split('.').map((part) {
160 try {
161 return int.parse(part);
162 } on FormatException catch (ex) {
163 // Not a number.
164 return part;
165 }
166 }).toList();
167 }
168
169 bool operator ==(other) {
170 if (other is! Version) return false;
171 return major == other.major && minor == other.minor &&
172 patch == other.patch &&
173 _equality.equals(preRelease, other.preRelease) &&
174 _equality.equals(build, other.build);
175 }
176
177 int get hashCode => major ^ minor ^ patch ^ _equality.hash(preRelease) ^
178 _equality.hash(build);
179
180 bool operator <(Version other) => compareTo(other) < 0;
181 bool operator >(Version other) => compareTo(other) > 0;
182 bool operator <=(Version other) => compareTo(other) <= 0;
183 bool operator >=(Version other) => compareTo(other) >= 0;
184
185 bool get isAny => false;
186 bool get isEmpty => false;
187
188 /// Whether or not this is a pre-release version.
189 bool get isPreRelease => preRelease.isNotEmpty;
190
191 /// Gets the next major version number that follows this one.
192 ///
193 /// If this version is a pre-release of a major version release (i.e. the
194 /// minor and patch versions are zero), then it just strips the pre-release
195 /// suffix. Otherwise, it increments the major version and resets the minor
196 /// and patch.
197 Version get nextMajor {
198 if (isPreRelease && minor == 0 && patch == 0) {
199 return new Version(major, minor, patch);
200 }
201
202 return new Version(major + 1, 0, 0);
203 }
204
205 /// Gets the next minor version number that follows this one.
206 ///
207 /// If this version is a pre-release of a minor version release (i.e. the
208 /// patch version is zero), then it just strips the pre-release suffix.
209 /// Otherwise, it increments the minor version and resets the patch.
210 Version get nextMinor {
211 if (isPreRelease && patch == 0) {
212 return new Version(major, minor, patch);
213 }
214
215 return new Version(major, minor + 1, 0);
216 }
217
218 /// Gets the next patch version number that follows this one.
219 ///
220 /// If this version is a pre-release, then it just strips the pre-release
221 /// suffix. Otherwise, it increments the patch version.
222 Version get nextPatch {
223 if (isPreRelease) {
224 return new Version(major, minor, patch);
225 }
226
227 return new Version(major, minor, patch + 1);
228 }
229
230 /// Tests if [other] matches this version exactly.
231 bool allows(Version other) => this == other;
232
233 VersionConstraint intersect(VersionConstraint other) {
234 if (other.isEmpty) return other;
235
236 // Intersect a version and a range.
237 if (other is VersionRange) return other.intersect(this);
238
239 // Intersecting two versions only works if they are the same.
240 if (other is Version) {
241 return this == other ? this : VersionConstraint.empty;
242 }
243
244 throw new ArgumentError(
245 'Unknown VersionConstraint type $other.');
246 }
247
248 int compareTo(Version other) {
249 if (major != other.major) return major.compareTo(other.major);
250 if (minor != other.minor) return minor.compareTo(other.minor);
251 if (patch != other.patch) return patch.compareTo(other.patch);
252
253 // Pre-releases always come before no pre-release string.
254 if (!isPreRelease && other.isPreRelease) return 1;
255 if (!other.isPreRelease && isPreRelease) return -1;
256
257 var comparison = _compareLists(preRelease, other.preRelease);
258 if (comparison != 0) return comparison;
259
260 // Builds always come after no build string.
261 if (build.isEmpty && other.build.isNotEmpty) return -1;
262 if (other.build.isEmpty && build.isNotEmpty) return 1;
263 return _compareLists(build, other.build);
264 }
265
266 String toString() => _text;
267
268 /// Compares a dot-separated component of two versions.
269 ///
270 /// This is used for the pre-release and build version parts. This follows
271 /// Rule 12 of the Semantic Versioning spec (v2.0.0-rc.1).
272 int _compareLists(List a, List b) {
273 for (var i = 0; i < max(a.length, b.length); i++) {
274 var aPart = (i < a.length) ? a[i] : null;
275 var bPart = (i < b.length) ? b[i] : null;
276
277 if (aPart == bPart) continue;
278
279 // Missing parts come before present ones.
280 if (aPart == null) return -1;
281 if (bPart == null) return 1;
282
283 if (aPart is num) {
284 if (bPart is num) {
285 // Compare two numbers.
286 return aPart.compareTo(bPart);
287 } else {
288 // Numbers come before strings.
289 return -1;
290 }
291 } else {
292 if (bPart is num) {
293 // Strings come after numbers.
294 return 1;
295 } else {
296 // Compare two strings.
297 return aPart.compareTo(bPart);
298 }
299 }
300 }
301
302 // The lists are entirely equal.
303 return 0;
304 }
305 }
306
307 /// A [VersionConstraint] is a predicate that can determine whether a given
308 /// version is valid or not.
309 ///
310 /// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or
311 /// greater. Version objects themselves implement this to match a specific
312 /// version.
313 abstract class VersionConstraint {
314 /// A [VersionConstraint] that allows all versions.
315 static VersionConstraint any = new VersionRange();
316
317 /// A [VersionConstraint] that allows no versions: i.e. the empty set.
318 static VersionConstraint empty = const _EmptyVersion();
319
320 /// Parses a version constraint.
321 ///
322 /// This string is either "any" or a series of version parts. Each part can
323 /// be one of:
324 ///
325 /// * A version string like `1.2.3`. In other words, anything that can be
326 /// parsed by [Version.parse()].
327 /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version
328 /// string.
329 ///
330 /// Whitespace is ignored.
331 ///
332 /// Examples:
333 ///
334 /// any
335 /// 1.2.3-alpha
336 /// <=5.1.4
337 /// >2.0.4 <= 2.4.6
338 factory VersionConstraint.parse(String text) {
339 // Handle the "any" constraint.
340 if (text.trim() == "any") return new VersionRange();
341
342 var originalText = text;
343 var constraints = <VersionConstraint>[];
344
345 void skipWhitespace() {
346 text = text.trim();
347 }
348
349 // Try to parse and consume a version number.
350 Version matchVersion() {
351 var version = _START_VERSION.firstMatch(text);
352 if (version == null) return null;
353
354 text = text.substring(version.end);
355 return new Version.parse(version[0]);
356 }
357
358 // Try to parse and consume a comparison operator followed by a version.
359 VersionConstraint matchComparison() {
360 var comparison = _START_COMPARISON.firstMatch(text);
361 if (comparison == null) return null;
362
363 var op = comparison[0];
364 text = text.substring(comparison.end);
365 skipWhitespace();
366
367 var version = matchVersion();
368 if (version == null) {
369 throw new FormatException('Expected version number after "$op" in '
370 '"$originalText", got "$text".');
371 }
372
373 switch (op) {
374 case '<=':
375 return new VersionRange(max: version, includeMax: true);
376 case '<':
377 return new VersionRange(max: version, includeMax: false);
378 case '>=':
379 return new VersionRange(min: version, includeMin: true);
380 case '>':
381 return new VersionRange(min: version, includeMin: false);
382 }
383 throw "Unreachable.";
384 }
385
386 while (true) {
387 skipWhitespace();
388 if (text.isEmpty) break;
389
390 var version = matchVersion();
391 if (version != null) {
392 constraints.add(version);
393 continue;
394 }
395
396 var comparison = matchComparison();
397 if (comparison != null) {
398 constraints.add(comparison);
399 continue;
400 }
401
402 // If we got here, we couldn't parse the remaining string.
403 throw new FormatException('Could not parse version "$originalText". '
404 'Unknown text at "$text".');
405 }
406
407 if (constraints.isEmpty) {
408 throw new FormatException('Cannot parse an empty string.');
409 }
410
411 return new VersionConstraint.intersection(constraints);
412 }
413
414 /// Creates a new version constraint that is the intersection of
415 /// [constraints].
416 ///
417 /// It only allows versions that all of those constraints allow. If
418 /// constraints is empty, then it returns a VersionConstraint that allows
419 /// all versions.
420 factory VersionConstraint.intersection(
421 Iterable<VersionConstraint> constraints) {
422 var constraint = new VersionRange();
423 for (var other in constraints) {
424 constraint = constraint.intersect(other);
425 }
426 return constraint;
427 }
428
429 /// Returns `true` if this constraint allows no versions.
430 bool get isEmpty;
431
432 /// Returns `true` if this constraint allows all versions.
433 bool get isAny;
434
435 /// Returns `true` if this constraint allows [version].
436 bool allows(Version version);
437
438 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by
439 /// both this and [other].
440 VersionConstraint intersect(VersionConstraint other);
441 }
442
443 /// Constrains versions to a fall within a given range.
444 ///
445 /// If there is a minimum, then this only allows versions that are at that
446 /// minimum or greater. If there is a maximum, then only versions less than
447 /// that are allowed. In other words, this allows `>= min, < max`.
448 class VersionRange implements VersionConstraint {
449 final Version min;
450 final Version max;
451 final bool includeMin;
452 final bool includeMax;
453
454 VersionRange({this.min, this.max,
455 this.includeMin: false, this.includeMax: false}) {
456 if (min != null && max != null && min > max) {
457 throw new ArgumentError(
458 'Minimum version ("$min") must be less than maximum ("$max").');
459 }
460 }
461
462 bool operator ==(other) {
463 if (other is! VersionRange) return false;
464
465 return min == other.min &&
466 max == other.max &&
467 includeMin == other.includeMin &&
468 includeMax == other.includeMax;
469 }
470
471 bool get isEmpty => false;
472
473 bool get isAny => min == null && max == null;
474
475 /// Tests if [other] matches falls within this version range.
476 bool allows(Version other) {
477 if (min != null) {
478 if (other < min) return false;
479 if (!includeMin && other == min) return false;
480 }
481
482 if (max != null) {
483 if (other > max) return false;
484 if (!includeMax && other == max) return false;
485
486 // If the max isn't itself a pre-release, don't allow any pre-release
487 // versions of the max.
488 //
489 // See: https://www.npmjs.org/doc/misc/semver.html
490 if (!includeMax &&
491 !max.isPreRelease && other.isPreRelease &&
492 other.major == max.major && other.minor == max.minor &&
493 other.patch == max.patch) {
494 return false;
495 }
496 }
497
498 return true;
499 }
500
501 VersionConstraint intersect(VersionConstraint other) {
502 if (other.isEmpty) return other;
503
504 // A range and a Version just yields the version if it's in the range.
505 if (other is Version) {
506 return allows(other) ? other : VersionConstraint.empty;
507 }
508
509 if (other is VersionRange) {
510 // Intersect the two ranges.
511 var intersectMin = min;
512 var intersectIncludeMin = includeMin;
513 var intersectMax = max;
514 var intersectIncludeMax = includeMax;
515
516 if (other.min == null) {
517 // Do nothing.
518 } else if (intersectMin == null || intersectMin < other.min) {
519 intersectMin = other.min;
520 intersectIncludeMin = other.includeMin;
521 } else if (intersectMin == other.min && !other.includeMin) {
522 // The edges are the same, but one is exclusive, make it exclusive.
523 intersectIncludeMin = false;
524 }
525
526 if (other.max == null) {
527 // Do nothing.
528 } else if (intersectMax == null || intersectMax > other.max) {
529 intersectMax = other.max;
530 intersectIncludeMax = other.includeMax;
531 } else if (intersectMax == other.max && !other.includeMax) {
532 // The edges are the same, but one is exclusive, make it exclusive.
533 intersectIncludeMax = false;
534 }
535
536 if (intersectMin == null && intersectMax == null) {
537 // Open range.
538 return new VersionRange();
539 }
540
541 // If the range is just a single version.
542 if (intersectMin == intersectMax) {
543 // If both ends are inclusive, allow that version.
544 if (intersectIncludeMin && intersectIncludeMax) return intersectMin;
545
546 // Otherwise, no versions.
547 return VersionConstraint.empty;
548 }
549
550 if (intersectMin != null && intersectMax != null &&
551 intersectMin > intersectMax) {
552 // Non-overlapping ranges, so empty.
553 return VersionConstraint.empty;
554 }
555
556 // If we got here, there is an actual range.
557 return new VersionRange(min: intersectMin, max: intersectMax,
558 includeMin: intersectIncludeMin, includeMax: intersectIncludeMax);
559 }
560
561 throw new ArgumentError(
562 'Unknown VersionConstraint type $other.');
563 }
564
565 String toString() {
566 var buffer = new StringBuffer();
567
568 if (min != null) {
569 buffer.write(includeMin ? '>=' : '>');
570 buffer.write(min);
571 }
572
573 if (max != null) {
574 if (min != null) buffer.write(' ');
575 buffer.write(includeMax ? '<=' : '<');
576 buffer.write(max);
577 }
578
579 if (min == null && max == null) buffer.write('any');
580 return buffer.toString();
581 }
582 }
583
584 class _EmptyVersion implements VersionConstraint {
585 const _EmptyVersion();
586
587 bool get isEmpty => true;
588 bool get isAny => false;
589 bool allows(Version other) => false;
590 VersionConstraint intersect(VersionConstraint other) => this;
591 String toString() => '<empty>';
592 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698