Merge branch 'caret_operator'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf0e1f9..5a89ebd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+# 1.1.0
+
+* Add support for the `^` operator for compatible versions according to pub's
+ notion of compatibility. `^1.2.3` is equivalent to `>=1.2.3 <2.0.0`; `^0.1.2`
+ is equivalent to `>=0.1.2 <0.2.0`.
+
+* Add `Version.nextBreaking`, which returns the next version that introduces
+ breaking changes after a given version.
+
+* Add `new VersionConstraint.compatibleWith()`, which returns a range covering
+ all versions compatible with a given version.
+
# 1.0.0
* Initial release.
diff --git a/README.md b/README.md
index 1cb1527..3f9558f 100644
--- a/README.md
+++ b/README.md
@@ -73,5 +73,24 @@
specifically selects that unstable version -- they've deliberately opted
into it.
+ * **There is a notion of compatibility between pre-1.0.0 versions.** Semver
+ deems all pre-1.0.0 versions to be incompatible. This means that the only
+ way to ensure compatibility when depending on a pre-1.0.0 package is to
+ pin the dependency to an exact version. Pinned version constraints prevent
+ automatic patch and pre-release updates. To avoid this situation, pub
+ defines the "next breaking" version as the version which increments the
+ major version if it's greater than zero, and the minor version otherwise,
+ resets subsequent digits to zero, and strips any pre-release or build
+ suffix. For example, here are some versions along with their next breaking
+ ones:
+
+ `0.0.3` -> `0.1.0`
+ `0.7.2-alpha` -> `0.8.0`
+ `1.2.3` -> `2.0.0`
+
+ To make use of this, pub defines a "^" operator which yields a version
+ constraint greater than or equal to a given version, but less than its next
+ breaking one.
+
[pub]: http://pub.dartlang.org/
[semver]: http://semver.org/
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
index 4a22618..8b32900 100644
--- a/lib/src/patterns.dart
+++ b/lib/src/patterns.dart
@@ -17,3 +17,6 @@
/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
/// a string.
final START_COMPARISON = new RegExp(r"^[<>]=?");
+
+/// The "compatible with" operator.
+const COMPATIBLE_WITH = "^";
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 866d89c..0eb96c7 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -189,7 +189,7 @@
return new Version(major, minor, patch);
}
- return new Version(major + 1, 0, 0);
+ return _incrementMajor();
}
/// Gets the next minor version number that follows this one.
@@ -202,7 +202,7 @@
return new Version(major, minor, patch);
}
- return new Version(major, minor + 1, 0);
+ return _incrementMinor();
}
/// Gets the next patch version number that follows this one.
@@ -214,9 +214,26 @@
return new Version(major, minor, patch);
}
- return new Version(major, minor, patch + 1);
+ return _incrementPatch();
}
+ /// Gets the next breaking version number that follows this one.
+ ///
+ /// Increments [major] if it's greater than zero, otherwise [minor], resets
+ /// subsequent digits to zero, and strips any [preRelease] or [build]
+ /// suffix.
+ Version get nextBreaking {
+ if (major == 0) {
+ return _incrementMinor();
+ }
+
+ return _incrementMajor();
+ }
+
+ Version _incrementMajor() => new Version(major + 1, 0, 0);
+ Version _incrementMinor() => new Version(major, minor + 1, 0);
+ Version _incrementPatch() => new Version(major, minor, patch + 1);
+
/// Tests if [other] matches this version exactly.
bool allows(Version other) => this == other;
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
index 499892e..cb1c64c 100644
--- a/lib/src/version_constraint.dart
+++ b/lib/src/version_constraint.dart
@@ -23,33 +23,39 @@
/// Parses a version constraint.
///
- /// This string is either "any" or a series of version parts. Each part can
- /// be one of:
+ /// This string is one of:
///
- /// * A version string like `1.2.3`. In other words, anything that can be
- /// parsed by [Version.parse()].
- /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version
- /// string.
+ /// * "any". [any] version.
+ /// * "^" followed by a version string. Versions compatible with
+ /// ([VersionConstraint.compatibleWith]) the version.
+ /// * a series of version parts. Each part can be one of:
+ /// * A version string like `1.2.3`. In other words, anything that can be
+ /// parsed by [Version.parse()].
+ /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a
+ /// version string.
///
/// Whitespace is ignored.
///
/// Examples:
///
/// any
+ /// ^0.7.2
+ /// ^1.0.0-alpha
/// 1.2.3-alpha
/// <=5.1.4
/// >2.0.4 <= 2.4.6
factory VersionConstraint.parse(String text) {
- // Handle the "any" constraint.
- if (text.trim() == "any") return new VersionRange();
-
var originalText = text;
- var constraints = [];
skipWhitespace() {
text = text.trim();
}
+ skipWhitespace();
+
+ // Handle the "any" constraint.
+ if (text == "any") return any;
+
// Try to parse and consume a version number.
matchVersion() {
var version = START_VERSION.firstMatch(text);
@@ -83,8 +89,35 @@
throw "Unreachable.";
}
+ // Try to parse the "^" operator followed by a version.
+ matchCompatibleWith() {
+ if (!text.startsWith(COMPATIBLE_WITH)) return null;
+
+ text = text.substring(COMPATIBLE_WITH.length);
+ skipWhitespace();
+
+ var version = matchVersion();
+ if (version == null) {
+ throw new FormatException('Expected version number after '
+ '"$COMPATIBLE_WITH" in "$originalText", got "$text".');
+ }
+
+ if (text.isNotEmpty) {
+ throw new FormatException('Cannot include other constraints with '
+ '"$COMPATIBLE_WITH" constraint in "$originalText".');
+ }
+
+ return new VersionConstraint.compatibleWith(version);
+ }
+
+ var compatibleWith = matchCompatibleWith();
+ if (compatibleWith != null) return compatibleWith;
+
+ var constraints = [];
+
while (true) {
skipWhitespace();
+
if (text.isEmpty) break;
var version = matchVersion();
@@ -111,6 +144,15 @@
return new VersionConstraint.intersection(constraints);
}
+ /// Creates a version constraint which allows all versions that are
+ /// backward compatible with [version].
+ ///
+ /// Versions are considered backward compatible with [version] if they
+ /// are greater than or equal to [version], but less than the next breaking
+ /// version ([Version.nextBreaking]) of [version].
+ factory VersionConstraint.compatibleWith(Version version) =>
+ new _CompatibleWithVersionRange(version);
+
/// Creates a new version constraint that is the intersection of
/// [constraints].
///
@@ -149,3 +191,11 @@
VersionConstraint intersect(VersionConstraint other) => this;
String toString() => '<empty>';
}
+
+class _CompatibleWithVersionRange extends VersionRange {
+ _CompatibleWithVersionRange(Version version) : super(
+ min: version, includeMin: true,
+ max: version.nextBreaking, includeMax: false);
+
+ String toString() => '^$min';
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 63b09e1..b422064 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: pub_semver
-version: 1.0.0
+version: 1.1.0-dev
author: Dart Team <misc@dartlang.org>
description: >
Versions and version constraints implementing pub's versioning policy. This
diff --git a/test/utils.dart b/test/utils.dart
index da89683..d918422 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -9,6 +9,10 @@
import 'package:pub_semver/pub_semver.dart';
/// Some stock example versions to use in tests.
+final v003 = new Version.parse('0.0.3');
+final v010 = new Version.parse('0.1.0');
+final v072 = new Version.parse('0.7.2');
+final v080 = new Version.parse('0.8.0');
final v114 = new Version.parse('1.1.4');
final v123 = new Version.parse('1.2.3');
final v124 = new Version.parse('1.2.4');
diff --git a/test/version_constraint_test.dart b/test/version_constraint_test.dart
index df8c099..ee10a89 100644
--- a/test/version_constraint_test.dart
+++ b/test/version_constraint_test.dart
@@ -106,7 +106,7 @@
new Version.parse('3.4.5')));
});
- test('ignores whitespace around operators', () {
+ test('ignores whitespace around comparison operators', () {
var constraint = new VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0');
expect(constraint, allows(
@@ -123,11 +123,41 @@
throwsFormatException);
});
+ test('parses a "^" version', () {
+ expect(new VersionConstraint.parse('^0.0.3'), equals(
+ new VersionConstraint.compatibleWith(v003)));
+
+ expect(new VersionConstraint.parse('^0.7.2'), equals(
+ new VersionConstraint.compatibleWith(v072)));
+
+ expect(new VersionConstraint.parse('^1.2.3'), equals(
+ new VersionConstraint.compatibleWith(v123)));
+
+ var min = new Version.parse('0.7.2-pre+1');
+ expect(new VersionConstraint.parse('^0.7.2-pre+1'), equals(
+ new VersionConstraint.compatibleWith(min)));
+ });
+
+ test('does not allow "^" to be mixed with other constraints', () {
+ expect(() => new VersionConstraint.parse('>=1.2.3 ^1.0.0'),
+ throwsFormatException);
+ expect(() => new VersionConstraint.parse('^1.0.0 <1.2.3'),
+ throwsFormatException);
+ });
+
+ test('ignores whitespace around "^"', () {
+ var constraint = new VersionConstraint.parse(' ^ 1.2.3 ');
+
+ expect(constraint, equals(
+ new VersionConstraint.compatibleWith(v123)));
+ });
+
test('throws FormatException on a bad string', () {
var bad = [
"", " ", // Empty string.
"foo", // Bad text.
">foo", // Bad text after operator.
+ "^foo", // Bad text after "^".
"1.0.0 foo", "1.0.0foo", // Bad text after version.
"anything", // Bad text after "any".
"<>1.0.0", // Multiple operators.
@@ -140,4 +170,19 @@
}
});
});
+
+ group('compatibleWith()', () {
+ test('returns the range of compatible versions', () {
+ var constraint = new VersionConstraint.compatibleWith(v072);
+
+ expect(constraint, equals(new VersionRange(min: v072, includeMin: true,
+ max: v072.nextBreaking)));
+ });
+
+ test('toString() uses "^"', () {
+ var constraint = new VersionConstraint.compatibleWith(v072);
+
+ expect(constraint.toString(), equals('^0.7.2'));
+ });
+ });
}
diff --git a/test/version_test.dart b/test/version_test.dart
index b271ecc..3c652ac 100644
--- a/test/version_test.dart
+++ b/test/version_test.dart
@@ -198,6 +198,18 @@
expect(new Version.parse('1.2.3+patch').nextPatch, equals(v124));
});
+ test('nextBreaking', () {
+ expect(v123.nextBreaking, equals(v200));
+ expect(v072.nextBreaking, equals(v080));
+ expect(v003.nextBreaking, equals(v010));
+
+ // Removes pre-release version if present.
+ expect(new Version.parse('1.2.3-dev').nextBreaking, equals(v200));
+
+ // Strips build suffix.
+ expect(new Version.parse('1.2.3+patch').nextBreaking, equals(v200));
+ });
+
test('parse()', () {
expect(new Version.parse('0.0.0'), equals(new Version(0, 0, 0)));
expect(new Version.parse('12.34.56'), equals(new Version(12, 34, 56)));