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 : const _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 : const _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. |
| 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 const _EmptyVersion(); |
| 331 } |
| 332 |
| 333 if (intersectMin != null && intersectMax != null && |
| 334 intersectMin > intersectMax) { |
| 335 // Non-overlapping ranges, so empty. |
| 336 return const _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'); |
| 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 class _VersionConstraintFactory { |
| 377 factory VersionConstraint.empty() => const _EmptyVersion(); |
| 378 |
| 379 factory VersionConstraint.parse(String text) { |
| 380 if (text.trim() == '') { |
| 381 throw new FormatException('Cannot parse an empty string.'); |
| 382 } |
| 383 |
| 384 // Split it into space-separated parts. |
| 385 var constraints = <VersionConstraint>[]; |
| 386 for (var part in text.split(' ')) { |
| 387 constraints.add(parseSingleConstraint(part)); |
| 388 } |
| 389 |
| 390 return new VersionConstraint.intersect(constraints); |
| 391 } |
| 392 |
| 393 factory VersionConstraint.intersect( |
| 394 Collection<VersionConstraint> constraints) { |
| 395 var constraint = new VersionRange(); |
| 396 for (var other in constraints) { |
| 397 constraint = constraint.intersect(other); |
| 398 } |
| 399 return constraint; |
| 400 } |
| 401 |
| 402 static VersionConstraint parseSingleConstraint(String text) { |
| 403 if (text == 'any') { |
| 404 return new VersionRange(); |
| 405 } |
| 406 |
| 407 // TODO(rnystrom): Consider other syntaxes for version constraints. This |
| 408 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is |
| 409 // unfortunately meaningful in YAML, requiring it to be quoted in a |
| 410 // pubspec. |
| 411 // See if it's a comparison operator followed by a version, like ">1.2.3". |
| 412 var match = const RegExp(@"^([<>]=?)?(.*)$").firstMatch(text); |
| 413 if (match != null) { |
| 414 var comparison = match[1]; |
| 415 var version = new Version.parse(match[2]); |
| 416 switch (match[1]) { |
| 417 case '<=': return new VersionRange(max: version, includeMax: true); |
| 418 case '<': return new VersionRange(max: version, includeMax: false); |
| 419 case '>=': return new VersionRange(min: version, includeMin: true); |
| 420 case '>': return new VersionRange(min: version, includeMin: false); |
| 421 } |
| 422 } |
| 423 |
| 424 // Otherwise, it must be an explicit version. |
| 425 return new Version.parse(text); |
| 426 } |
| 427 } |
OLD | NEW |