| 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 /// Handles version numbers, following the [Semantic Versioning][semver] spec. |
| 6 * Handles version numbers, following the [Semantic Versioning][semver] spec. | 6 /// |
| 7 * | 7 /// [semver]: http://semver.org/ |
| 8 * [semver]: http://semver.org/ | |
| 9 */ | |
| 10 library version; | 8 library version; |
| 11 | 9 |
| 12 import 'dart:math'; | 10 import 'dart:math'; |
| 13 | 11 |
| 14 import 'utils.dart'; | 12 import 'utils.dart'; |
| 15 | 13 |
| 16 /** A parsed semantic version number. */ | 14 /// A parsed semantic version number. |
| 17 class Version implements Comparable, VersionConstraint { | 15 class Version implements Comparable, VersionConstraint { |
| 18 /** No released version: i.e. "0.0.0". */ | 16 /// No released version: i.e. "0.0.0". |
| 19 static Version get none => new Version(0, 0, 0); | 17 static Version get none => new Version(0, 0, 0); |
| 20 | 18 |
| 21 static final _PARSE_REGEX = new RegExp( | 19 static final _PARSE_REGEX = new RegExp( |
| 22 r'^' // Start at beginning. | 20 r'^' // Start at beginning. |
| 23 r'(\d+).(\d+).(\d+)' // Version number. | 21 r'(\d+).(\d+).(\d+)' // Version number. |
| 24 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. | 22 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. |
| 25 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. | 23 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. |
| 26 r'$'); // Consume entire string. | 24 r'$'); // Consume entire string. |
| 27 | 25 |
| 28 /** The major version number: "1" in "1.2.3". */ | 26 /// The major version number: "1" in "1.2.3". |
| 29 final int major; | 27 final int major; |
| 30 | 28 |
| 31 /** The minor version number: "2" in "1.2.3". */ | 29 /// The minor version number: "2" in "1.2.3". |
| 32 final int minor; | 30 final int minor; |
| 33 | 31 |
| 34 /** The patch version number: "3" in "1.2.3". */ | 32 /// The patch version number: "3" in "1.2.3". |
| 35 final int patch; | 33 final int patch; |
| 36 | 34 |
| 37 /** The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. */ | 35 /// The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. |
| 38 final String preRelease; | 36 final String preRelease; |
| 39 | 37 |
| 40 /** The build identifier: "foo" in "1.2.3+foo". May be `null`. */ | 38 /// The build identifier: "foo" in "1.2.3+foo". May be `null`. |
| 41 final String build; | 39 final String build; |
| 42 | 40 |
| 43 /** Creates a new [Version] object. */ | 41 /// Creates a new [Version] object. |
| 44 Version(this.major, this.minor, this.patch, {String pre, this.build}) | 42 Version(this.major, this.minor, this.patch, {String pre, this.build}) |
| 45 : preRelease = pre { | 43 : preRelease = pre { |
| 46 if (major < 0) throw new ArgumentError( | 44 if (major < 0) throw new ArgumentError( |
| 47 'Major version must be non-negative.'); | 45 'Major version must be non-negative.'); |
| 48 if (minor < 0) throw new ArgumentError( | 46 if (minor < 0) throw new ArgumentError( |
| 49 'Minor version must be non-negative.'); | 47 'Minor version must be non-negative.'); |
| 50 if (patch < 0) throw new ArgumentError( | 48 if (patch < 0) throw new ArgumentError( |
| 51 'Patch version must be non-negative.'); | 49 'Patch version must be non-negative.'); |
| 52 } | 50 } |
| 53 | 51 |
| 54 /** | 52 /// Creates a new [Version] by parsing [text]. |
| 55 * Creates a new [Version] by parsing [text]. | |
| 56 */ | |
| 57 factory Version.parse(String text) { | 53 factory Version.parse(String text) { |
| 58 final match = _PARSE_REGEX.firstMatch(text); | 54 final match = _PARSE_REGEX.firstMatch(text); |
| 59 if (match == null) { | 55 if (match == null) { |
| 60 throw new FormatException('Could not parse "$text".'); | 56 throw new FormatException('Could not parse "$text".'); |
| 61 } | 57 } |
| 62 | 58 |
| 63 try { | 59 try { |
| 64 int major = parseInt(match[1]); | 60 int major = parseInt(match[1]); |
| 65 int minor = parseInt(match[2]); | 61 int minor = parseInt(match[2]); |
| 66 int patch = parseInt(match[3]); | 62 int patch = parseInt(match[3]); |
| (...skipping 12 matching lines...) Expand all Loading... |
| 79 return compareTo(other) == 0; | 75 return compareTo(other) == 0; |
| 80 } | 76 } |
| 81 | 77 |
| 82 bool operator <(Version other) => compareTo(other) < 0; | 78 bool operator <(Version other) => compareTo(other) < 0; |
| 83 bool operator >(Version other) => compareTo(other) > 0; | 79 bool operator >(Version other) => compareTo(other) > 0; |
| 84 bool operator <=(Version other) => compareTo(other) <= 0; | 80 bool operator <=(Version other) => compareTo(other) <= 0; |
| 85 bool operator >=(Version other) => compareTo(other) >= 0; | 81 bool operator >=(Version other) => compareTo(other) >= 0; |
| 86 | 82 |
| 87 bool get isEmpty => false; | 83 bool get isEmpty => false; |
| 88 | 84 |
| 89 /** Tests if [other] matches this version exactly. */ | 85 /// Tests if [other] matches this version exactly. |
| 90 bool allows(Version other) => this == other; | 86 bool allows(Version other) => this == other; |
| 91 | 87 |
| 92 VersionConstraint intersect(VersionConstraint other) { | 88 VersionConstraint intersect(VersionConstraint other) { |
| 93 if (other.isEmpty) return other; | 89 if (other.isEmpty) return other; |
| 94 | 90 |
| 95 // Intersect a version and a range. | 91 // Intersect a version and a range. |
| 96 if (other is VersionRange) return other.intersect(this); | 92 if (other is VersionRange) return other.intersect(this); |
| 97 | 93 |
| 98 // Intersecting two versions only works if they are the same. | 94 // Intersecting two versions only works if they are the same. |
| 99 if (other is Version) return this == other ? this : const _EmptyVersion(); | 95 if (other is Version) return this == other ? this : const _EmptyVersion(); |
| (...skipping 29 matching lines...) Expand all Loading... |
| 129 int get hashCode => toString().hashCode; | 125 int get hashCode => toString().hashCode; |
| 130 | 126 |
| 131 String toString() { | 127 String toString() { |
| 132 var buffer = new StringBuffer(); | 128 var buffer = new StringBuffer(); |
| 133 buffer.add('$major.$minor.$patch'); | 129 buffer.add('$major.$minor.$patch'); |
| 134 if (preRelease != null) buffer.add('-$preRelease'); | 130 if (preRelease != null) buffer.add('-$preRelease'); |
| 135 if (build != null) buffer.add('+$build'); | 131 if (build != null) buffer.add('+$build'); |
| 136 return buffer.toString(); | 132 return buffer.toString(); |
| 137 } | 133 } |
| 138 | 134 |
| 139 /** | 135 /// Compares the string part of two versions. This is used for the pre-release |
| 140 * Compares the string part of two versions. This is used for the pre-release | 136 /// and build version parts. This follows Rule 12. of the Semantic Versioning |
| 141 * and build version parts. This follows Rule 12. of the Semantic Versioning | 137 /// spec. |
| 142 * spec. | |
| 143 */ | |
| 144 int _compareStrings(String a, String b) { | 138 int _compareStrings(String a, String b) { |
| 145 var aParts = _splitParts(a); | 139 var aParts = _splitParts(a); |
| 146 var bParts = _splitParts(b); | 140 var bParts = _splitParts(b); |
| 147 | 141 |
| 148 for (int i = 0; i < max(aParts.length, bParts.length); i++) { | 142 for (int i = 0; i < max(aParts.length, bParts.length); i++) { |
| 149 var aPart = (i < aParts.length) ? aParts[i] : null; | 143 var aPart = (i < aParts.length) ? aParts[i] : null; |
| 150 var bPart = (i < bParts.length) ? bParts[i] : null; | 144 var bPart = (i < bParts.length) ? bParts[i] : null; |
| 151 | 145 |
| 152 if (aPart != bPart) { | 146 if (aPart != bPart) { |
| 153 // Missing parts come before present ones. | 147 // Missing parts come before present ones. |
| (...skipping 14 matching lines...) Expand all Loading... |
| 168 return 1; | 162 return 1; |
| 169 } else { | 163 } else { |
| 170 // Compare two strings. | 164 // Compare two strings. |
| 171 return aPart.compareTo(bPart); | 165 return aPart.compareTo(bPart); |
| 172 } | 166 } |
| 173 } | 167 } |
| 174 } | 168 } |
| 175 } | 169 } |
| 176 } | 170 } |
| 177 | 171 |
| 178 /** | 172 /// Splits a string of dot-delimited identifiers into their component parts. |
| 179 * Splits a string of dot-delimited identifiers into their component parts. | 173 /// Identifiers that are numeric are converted to numbers. |
| 180 * Identifiers that are numeric are converted to numbers. | |
| 181 */ | |
| 182 List _splitParts(String text) { | 174 List _splitParts(String text) { |
| 183 return text.split('.').map((part) { | 175 return text.split('.').map((part) { |
| 184 try { | 176 try { |
| 185 return parseInt(part); | 177 return parseInt(part); |
| 186 } on FormatException catch (ex) { | 178 } on FormatException catch (ex) { |
| 187 // Not a number. | 179 // Not a number. |
| 188 return part; | 180 return part; |
| 189 } | 181 } |
| 190 }); | 182 }); |
| 191 } | 183 } |
| 192 } | 184 } |
| 193 | 185 |
| 194 /** | 186 /// A [VersionConstraint] is a predicate that can determine whether a given |
| 195 * A [VersionConstraint] is a predicate that can determine whether a given | 187 /// version is valid or not. For example, a ">= 2.0.0" constraint allows any |
| 196 * version is valid or not. For example, a ">= 2.0.0" constraint allows any | 188 /// version that is "2.0.0" or greater. Version objects themselves implement |
| 197 * version that is "2.0.0" or greater. Version objects themselves implement | 189 /// this to match a specific version. |
| 198 * this to match a specific version. | |
| 199 */ | |
| 200 abstract class VersionConstraint { | 190 abstract class VersionConstraint { |
| 201 /** | 191 /// A [VersionConstraint] that allows no versions: i.e. the empty set. |
| 202 * A [VersionConstraint] that allows no versions: i.e. the empty set. | |
| 203 */ | |
| 204 factory VersionConstraint.empty() => const _EmptyVersion(); | 192 factory VersionConstraint.empty() => const _EmptyVersion(); |
| 205 | 193 |
| 206 /** | 194 /// Parses a version constraint. This string is a space-separated series of |
| 207 * Parses a version constraint. This string is a space-separated series of | 195 /// version parts. Each part can be one of: |
| 208 * version parts. Each part can be one of: | 196 /// |
| 209 * | 197 /// * A version string like `1.2.3`. In other words, anything that can be |
| 210 * * A version string like `1.2.3`. In other words, anything that can be | 198 /// parsed by [Version.parse()]. |
| 211 * parsed by [Version.parse()]. | 199 /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version |
| 212 * * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version | 200 /// string. There cannot be a space between the operator and the version. |
| 213 * string. There cannot be a space between the operator and the version. | 201 /// |
| 214 * | 202 /// Examples: |
| 215 * Examples: | 203 /// |
| 216 * | 204 /// 1.2.3-alpha |
| 217 * 1.2.3-alpha | 205 /// <=5.1.4 |
| 218 * <=5.1.4 | 206 /// >2.0.4 <=2.4.6 |
| 219 * >2.0.4 <=2.4.6 | |
| 220 */ | |
| 221 factory VersionConstraint.parse(String text) { | 207 factory VersionConstraint.parse(String text) { |
| 222 if (text.trim() == '') { | 208 if (text.trim() == '') { |
| 223 throw new FormatException('Cannot parse an empty string.'); | 209 throw new FormatException('Cannot parse an empty string.'); |
| 224 } | 210 } |
| 225 | 211 |
| 226 // Split it into space-separated parts. | 212 // Split it into space-separated parts. |
| 227 var constraints = <VersionConstraint>[]; | 213 var constraints = <VersionConstraint>[]; |
| 228 for (var part in text.split(' ')) { | 214 for (var part in text.split(' ')) { |
| 229 constraints.add(_parseSingleConstraint(part)); | 215 constraints.add(_parseSingleConstraint(part)); |
| 230 } | 216 } |
| 231 | 217 |
| 232 return new VersionConstraint.intersection(constraints); | 218 return new VersionConstraint.intersection(constraints); |
| 233 } | 219 } |
| 234 | 220 |
| 235 /** | 221 /// Creates a new version constraint that is the intersection of |
| 236 * Creates a new version constraint that is the intersection of [constraints]. | 222 /// [constraints]. It will only allow versions that all of those constraints |
| 237 * It will only allow versions that all of those constraints allow. If | 223 /// allow. If constraints is empty, then it returns a VersionConstraint that |
| 238 * constraints is empty, then it returns a VersionConstraint that allows all | 224 /// allows all versions. |
| 239 * versions. | |
| 240 */ | |
| 241 factory VersionConstraint.intersection( | 225 factory VersionConstraint.intersection( |
| 242 Collection<VersionConstraint> constraints) { | 226 Collection<VersionConstraint> constraints) { |
| 243 var constraint = new VersionRange(); | 227 var constraint = new VersionRange(); |
| 244 for (var other in constraints) { | 228 for (var other in constraints) { |
| 245 constraint = constraint.intersect(other); | 229 constraint = constraint.intersect(other); |
| 246 } | 230 } |
| 247 return constraint; | 231 return constraint; |
| 248 } | 232 } |
| 249 | 233 |
| 250 /** | 234 /// Returns `true` if this constraint allows no versions. |
| 251 * Returns `true` if this constraint allows no versions. | |
| 252 */ | |
| 253 bool get isEmpty; | 235 bool get isEmpty; |
| 254 | 236 |
| 255 /** | 237 /// Returns `true` if this constraint allows [version]. |
| 256 * Returns `true` if this constraint allows [version]. | |
| 257 */ | |
| 258 bool allows(Version version); | 238 bool allows(Version version); |
| 259 | 239 |
| 260 /** | 240 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by |
| 261 * Creates a new [VersionConstraint] that only allows [Version]s allowed by | 241 /// both this and [other]. |
| 262 * both this and [other]. | |
| 263 */ | |
| 264 VersionConstraint intersect(VersionConstraint other); | 242 VersionConstraint intersect(VersionConstraint other); |
| 265 | 243 |
| 266 static VersionConstraint _parseSingleConstraint(String text) { | 244 static VersionConstraint _parseSingleConstraint(String text) { |
| 267 if (text == 'any') { | 245 if (text == 'any') { |
| 268 return new VersionRange(); | 246 return new VersionRange(); |
| 269 } | 247 } |
| 270 | 248 |
| 271 // TODO(rnystrom): Consider other syntaxes for version constraints. This | 249 // TODO(rnystrom): Consider other syntaxes for version constraints. This |
| 272 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is | 250 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is |
| 273 // unfortunately meaningful in YAML, requiring it to be quoted in a | 251 // unfortunately meaningful in YAML, requiring it to be quoted in a |
| 274 // pubspec. | 252 // pubspec. |
| 275 // See if it's a comparison operator followed by a version, like ">1.2.3". | 253 // See if it's a comparison operator followed by a version, like ">1.2.3". |
| 276 var match = new RegExp(r"^([<>]=?)?(.*)$").firstMatch(text); | 254 var match = new RegExp(r"^([<>]=?)?(.*)$").firstMatch(text); |
| 277 if (match != null) { | 255 if (match != null) { |
| 278 var comparison = match[1]; | 256 var comparison = match[1]; |
| 279 var version = new Version.parse(match[2]); | 257 var version = new Version.parse(match[2]); |
| 280 switch (match[1]) { | 258 switch (match[1]) { |
| 281 case '<=': return new VersionRange(max: version, includeMax: true); | 259 case '<=': return new VersionRange(max: version, includeMax: true); |
| 282 case '<': return new VersionRange(max: version, includeMax: false); | 260 case '<': return new VersionRange(max: version, includeMax: false); |
| 283 case '>=': return new VersionRange(min: version, includeMin: true); | 261 case '>=': return new VersionRange(min: version, includeMin: true); |
| 284 case '>': return new VersionRange(min: version, includeMin: false); | 262 case '>': return new VersionRange(min: version, includeMin: false); |
| 285 } | 263 } |
| 286 } | 264 } |
| 287 | 265 |
| 288 // Otherwise, it must be an explicit version. | 266 // Otherwise, it must be an explicit version. |
| 289 return new Version.parse(text); | 267 return new Version.parse(text); |
| 290 } | 268 } |
| 291 } | 269 } |
| 292 | 270 |
| 293 /** | 271 /// Constrains versions to a fall within a given range. If there is a minimum, |
| 294 * Constrains versions to a fall within a given range. If there is a minimum, | 272 /// then this only allows versions that are at that minimum or greater. If there |
| 295 * then this only allows versions that are at that minimum or greater. If there | 273 /// is a maximum, then only versions less than that are allowed. In other words, |
| 296 * is a maximum, then only versions less than that are allowed. In other words, | 274 /// this allows `>= min, < max`. |
| 297 * this allows `>= min, < max`. | |
| 298 */ | |
| 299 class VersionRange implements VersionConstraint { | 275 class VersionRange implements VersionConstraint { |
| 300 final Version min; | 276 final Version min; |
| 301 final Version max; | 277 final Version max; |
| 302 final bool includeMin; | 278 final bool includeMin; |
| 303 final bool includeMax; | 279 final bool includeMax; |
| 304 | 280 |
| 305 VersionRange({this.min, this.max, | 281 VersionRange({this.min, this.max, |
| 306 this.includeMin: false, this.includeMax: false}) { | 282 this.includeMin: false, this.includeMax: false}) { |
| 307 if (min != null && max != null && min > max) { | 283 if (min != null && max != null && min > max) { |
| 308 throw new ArgumentError( | 284 throw new ArgumentError( |
| 309 'Minimum version ("$min") must be less than maximum ("$max").'); | 285 'Minimum version ("$min") must be less than maximum ("$max").'); |
| 310 } | 286 } |
| 311 } | 287 } |
| 312 | 288 |
| 313 bool operator ==(other) { | 289 bool operator ==(other) { |
| 314 if (other is! VersionRange) return false; | 290 if (other is! VersionRange) return false; |
| 315 | 291 |
| 316 return min == other.min && | 292 return min == other.min && |
| 317 max == other.max && | 293 max == other.max && |
| 318 includeMin == other.includeMin && | 294 includeMin == other.includeMin && |
| 319 includeMax == other.includeMax; | 295 includeMax == other.includeMax; |
| 320 } | 296 } |
| 321 | 297 |
| 322 bool get isEmpty => false; | 298 bool get isEmpty => false; |
| 323 | 299 |
| 324 /** Tests if [other] matches falls within this version range. */ | 300 /// Tests if [other] matches falls within this version range. |
| 325 bool allows(Version other) { | 301 bool allows(Version other) { |
| 326 if (min != null && other < min) return false; | 302 if (min != null && other < min) return false; |
| 327 if (min != null && !includeMin && other == min) return false; | 303 if (min != null && !includeMin && other == min) return false; |
| 328 if (max != null && other > max) return false; | 304 if (max != null && other > max) return false; |
| 329 if (max != null && !includeMax && other == max) return false; | 305 if (max != null && !includeMax && other == max) return false; |
| 330 return true; | 306 return true; |
| 331 } | 307 } |
| 332 | 308 |
| 333 VersionConstraint intersect(VersionConstraint other) { | 309 VersionConstraint intersect(VersionConstraint other) { |
| 334 if (other.isEmpty) return other; | 310 if (other.isEmpty) return other; |
| (...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 412 } | 388 } |
| 413 | 389 |
| 414 class _EmptyVersion implements VersionConstraint { | 390 class _EmptyVersion implements VersionConstraint { |
| 415 const _EmptyVersion(); | 391 const _EmptyVersion(); |
| 416 | 392 |
| 417 bool get isEmpty => true; | 393 bool get isEmpty => true; |
| 418 bool allows(Version other) => false; | 394 bool allows(Version other) => false; |
| 419 VersionConstraint intersect(VersionConstraint other) => this; | 395 VersionConstraint intersect(VersionConstraint other) => this; |
| 420 String toString() => '<empty>'; | 396 String toString() => '<empty>'; |
| 421 } | 397 } |
| OLD | NEW |