Chromium Code Reviews| 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 /// Handles version numbers, following the [Semantic Versioning][semver] spec. | 5 /// Handles version numbers, following the [Semantic Versioning][semver] spec. |
| 6 /// | 6 /// |
| 7 /// [semver]: http://semver.org/ | 7 /// [semver]: http://semver.org/ |
| 8 library version; | 8 library version; |
| 9 | 9 |
| 10 import 'dart:math'; | 10 import 'dart:math'; |
| 11 | 11 |
| 12 import 'utils.dart'; | 12 import 'utils.dart'; |
| 13 | 13 |
| 14 | |
| 15 /// Regex that matches a version number at the beginning of a string. | |
| 16 final _START_VERSION = new RegExp( | |
| 17 r'^' // Start at beginning. | |
| 18 r'(\d+).(\d+).(\d+)' // Version number. | |
| 19 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. | |
| 20 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build. | |
| 21 | |
| 22 /// Like [_START_VERSION] but matches the entire string. | |
| 23 final _COMPLETE_VERSION = new RegExp("${_START_VERSION.pattern}\$"); | |
| 24 | |
| 25 /// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of | |
| 26 /// a string. | |
| 27 final _START_COMPARISON = new RegExp(r"[<>]=?"); | |
|
nweiz
2013/02/21 01:31:11
This should start with "^".
Bob Nystrom
2013/02/21 20:00:50
Oops. Done.
| |
| 28 | |
| 14 /// A parsed semantic version number. | 29 /// A parsed semantic version number. |
| 15 class Version implements Comparable<Version>, VersionConstraint { | 30 class Version implements Comparable<Version>, VersionConstraint { |
| 16 /// No released version: i.e. "0.0.0". | 31 /// No released version: i.e. "0.0.0". |
| 17 static Version get none => new Version(0, 0, 0); | 32 static Version get none => new Version(0, 0, 0); |
| 18 | |
| 19 static final _PARSE_REGEX = new RegExp( | |
| 20 r'^' // Start at beginning. | |
| 21 r'(\d+).(\d+).(\d+)' // Version number. | |
| 22 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. | |
| 23 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. | |
| 24 r'$'); // Consume entire string. | |
| 25 | |
| 26 /// The major version number: "1" in "1.2.3". | 33 /// The major version number: "1" in "1.2.3". |
| 27 final int major; | 34 final int major; |
| 28 | 35 |
| 29 /// The minor version number: "2" in "1.2.3". | 36 /// The minor version number: "2" in "1.2.3". |
| 30 final int minor; | 37 final int minor; |
| 31 | 38 |
| 32 /// The patch version number: "3" in "1.2.3". | 39 /// The patch version number: "3" in "1.2.3". |
| 33 final int patch; | 40 final int patch; |
| 34 | 41 |
| 35 /// The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. | 42 /// The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. |
| 36 final String preRelease; | 43 final String preRelease; |
| 37 | 44 |
| 38 /// The build identifier: "foo" in "1.2.3+foo". May be `null`. | 45 /// The build identifier: "foo" in "1.2.3+foo". May be `null`. |
| 39 final String build; | 46 final String build; |
| 40 | 47 |
| 41 /// Creates a new [Version] object. | 48 /// Creates a new [Version] object. |
| 42 Version(this.major, this.minor, this.patch, {String pre, this.build}) | 49 Version(this.major, this.minor, this.patch, {String pre, this.build}) |
| 43 : preRelease = pre { | 50 : preRelease = pre { |
| 44 if (major < 0) throw new ArgumentError( | 51 if (major < 0) throw new ArgumentError( |
| 45 'Major version must be non-negative.'); | 52 'Major version must be non-negative.'); |
| 46 if (minor < 0) throw new ArgumentError( | 53 if (minor < 0) throw new ArgumentError( |
| 47 'Minor version must be non-negative.'); | 54 'Minor version must be non-negative.'); |
| 48 if (patch < 0) throw new ArgumentError( | 55 if (patch < 0) throw new ArgumentError( |
| 49 'Patch version must be non-negative.'); | 56 'Patch version must be non-negative.'); |
| 50 } | 57 } |
| 51 | 58 |
| 52 /// Creates a new [Version] by parsing [text]. | 59 /// Creates a new [Version] by parsing [text]. |
| 53 factory Version.parse(String text) { | 60 factory Version.parse(String text) { |
| 54 final match = _PARSE_REGEX.firstMatch(text); | 61 final match = _COMPLETE_VERSION.firstMatch(text); |
| 55 if (match == null) { | 62 if (match == null) { |
| 56 throw new FormatException('Could not parse "$text".'); | 63 throw new FormatException('Could not parse "$text".'); |
| 57 } | 64 } |
| 58 | 65 |
| 59 try { | 66 try { |
| 60 int major = int.parse(match[1]); | 67 int major = int.parse(match[1]); |
| 61 int minor = int.parse(match[2]); | 68 int minor = int.parse(match[2]); |
| 62 int patch = int.parse(match[3]); | 69 int patch = int.parse(match[3]); |
| 63 | 70 |
| 64 String preRelease = match[5]; | 71 String preRelease = match[5]; |
| (...skipping 142 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 207 /// version is valid or not. For example, a ">= 2.0.0" constraint allows any | 214 /// version is valid or not. For example, a ">= 2.0.0" constraint allows any |
| 208 /// version that is "2.0.0" or greater. Version objects themselves implement | 215 /// version that is "2.0.0" or greater. Version objects themselves implement |
| 209 /// this to match a specific version. | 216 /// this to match a specific version. |
| 210 abstract class VersionConstraint { | 217 abstract class VersionConstraint { |
| 211 /// A [VersionConstraint] that allows all versions. | 218 /// A [VersionConstraint] that allows all versions. |
| 212 static VersionConstraint any = new VersionRange(); | 219 static VersionConstraint any = new VersionRange(); |
| 213 | 220 |
| 214 /// A [VersionConstraint] that allows no versions: i.e. the empty set. | 221 /// A [VersionConstraint] that allows no versions: i.e. the empty set. |
| 215 static VersionConstraint empty = const _EmptyVersion(); | 222 static VersionConstraint empty = const _EmptyVersion(); |
| 216 | 223 |
| 217 /// Parses a version constraint. This string is a space-separated series of | 224 /// Parses a version constraint. This string is a series of version parts. |
| 218 /// version parts. Each part can be one of: | 225 /// Each part can be one of: |
| 219 /// | 226 /// |
| 220 /// * A version string like `1.2.3`. In other words, anything that can be | 227 /// * A version string like `1.2.3`. In other words, anything that can be |
| 221 /// parsed by [Version.parse()]. | 228 /// parsed by [Version.parse()]. |
| 222 /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version | 229 /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version |
| 223 /// string. There cannot be a space between the operator and the version. | 230 /// string. |
| 231 /// | |
| 232 /// Whitespace is ignored. | |
| 224 /// | 233 /// |
| 225 /// Examples: | 234 /// Examples: |
| 226 /// | 235 /// |
| 227 /// 1.2.3-alpha | 236 /// 1.2.3-alpha |
| 228 /// <=5.1.4 | 237 /// <=5.1.4 |
| 229 /// >2.0.4 <=2.4.6 | 238 /// >2.0.4 <= 2.4.6 |
| 239 /// any | |
| 230 factory VersionConstraint.parse(String text) { | 240 factory VersionConstraint.parse(String text) { |
| 231 if (text.trim() == '') { | 241 var originalText = text; |
| 242 var constraints = <VersionConstraint>[]; | |
| 243 | |
| 244 void skipWhitespace() { | |
| 245 text = text.trim(); | |
| 246 } | |
| 247 | |
| 248 // Try to parse and consume "any". | |
| 249 VersionRange matchAny() { | |
| 250 // TODO(rnystrom): This doesn't require whitespace or a punctuator to | |
| 251 // separate "any". So this is valid: "anyany>1.0.0any". Is that OK? | |
|
nweiz
2013/02/21 01:31:11
It's weird that we allow "any" to be alongside any
Bob Nystrom
2013/02/21 20:00:50
You're exactly right. Thinking of "any" as a const
| |
| 252 if (!text.startsWith("any")) return null; | |
| 253 text = text.substring("any".length); | |
| 254 return new VersionRange(); | |
| 255 } | |
| 256 | |
| 257 // Try to parse and consume a version number. | |
| 258 Version matchVersion() { | |
| 259 var version = _START_VERSION.firstMatch(text); | |
| 260 if (version == null) return null; | |
| 261 | |
| 262 text = text.substring(version.end); | |
| 263 return new Version.parse(version[0]); | |
| 264 } | |
| 265 | |
| 266 // Try to parse and consume a comparison operator followed by a version. | |
| 267 VersionConstraint matchComparison() { | |
| 268 var comparison = _START_COMPARISON.firstMatch(text); | |
| 269 if (comparison == null) return null; | |
| 270 | |
| 271 var op = comparison[0]; | |
| 272 text = text.substring(comparison.end); | |
| 273 skipWhitespace(); | |
| 274 | |
| 275 var version = matchVersion(); | |
| 276 if (version == null) { | |
| 277 throw new FormatException('Expected version number after "$op" in ' | |
| 278 '"$originalText", got "$text".'); | |
|
nweiz
2013/02/21 01:31:11
I still wish these exceptions gave more context, a
Bob Nystrom
2013/02/21 20:00:50
There's not much Version can do here. What we can
| |
| 279 } | |
| 280 | |
| 281 switch (op) { | |
| 282 case '<=': | |
| 283 return new VersionRange(max: version, includeMax: true); | |
| 284 case '<': | |
| 285 return new VersionRange(max: version, includeMax: false); | |
| 286 case '>=': | |
| 287 return new VersionRange(min: version, includeMin: true); | |
| 288 case '>': | |
| 289 return new VersionRange(min: version, includeMin: false); | |
| 290 } | |
| 291 } | |
| 292 | |
| 293 while (true) { | |
| 294 skipWhitespace(); | |
| 295 if (text.isEmpty) break; | |
| 296 | |
| 297 var any = matchAny(); | |
| 298 if (any != null) { | |
| 299 constraints.add(any); | |
| 300 continue; | |
| 301 } | |
| 302 | |
| 303 var version = matchVersion(); | |
| 304 if (version != null) { | |
| 305 constraints.add(version); | |
| 306 continue; | |
| 307 } | |
| 308 | |
| 309 var comparison = matchComparison(); | |
| 310 if (comparison != null) { | |
| 311 constraints.add(comparison); | |
| 312 continue; | |
| 313 } | |
| 314 | |
| 315 // If we got here, we couldn't parse the remaining string. | |
| 316 throw new FormatException('Could not parse version "$originalText". ' | |
| 317 'Unknown text at "$text".'); | |
| 318 } | |
| 319 | |
| 320 if (constraints.isEmpty) { | |
| 232 throw new FormatException('Cannot parse an empty string.'); | 321 throw new FormatException('Cannot parse an empty string.'); |
| 233 } | 322 } |
| 234 | 323 |
| 235 // Split it into space-separated parts. | |
| 236 var constraints = <VersionConstraint>[]; | |
| 237 for (var part in text.split(' ')) { | |
| 238 constraints.add(_parseSingleConstraint(part)); | |
| 239 } | |
| 240 | |
| 241 return new VersionConstraint.intersection(constraints); | 324 return new VersionConstraint.intersection(constraints); |
| 242 } | 325 } |
| 243 | 326 |
| 244 /// Creates a new version constraint that is the intersection of | 327 /// Creates a new version constraint that is the intersection of |
| 245 /// [constraints]. It will only allow versions that all of those constraints | 328 /// [constraints]. It will only allow versions that all of those constraints |
| 246 /// allow. If constraints is empty, then it returns a VersionConstraint that | 329 /// allow. If constraints is empty, then it returns a VersionConstraint that |
| 247 /// allows all versions. | 330 /// allows all versions. |
| 248 factory VersionConstraint.intersection( | 331 factory VersionConstraint.intersection( |
| 249 Iterable<VersionConstraint> constraints) { | 332 Iterable<VersionConstraint> constraints) { |
| 250 var constraint = new VersionRange(); | 333 var constraint = new VersionRange(); |
| 251 for (var other in constraints) { | 334 for (var other in constraints) { |
| 252 constraint = constraint.intersect(other); | 335 constraint = constraint.intersect(other); |
| 253 } | 336 } |
| 254 return constraint; | 337 return constraint; |
| 255 } | 338 } |
| 256 | 339 |
| 257 /// Returns `true` if this constraint allows no versions. | 340 /// Returns `true` if this constraint allows no versions. |
| 258 bool get isEmpty; | 341 bool get isEmpty; |
| 259 | 342 |
| 260 /// Returns `true` if this constraint allows all versions. | 343 /// Returns `true` if this constraint allows all versions. |
| 261 bool get isAny; | 344 bool get isAny; |
| 262 | 345 |
| 263 /// Returns `true` if this constraint allows [version]. | 346 /// Returns `true` if this constraint allows [version]. |
| 264 bool allows(Version version); | 347 bool allows(Version version); |
| 265 | 348 |
| 266 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by | 349 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by |
| 267 /// both this and [other]. | 350 /// both this and [other]. |
| 268 VersionConstraint intersect(VersionConstraint other); | 351 VersionConstraint intersect(VersionConstraint other); |
| 269 | |
| 270 static VersionConstraint _parseSingleConstraint(String text) { | |
| 271 if (text == 'any') { | |
| 272 return new VersionRange(); | |
| 273 } | |
| 274 | |
| 275 // TODO(rnystrom): Consider other syntaxes for version constraints. This | |
| 276 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is | |
| 277 // unfortunately meaningful in YAML, requiring it to be quoted in a | |
| 278 // pubspec. | |
| 279 // See if it's a comparison operator followed by a version, like ">1.2.3". | |
| 280 var match = new RegExp(r"^([<>]=?)?(.*)$").firstMatch(text); | |
| 281 if (match != null) { | |
| 282 var comparison = match[1]; | |
| 283 var version = new Version.parse(match[2]); | |
| 284 switch (match[1]) { | |
| 285 case '<=': return new VersionRange(max: version, includeMax: true); | |
| 286 case '<': return new VersionRange(max: version, includeMax: false); | |
| 287 case '>=': return new VersionRange(min: version, includeMin: true); | |
| 288 case '>': return new VersionRange(min: version, includeMin: false); | |
| 289 } | |
| 290 } | |
| 291 | |
| 292 // Otherwise, it must be an explicit version. | |
| 293 return new Version.parse(text); | |
| 294 } | |
| 295 } | 352 } |
| 296 | 353 |
| 297 /// Constrains versions to a fall within a given range. If there is a minimum, | 354 /// Constrains versions to a fall within a given range. If there is a minimum, |
| 298 /// then this only allows versions that are at that minimum or greater. If there | 355 /// then this only allows versions that are at that minimum or greater. If there |
| 299 /// is a maximum, then only versions less than that are allowed. In other words, | 356 /// is a maximum, then only versions less than that are allowed. In other words, |
| 300 /// this allows `>= min, < max`. | 357 /// this allows `>= min, < max`. |
| 301 class VersionRange implements VersionConstraint { | 358 class VersionRange implements VersionConstraint { |
| 302 final Version min; | 359 final Version min; |
| 303 final Version max; | 360 final Version max; |
| 304 final bool includeMin; | 361 final bool includeMin; |
| (...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 419 | 476 |
| 420 class _EmptyVersion implements VersionConstraint { | 477 class _EmptyVersion implements VersionConstraint { |
| 421 const _EmptyVersion(); | 478 const _EmptyVersion(); |
| 422 | 479 |
| 423 bool get isEmpty => true; | 480 bool get isEmpty => true; |
| 424 bool get isAny => false; | 481 bool get isAny => false; |
| 425 bool allows(Version other) => false; | 482 bool allows(Version other) => false; |
| 426 VersionConstraint intersect(VersionConstraint other) => this; | 483 VersionConstraint intersect(VersionConstraint other) => this; |
| 427 String toString() => '<empty>'; | 484 String toString() => '<empty>'; |
| 428 } | 485 } |
| OLD | NEW |