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 /** | 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 |