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 |