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"^[<>]=?"); |
| 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 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
139 return _compareStrings(build, other.build); | 146 return _compareStrings(build, other.build); |
140 } | 147 } |
141 | 148 |
142 return 0; | 149 return 0; |
143 } | 150 } |
144 | 151 |
145 int get hashCode => toString().hashCode; | 152 int get hashCode => toString().hashCode; |
146 | 153 |
147 String toString() { | 154 String toString() { |
148 var buffer = new StringBuffer(); | 155 var buffer = new StringBuffer(); |
149 buffer.add('$major.$minor.$patch'); | 156 buffer.write('$major.$minor.$patch'); |
150 if (preRelease != null) buffer.add('-$preRelease'); | 157 if (preRelease != null) buffer.write('-$preRelease'); |
151 if (build != null) buffer.add('+$build'); | 158 if (build != null) buffer.write('+$build'); |
152 return buffer.toString(); | 159 return buffer.toString(); |
153 } | 160 } |
154 | 161 |
155 /// Compares the string part of two versions. This is used for the pre-release | 162 /// Compares the string part of two versions. This is used for the pre-release |
156 /// and build version parts. This follows Rule 12. of the Semantic Versioning | 163 /// and build version parts. This follows Rule 12. of the Semantic Versioning |
157 /// spec. | 164 /// spec. |
158 int _compareStrings(String a, String b) { | 165 int _compareStrings(String a, String b) { |
159 var aParts = _splitParts(a); | 166 var aParts = _splitParts(a); |
160 var bParts = _splitParts(b); | 167 var bParts = _splitParts(b); |
161 | 168 |
(...skipping 45 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 either "any" or a series of |
218 /// version parts. Each part can be one of: | 225 /// version parts. 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 /// |
| 236 /// any |
227 /// 1.2.3-alpha | 237 /// 1.2.3-alpha |
228 /// <=5.1.4 | 238 /// <=5.1.4 |
229 /// >2.0.4 <=2.4.6 | 239 /// >2.0.4 <= 2.4.6 |
230 factory VersionConstraint.parse(String text) { | 240 factory VersionConstraint.parse(String text) { |
231 if (text.trim() == '') { | 241 // Handle the "any" constraint. |
| 242 if (text.trim() == "any") return new VersionRange(); |
| 243 |
| 244 var originalText = text; |
| 245 var constraints = <VersionConstraint>[]; |
| 246 |
| 247 void skipWhitespace() { |
| 248 text = text.trim(); |
| 249 } |
| 250 |
| 251 // Try to parse and consume a version number. |
| 252 Version matchVersion() { |
| 253 var version = _START_VERSION.firstMatch(text); |
| 254 if (version == null) return null; |
| 255 |
| 256 text = text.substring(version.end); |
| 257 return new Version.parse(version[0]); |
| 258 } |
| 259 |
| 260 // Try to parse and consume a comparison operator followed by a version. |
| 261 VersionConstraint matchComparison() { |
| 262 var comparison = _START_COMPARISON.firstMatch(text); |
| 263 if (comparison == null) return null; |
| 264 |
| 265 var op = comparison[0]; |
| 266 text = text.substring(comparison.end); |
| 267 skipWhitespace(); |
| 268 |
| 269 var version = matchVersion(); |
| 270 if (version == null) { |
| 271 throw new FormatException('Expected version number after "$op" in ' |
| 272 '"$originalText", got "$text".'); |
| 273 } |
| 274 |
| 275 switch (op) { |
| 276 case '<=': |
| 277 return new VersionRange(max: version, includeMax: true); |
| 278 case '<': |
| 279 return new VersionRange(max: version, includeMax: false); |
| 280 case '>=': |
| 281 return new VersionRange(min: version, includeMin: true); |
| 282 case '>': |
| 283 return new VersionRange(min: version, includeMin: false); |
| 284 } |
| 285 } |
| 286 |
| 287 while (true) { |
| 288 skipWhitespace(); |
| 289 if (text.isEmpty) break; |
| 290 |
| 291 var version = matchVersion(); |
| 292 if (version != null) { |
| 293 constraints.add(version); |
| 294 continue; |
| 295 } |
| 296 |
| 297 var comparison = matchComparison(); |
| 298 if (comparison != null) { |
| 299 constraints.add(comparison); |
| 300 continue; |
| 301 } |
| 302 |
| 303 // If we got here, we couldn't parse the remaining string. |
| 304 throw new FormatException('Could not parse version "$originalText". ' |
| 305 'Unknown text at "$text".'); |
| 306 } |
| 307 |
| 308 if (constraints.isEmpty) { |
232 throw new FormatException('Cannot parse an empty string.'); | 309 throw new FormatException('Cannot parse an empty string.'); |
233 } | 310 } |
234 | 311 |
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); | 312 return new VersionConstraint.intersection(constraints); |
242 } | 313 } |
243 | 314 |
244 /// Creates a new version constraint that is the intersection of | 315 /// Creates a new version constraint that is the intersection of |
245 /// [constraints]. It will only allow versions that all of those constraints | 316 /// [constraints]. It will only allow versions that all of those constraints |
246 /// allow. If constraints is empty, then it returns a VersionConstraint that | 317 /// allow. If constraints is empty, then it returns a VersionConstraint that |
247 /// allows all versions. | 318 /// allows all versions. |
248 factory VersionConstraint.intersection( | 319 factory VersionConstraint.intersection( |
249 Iterable<VersionConstraint> constraints) { | 320 Iterable<VersionConstraint> constraints) { |
250 var constraint = new VersionRange(); | 321 var constraint = new VersionRange(); |
251 for (var other in constraints) { | 322 for (var other in constraints) { |
252 constraint = constraint.intersect(other); | 323 constraint = constraint.intersect(other); |
253 } | 324 } |
254 return constraint; | 325 return constraint; |
255 } | 326 } |
256 | 327 |
257 /// Returns `true` if this constraint allows no versions. | 328 /// Returns `true` if this constraint allows no versions. |
258 bool get isEmpty; | 329 bool get isEmpty; |
259 | 330 |
260 /// Returns `true` if this constraint allows all versions. | 331 /// Returns `true` if this constraint allows all versions. |
261 bool get isAny; | 332 bool get isAny; |
262 | 333 |
263 /// Returns `true` if this constraint allows [version]. | 334 /// Returns `true` if this constraint allows [version]. |
264 bool allows(Version version); | 335 bool allows(Version version); |
265 | 336 |
266 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by | 337 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by |
267 /// both this and [other]. | 338 /// both this and [other]. |
268 VersionConstraint intersect(VersionConstraint other); | 339 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 } | 340 } |
296 | 341 |
297 /// Constrains versions to a fall within a given range. If there is a minimum, | 342 /// 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 | 343 /// 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, | 344 /// is a maximum, then only versions less than that are allowed. In other words, |
300 /// this allows `>= min, < max`. | 345 /// this allows `>= min, < max`. |
301 class VersionRange implements VersionConstraint { | 346 class VersionRange implements VersionConstraint { |
302 final Version min; | 347 final Version min; |
303 final Version max; | 348 final Version max; |
304 final bool includeMin; | 349 final bool includeMin; |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
395 } | 440 } |
396 | 441 |
397 throw new ArgumentError( | 442 throw new ArgumentError( |
398 'Unknown VersionConstraint type $other.'); | 443 'Unknown VersionConstraint type $other.'); |
399 } | 444 } |
400 | 445 |
401 String toString() { | 446 String toString() { |
402 var buffer = new StringBuffer(); | 447 var buffer = new StringBuffer(); |
403 | 448 |
404 if (min != null) { | 449 if (min != null) { |
405 buffer.add(includeMin ? '>=' : '>'); | 450 buffer.write(includeMin ? '>=' : '>'); |
406 buffer.add(min); | 451 buffer.write(min); |
407 } | 452 } |
408 | 453 |
409 if (max != null) { | 454 if (max != null) { |
410 if (min != null) buffer.add(' '); | 455 if (min != null) buffer.write(' '); |
411 buffer.add(includeMax ? '<=' : '<'); | 456 buffer.write(includeMax ? '<=' : '<'); |
412 buffer.add(max); | 457 buffer.write(max); |
413 } | 458 } |
414 | 459 |
415 if (min == null && max == null) buffer.add('any'); | 460 if (min == null && max == null) buffer.write('any'); |
416 return buffer.toString(); | 461 return buffer.toString(); |
417 } | 462 } |
418 } | 463 } |
419 | 464 |
420 class _EmptyVersion implements VersionConstraint { | 465 class _EmptyVersion implements VersionConstraint { |
421 const _EmptyVersion(); | 466 const _EmptyVersion(); |
422 | 467 |
423 bool get isEmpty => true; | 468 bool get isEmpty => true; |
424 bool get isAny => false; | 469 bool get isAny => false; |
425 bool allows(Version other) => false; | 470 bool allows(Version other) => false; |
426 VersionConstraint intersect(VersionConstraint other) => this; | 471 VersionConstraint intersect(VersionConstraint other) => this; |
427 String toString() => '<empty>'; | 472 String toString() => '<empty>'; |
428 } | 473 } |
OLD | NEW |