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 /// Handles version numbers, following the [Semantic Versioning][semver] spec. |
6 * Handles version numbers, following the [Semantic Versioning][semver] spec. | 6 /// |
7 * | 7 /// [semver]: http://semver.org/ |
8 * [semver]: http://semver.org/ | |
9 */ | |
10 library version; | 8 library version; |
11 | 9 |
12 import 'dart:math'; | 10 import 'dart:math'; |
13 | 11 |
14 import 'utils.dart'; | 12 import 'utils.dart'; |
15 | 13 |
16 /** A parsed semantic version number. */ | 14 /// A parsed semantic version number. |
17 class Version implements Comparable, VersionConstraint { | 15 class Version implements Comparable, VersionConstraint { |
18 /** No released version: i.e. "0.0.0". */ | 16 /// No released version: i.e. "0.0.0". |
19 static Version get none => new Version(0, 0, 0); | 17 static Version get none => new Version(0, 0, 0); |
20 | 18 |
21 static final _PARSE_REGEX = new RegExp( | 19 static final _PARSE_REGEX = new RegExp( |
22 r'^' // Start at beginning. | 20 r'^' // Start at beginning. |
23 r'(\d+).(\d+).(\d+)' // Version number. | 21 r'(\d+).(\d+).(\d+)' // Version number. |
24 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. | 22 r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. |
25 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. | 23 r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Build. |
26 r'$'); // Consume entire string. | 24 r'$'); // Consume entire string. |
27 | 25 |
28 /** The major version number: "1" in "1.2.3". */ | 26 /// The major version number: "1" in "1.2.3". |
29 final int major; | 27 final int major; |
30 | 28 |
31 /** The minor version number: "2" in "1.2.3". */ | 29 /// The minor version number: "2" in "1.2.3". |
32 final int minor; | 30 final int minor; |
33 | 31 |
34 /** The patch version number: "3" in "1.2.3". */ | 32 /// The patch version number: "3" in "1.2.3". |
35 final int patch; | 33 final int patch; |
36 | 34 |
37 /** The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. */ | 35 /// The pre-release identifier: "foo" in "1.2.3-foo". May be `null`. |
38 final String preRelease; | 36 final String preRelease; |
39 | 37 |
40 /** The build identifier: "foo" in "1.2.3+foo". May be `null`. */ | 38 /// The build identifier: "foo" in "1.2.3+foo". May be `null`. |
41 final String build; | 39 final String build; |
42 | 40 |
43 /** Creates a new [Version] object. */ | 41 /// Creates a new [Version] object. |
44 Version(this.major, this.minor, this.patch, {String pre, this.build}) | 42 Version(this.major, this.minor, this.patch, {String pre, this.build}) |
45 : preRelease = pre { | 43 : preRelease = pre { |
46 if (major < 0) throw new ArgumentError( | 44 if (major < 0) throw new ArgumentError( |
47 'Major version must be non-negative.'); | 45 'Major version must be non-negative.'); |
48 if (minor < 0) throw new ArgumentError( | 46 if (minor < 0) throw new ArgumentError( |
49 'Minor version must be non-negative.'); | 47 'Minor version must be non-negative.'); |
50 if (patch < 0) throw new ArgumentError( | 48 if (patch < 0) throw new ArgumentError( |
51 'Patch version must be non-negative.'); | 49 'Patch version must be non-negative.'); |
52 } | 50 } |
53 | 51 |
54 /** | 52 /// Creates a new [Version] by parsing [text]. |
55 * Creates a new [Version] by parsing [text]. | |
56 */ | |
57 factory Version.parse(String text) { | 53 factory Version.parse(String text) { |
58 final match = _PARSE_REGEX.firstMatch(text); | 54 final match = _PARSE_REGEX.firstMatch(text); |
59 if (match == null) { | 55 if (match == null) { |
60 throw new FormatException('Could not parse "$text".'); | 56 throw new FormatException('Could not parse "$text".'); |
61 } | 57 } |
62 | 58 |
63 try { | 59 try { |
64 int major = parseInt(match[1]); | 60 int major = parseInt(match[1]); |
65 int minor = parseInt(match[2]); | 61 int minor = parseInt(match[2]); |
66 int patch = parseInt(match[3]); | 62 int patch = parseInt(match[3]); |
(...skipping 12 matching lines...) Expand all Loading... |
79 return compareTo(other) == 0; | 75 return compareTo(other) == 0; |
80 } | 76 } |
81 | 77 |
82 bool operator <(Version other) => compareTo(other) < 0; | 78 bool operator <(Version other) => compareTo(other) < 0; |
83 bool operator >(Version other) => compareTo(other) > 0; | 79 bool operator >(Version other) => compareTo(other) > 0; |
84 bool operator <=(Version other) => compareTo(other) <= 0; | 80 bool operator <=(Version other) => compareTo(other) <= 0; |
85 bool operator >=(Version other) => compareTo(other) >= 0; | 81 bool operator >=(Version other) => compareTo(other) >= 0; |
86 | 82 |
87 bool get isEmpty => false; | 83 bool get isEmpty => false; |
88 | 84 |
89 /** Tests if [other] matches this version exactly. */ | 85 /// Tests if [other] matches this version exactly. |
90 bool allows(Version other) => this == other; | 86 bool allows(Version other) => this == other; |
91 | 87 |
92 VersionConstraint intersect(VersionConstraint other) { | 88 VersionConstraint intersect(VersionConstraint other) { |
93 if (other.isEmpty) return other; | 89 if (other.isEmpty) return other; |
94 | 90 |
95 // Intersect a version and a range. | 91 // Intersect a version and a range. |
96 if (other is VersionRange) return other.intersect(this); | 92 if (other is VersionRange) return other.intersect(this); |
97 | 93 |
98 // Intersecting two versions only works if they are the same. | 94 // Intersecting two versions only works if they are the same. |
99 if (other is Version) return this == other ? this : const _EmptyVersion(); | 95 if (other is Version) return this == other ? this : const _EmptyVersion(); |
(...skipping 29 matching lines...) Expand all Loading... |
129 int get hashCode => toString().hashCode; | 125 int get hashCode => toString().hashCode; |
130 | 126 |
131 String toString() { | 127 String toString() { |
132 var buffer = new StringBuffer(); | 128 var buffer = new StringBuffer(); |
133 buffer.add('$major.$minor.$patch'); | 129 buffer.add('$major.$minor.$patch'); |
134 if (preRelease != null) buffer.add('-$preRelease'); | 130 if (preRelease != null) buffer.add('-$preRelease'); |
135 if (build != null) buffer.add('+$build'); | 131 if (build != null) buffer.add('+$build'); |
136 return buffer.toString(); | 132 return buffer.toString(); |
137 } | 133 } |
138 | 134 |
139 /** | 135 /// Compares the string part of two versions. This is used for the pre-release |
140 * Compares the string part of two versions. This is used for the pre-release | 136 /// and build version parts. This follows Rule 12. of the Semantic Versioning |
141 * and build version parts. This follows Rule 12. of the Semantic Versioning | 137 /// spec. |
142 * spec. | |
143 */ | |
144 int _compareStrings(String a, String b) { | 138 int _compareStrings(String a, String b) { |
145 var aParts = _splitParts(a); | 139 var aParts = _splitParts(a); |
146 var bParts = _splitParts(b); | 140 var bParts = _splitParts(b); |
147 | 141 |
148 for (int i = 0; i < max(aParts.length, bParts.length); i++) { | 142 for (int i = 0; i < max(aParts.length, bParts.length); i++) { |
149 var aPart = (i < aParts.length) ? aParts[i] : null; | 143 var aPart = (i < aParts.length) ? aParts[i] : null; |
150 var bPart = (i < bParts.length) ? bParts[i] : null; | 144 var bPart = (i < bParts.length) ? bParts[i] : null; |
151 | 145 |
152 if (aPart != bPart) { | 146 if (aPart != bPart) { |
153 // Missing parts come before present ones. | 147 // Missing parts come before present ones. |
(...skipping 14 matching lines...) Expand all Loading... |
168 return 1; | 162 return 1; |
169 } else { | 163 } else { |
170 // Compare two strings. | 164 // Compare two strings. |
171 return aPart.compareTo(bPart); | 165 return aPart.compareTo(bPart); |
172 } | 166 } |
173 } | 167 } |
174 } | 168 } |
175 } | 169 } |
176 } | 170 } |
177 | 171 |
178 /** | 172 /// Splits a string of dot-delimited identifiers into their component parts. |
179 * Splits a string of dot-delimited identifiers into their component parts. | 173 /// Identifiers that are numeric are converted to numbers. |
180 * Identifiers that are numeric are converted to numbers. | |
181 */ | |
182 List _splitParts(String text) { | 174 List _splitParts(String text) { |
183 return text.split('.').map((part) { | 175 return text.split('.').map((part) { |
184 try { | 176 try { |
185 return parseInt(part); | 177 return parseInt(part); |
186 } on FormatException catch (ex) { | 178 } on FormatException catch (ex) { |
187 // Not a number. | 179 // Not a number. |
188 return part; | 180 return part; |
189 } | 181 } |
190 }); | 182 }); |
191 } | 183 } |
192 } | 184 } |
193 | 185 |
194 /** | 186 /// A [VersionConstraint] is a predicate that can determine whether a given |
195 * A [VersionConstraint] is a predicate that can determine whether a given | 187 /// version is valid or not. For example, a ">= 2.0.0" constraint allows any |
196 * version is valid or not. For example, a ">= 2.0.0" constraint allows any | 188 /// version that is "2.0.0" or greater. Version objects themselves implement |
197 * version that is "2.0.0" or greater. Version objects themselves implement | 189 /// this to match a specific version. |
198 * this to match a specific version. | |
199 */ | |
200 abstract class VersionConstraint { | 190 abstract class VersionConstraint { |
201 /** | 191 /// A [VersionConstraint] that allows no versions: i.e. the empty set. |
202 * A [VersionConstraint] that allows no versions: i.e. the empty set. | |
203 */ | |
204 factory VersionConstraint.empty() => const _EmptyVersion(); | 192 factory VersionConstraint.empty() => const _EmptyVersion(); |
205 | 193 |
206 /** | 194 /// Parses a version constraint. This string is a space-separated series of |
207 * Parses a version constraint. This string is a space-separated series of | 195 /// version parts. Each part can be one of: |
208 * version parts. Each part can be one of: | 196 /// |
209 * | 197 /// * A version string like `1.2.3`. In other words, anything that can be |
210 * * A version string like `1.2.3`. In other words, anything that can be | 198 /// parsed by [Version.parse()]. |
211 * parsed by [Version.parse()]. | 199 /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version |
212 * * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a version | 200 /// string. There cannot be a space between the operator and the version. |
213 * string. There cannot be a space between the operator and the version. | 201 /// |
214 * | 202 /// Examples: |
215 * Examples: | 203 /// |
216 * | 204 /// 1.2.3-alpha |
217 * 1.2.3-alpha | 205 /// <=5.1.4 |
218 * <=5.1.4 | 206 /// >2.0.4 <=2.4.6 |
219 * >2.0.4 <=2.4.6 | |
220 */ | |
221 factory VersionConstraint.parse(String text) { | 207 factory VersionConstraint.parse(String text) { |
222 if (text.trim() == '') { | 208 if (text.trim() == '') { |
223 throw new FormatException('Cannot parse an empty string.'); | 209 throw new FormatException('Cannot parse an empty string.'); |
224 } | 210 } |
225 | 211 |
226 // Split it into space-separated parts. | 212 // Split it into space-separated parts. |
227 var constraints = <VersionConstraint>[]; | 213 var constraints = <VersionConstraint>[]; |
228 for (var part in text.split(' ')) { | 214 for (var part in text.split(' ')) { |
229 constraints.add(_parseSingleConstraint(part)); | 215 constraints.add(_parseSingleConstraint(part)); |
230 } | 216 } |
231 | 217 |
232 return new VersionConstraint.intersection(constraints); | 218 return new VersionConstraint.intersection(constraints); |
233 } | 219 } |
234 | 220 |
235 /** | 221 /// Creates a new version constraint that is the intersection of |
236 * Creates a new version constraint that is the intersection of [constraints]. | 222 /// [constraints]. It will only allow versions that all of those constraints |
237 * It will only allow versions that all of those constraints allow. If | 223 /// allow. If constraints is empty, then it returns a VersionConstraint that |
238 * constraints is empty, then it returns a VersionConstraint that allows all | 224 /// allows all versions. |
239 * versions. | |
240 */ | |
241 factory VersionConstraint.intersection( | 225 factory VersionConstraint.intersection( |
242 Collection<VersionConstraint> constraints) { | 226 Collection<VersionConstraint> constraints) { |
243 var constraint = new VersionRange(); | 227 var constraint = new VersionRange(); |
244 for (var other in constraints) { | 228 for (var other in constraints) { |
245 constraint = constraint.intersect(other); | 229 constraint = constraint.intersect(other); |
246 } | 230 } |
247 return constraint; | 231 return constraint; |
248 } | 232 } |
249 | 233 |
250 /** | 234 /// Returns `true` if this constraint allows no versions. |
251 * Returns `true` if this constraint allows no versions. | |
252 */ | |
253 bool get isEmpty; | 235 bool get isEmpty; |
254 | 236 |
255 /** | 237 /// Returns `true` if this constraint allows [version]. |
256 * Returns `true` if this constraint allows [version]. | |
257 */ | |
258 bool allows(Version version); | 238 bool allows(Version version); |
259 | 239 |
260 /** | 240 /// Creates a new [VersionConstraint] that only allows [Version]s allowed by |
261 * Creates a new [VersionConstraint] that only allows [Version]s allowed by | 241 /// both this and [other]. |
262 * both this and [other]. | |
263 */ | |
264 VersionConstraint intersect(VersionConstraint other); | 242 VersionConstraint intersect(VersionConstraint other); |
265 | 243 |
266 static VersionConstraint _parseSingleConstraint(String text) { | 244 static VersionConstraint _parseSingleConstraint(String text) { |
267 if (text == 'any') { | 245 if (text == 'any') { |
268 return new VersionRange(); | 246 return new VersionRange(); |
269 } | 247 } |
270 | 248 |
271 // TODO(rnystrom): Consider other syntaxes for version constraints. This | 249 // TODO(rnystrom): Consider other syntaxes for version constraints. This |
272 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is | 250 // one is whitespace sensitive (you can't do "< 1.2.3") and "<" is |
273 // unfortunately meaningful in YAML, requiring it to be quoted in a | 251 // unfortunately meaningful in YAML, requiring it to be quoted in a |
274 // pubspec. | 252 // pubspec. |
275 // See if it's a comparison operator followed by a version, like ">1.2.3". | 253 // See if it's a comparison operator followed by a version, like ">1.2.3". |
276 var match = new RegExp(r"^([<>]=?)?(.*)$").firstMatch(text); | 254 var match = new RegExp(r"^([<>]=?)?(.*)$").firstMatch(text); |
277 if (match != null) { | 255 if (match != null) { |
278 var comparison = match[1]; | 256 var comparison = match[1]; |
279 var version = new Version.parse(match[2]); | 257 var version = new Version.parse(match[2]); |
280 switch (match[1]) { | 258 switch (match[1]) { |
281 case '<=': return new VersionRange(max: version, includeMax: true); | 259 case '<=': return new VersionRange(max: version, includeMax: true); |
282 case '<': return new VersionRange(max: version, includeMax: false); | 260 case '<': return new VersionRange(max: version, includeMax: false); |
283 case '>=': return new VersionRange(min: version, includeMin: true); | 261 case '>=': return new VersionRange(min: version, includeMin: true); |
284 case '>': return new VersionRange(min: version, includeMin: false); | 262 case '>': return new VersionRange(min: version, includeMin: false); |
285 } | 263 } |
286 } | 264 } |
287 | 265 |
288 // Otherwise, it must be an explicit version. | 266 // Otherwise, it must be an explicit version. |
289 return new Version.parse(text); | 267 return new Version.parse(text); |
290 } | 268 } |
291 } | 269 } |
292 | 270 |
293 /** | 271 /// Constrains versions to a fall within a given range. If there is a minimum, |
294 * Constrains versions to a fall within a given range. If there is a minimum, | 272 /// then this only allows versions that are at that minimum or greater. If there |
295 * then this only allows versions that are at that minimum or greater. If there | 273 /// is a maximum, then only versions less than that are allowed. In other words, |
296 * is a maximum, then only versions less than that are allowed. In other words, | 274 /// this allows `>= min, < max`. |
297 * this allows `>= min, < max`. | |
298 */ | |
299 class VersionRange implements VersionConstraint { | 275 class VersionRange implements VersionConstraint { |
300 final Version min; | 276 final Version min; |
301 final Version max; | 277 final Version max; |
302 final bool includeMin; | 278 final bool includeMin; |
303 final bool includeMax; | 279 final bool includeMax; |
304 | 280 |
305 VersionRange({this.min, this.max, | 281 VersionRange({this.min, this.max, |
306 this.includeMin: false, this.includeMax: false}) { | 282 this.includeMin: false, this.includeMax: false}) { |
307 if (min != null && max != null && min > max) { | 283 if (min != null && max != null && min > max) { |
308 throw new ArgumentError( | 284 throw new ArgumentError( |
309 'Minimum version ("$min") must be less than maximum ("$max").'); | 285 'Minimum version ("$min") must be less than maximum ("$max").'); |
310 } | 286 } |
311 } | 287 } |
312 | 288 |
313 bool operator ==(other) { | 289 bool operator ==(other) { |
314 if (other is! VersionRange) return false; | 290 if (other is! VersionRange) return false; |
315 | 291 |
316 return min == other.min && | 292 return min == other.min && |
317 max == other.max && | 293 max == other.max && |
318 includeMin == other.includeMin && | 294 includeMin == other.includeMin && |
319 includeMax == other.includeMax; | 295 includeMax == other.includeMax; |
320 } | 296 } |
321 | 297 |
322 bool get isEmpty => false; | 298 bool get isEmpty => false; |
323 | 299 |
324 /** Tests if [other] matches falls within this version range. */ | 300 /// Tests if [other] matches falls within this version range. |
325 bool allows(Version other) { | 301 bool allows(Version other) { |
326 if (min != null && other < min) return false; | 302 if (min != null && other < min) return false; |
327 if (min != null && !includeMin && other == min) return false; | 303 if (min != null && !includeMin && other == min) return false; |
328 if (max != null && other > max) return false; | 304 if (max != null && other > max) return false; |
329 if (max != null && !includeMax && other == max) return false; | 305 if (max != null && !includeMax && other == max) return false; |
330 return true; | 306 return true; |
331 } | 307 } |
332 | 308 |
333 VersionConstraint intersect(VersionConstraint other) { | 309 VersionConstraint intersect(VersionConstraint other) { |
334 if (other.isEmpty) return other; | 310 if (other.isEmpty) return other; |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
412 } | 388 } |
413 | 389 |
414 class _EmptyVersion implements VersionConstraint { | 390 class _EmptyVersion implements VersionConstraint { |
415 const _EmptyVersion(); | 391 const _EmptyVersion(); |
416 | 392 |
417 bool get isEmpty => true; | 393 bool get isEmpty => true; |
418 bool allows(Version other) => false; | 394 bool allows(Version other) => false; |
419 VersionConstraint intersect(VersionConstraint other) => this; | 395 VersionConstraint intersect(VersionConstraint other) => this; |
420 String toString() => '<empty>'; | 396 String toString() => '<empty>'; |
421 } | 397 } |
OLD | NEW |