blob: 78cd6d0269864c972cceeb8783a70b099a80df11 [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'utils.dart';
import 'version.dart';
import 'version_constraint.dart';
import 'version_union.dart';
/// Constrains versions to a fall within a given range.
///
/// If there is a minimum, then this only allows versions that are at that
/// minimum or greater. If there is a maximum, then only versions less than
/// that are allowed. In other words, this allows `>= min, < max`.
///
/// Version ranges are ordered first by their lower bounds, then by their upper
/// bounds. For example, `>=1.0.0 <2.0.0` is before `>=1.5.0 <2.0.0` is before
/// `>=1.5.0 <3.0.0`.
class VersionRange implements Comparable<VersionRange>, VersionConstraint {
/// The minimum end of the range.
///
/// If [includeMin] is `true`, this will be the minimum allowed version.
/// Otherwise, it will be the highest version below the range that is not
/// allowed.
///
/// This may be `null` in which case the range has no minimum end and allows
/// any version less than the maximum.
final Version min;
/// The maximum end of the range.
///
/// If [includeMax] is `true`, this will be the maximum allowed version.
/// Otherwise, it will be the lowest version above the range that is not
/// allowed.
///
/// This may be `null` in which case the range has no maximum end and allows
/// any version greater than the minimum.
final Version max;
/// If `true` then [min] is allowed by the range.
final bool includeMin;
/// If `true`, then [max] is allowed by the range.
final bool includeMax;
/// Creates a new version range from [min] to [max], either inclusive or
/// exclusive.
///
/// If it is an error if [min] is greater than [max].
///
/// Either [max] or [min] may be omitted to not clamp the range at that end.
/// If both are omitted, the range allows all versions.
///
/// If [includeMin] is `true`, then the minimum end of the range is inclusive.
/// Likewise, passing [includeMax] as `true` makes the upper end inclusive.
///
/// If [alwaysIncludeMaxPreRelease] is `true`, this will always include
/// pre-release versions of an exclusive [max]. Otherwise, it will use the
/// default behavior for pre-release versions of [max].
factory VersionRange(
{Version min,
Version max,
bool includeMin = false,
bool includeMax = false,
bool alwaysIncludeMaxPreRelease = false}) {
if (min != null && max != null && min > max) {
throw ArgumentError(
'Minimum version ("$min") must be less than maximum ("$max").');
}
if (!alwaysIncludeMaxPreRelease &&
!includeMax &&
max != null &&
!max.isPreRelease &&
max.build.isEmpty &&
(min == null ||
!min.isPreRelease ||
!equalsWithoutPreRelease(min, max))) {
max = max.firstPreRelease;
}
return VersionRange._(min, max, includeMin, includeMax);
}
VersionRange._(this.min, this.max, this.includeMin, this.includeMax);
@override
bool operator ==(other) {
if (other is! VersionRange) return false;
return min == other.min &&
max == other.max &&
includeMin == other.includeMin &&
includeMax == other.includeMax;
}
@override
int get hashCode =>
min.hashCode ^
(max.hashCode * 3) ^
(includeMin.hashCode * 5) ^
(includeMax.hashCode * 7);
@override
bool get isEmpty => false;
@override
bool get isAny => min == null && max == null;
/// Tests if [other] falls within this version range.
@override
bool allows(Version other) {
if (min != null) {
if (other < min) return false;
if (!includeMin && other == min) return false;
}
if (max != null) {
if (other > max) return false;
if (!includeMax && other == max) return false;
}
return true;
}
@override
bool allowsAll(VersionConstraint other) {
if (other.isEmpty) return true;
if (other is Version) return allows(other);
if (other is VersionUnion) {
return other.ranges.every(allowsAll);
}
if (other is VersionRange) {
return !allowsLower(other, this) && !allowsHigher(other, this);
}
throw ArgumentError('Unknown VersionConstraint type $other.');
}
@override
bool allowsAny(VersionConstraint other) {
if (other.isEmpty) return false;
if (other is Version) return allows(other);
if (other is VersionUnion) {
return other.ranges.any(allowsAny);
}
if (other is VersionRange) {
return !strictlyLower(other, this) && !strictlyHigher(other, this);
}
throw ArgumentError('Unknown VersionConstraint type $other.');
}
@override
VersionConstraint intersect(VersionConstraint other) {
if (other.isEmpty) return other;
if (other is VersionUnion) return other.intersect(this);
// A range and a Version just yields the version if it's in the range.
if (other is Version) {
return allows(other) ? other : VersionConstraint.empty;
}
if (other is VersionRange) {
// Intersect the two ranges.
Version intersectMin;
bool intersectIncludeMin;
if (allowsLower(this, other)) {
if (strictlyLower(this, other)) return VersionConstraint.empty;
intersectMin = other.min;
intersectIncludeMin = other.includeMin;
} else {
if (strictlyLower(other, this)) return VersionConstraint.empty;
intersectMin = min;
intersectIncludeMin = includeMin;
}
Version intersectMax;
bool intersectIncludeMax;
if (allowsHigher(this, other)) {
intersectMax = other.max;
intersectIncludeMax = other.includeMax;
} else {
intersectMax = max;
intersectIncludeMax = includeMax;
}
if (intersectMin == null && intersectMax == null) {
// Open range.
return VersionRange();
}
// If the range is just a single version.
if (intersectMin == intersectMax) {
// Because we already verified that the lower range isn't strictly
// lower, there must be some overlap.
assert(intersectIncludeMin && intersectIncludeMax);
return intersectMin;
}
// If we got here, there is an actual range.
return VersionRange(
min: intersectMin,
max: intersectMax,
includeMin: intersectIncludeMin,
includeMax: intersectIncludeMax,
alwaysIncludeMaxPreRelease: true);
}
throw ArgumentError('Unknown VersionConstraint type $other.');
}
@override
VersionConstraint union(VersionConstraint other) {
if (other is Version) {
if (allows(other)) return this;
if (other == min) {
return VersionRange(
min: min,
max: max,
includeMin: true,
includeMax: includeMax,
alwaysIncludeMaxPreRelease: true);
}
if (other == max) {
return VersionRange(
min: min,
max: max,
includeMin: includeMin,
includeMax: true,
alwaysIncludeMaxPreRelease: true);
}
return VersionConstraint.unionOf([this, other]);
}
if (other is VersionRange) {
// If the two ranges don't overlap, we won't be able to create a single
// VersionRange for both of them.
var edgesTouch = (max != null &&
max == other.min &&
(includeMax || other.includeMin)) ||
(min != null && min == other.max && (includeMin || other.includeMax));
if (!edgesTouch && !allowsAny(other)) {
return VersionConstraint.unionOf([this, other]);
}
Version unionMin;
bool unionIncludeMin;
if (allowsLower(this, other)) {
unionMin = min;
unionIncludeMin = includeMin;
} else {
unionMin = other.min;
unionIncludeMin = other.includeMin;
}
Version unionMax;
bool unionIncludeMax;
if (allowsHigher(this, other)) {
unionMax = max;
unionIncludeMax = includeMax;
} else {
unionMax = other.max;
unionIncludeMax = other.includeMax;
}
return VersionRange(
min: unionMin,
max: unionMax,
includeMin: unionIncludeMin,
includeMax: unionIncludeMax,
alwaysIncludeMaxPreRelease: true);
}
return VersionConstraint.unionOf([this, other]);
}
@override
VersionConstraint difference(VersionConstraint other) {
if (other.isEmpty) return this;
if (other is Version) {
if (!allows(other)) return this;
if (other == min) {
if (!includeMin) return this;
return VersionRange(
min: min,
max: max,
includeMin: false,
includeMax: includeMax,
alwaysIncludeMaxPreRelease: true);
}
if (other == max) {
if (!includeMax) return this;
return VersionRange(
min: min,
max: max,
includeMin: includeMin,
includeMax: false,
alwaysIncludeMaxPreRelease: true);
}
return VersionUnion.fromRanges([
VersionRange(
min: min,
max: other,
includeMin: includeMin,
includeMax: false,
alwaysIncludeMaxPreRelease: true),
VersionRange(
min: other,
max: max,
includeMin: false,
includeMax: includeMax,
alwaysIncludeMaxPreRelease: true)
]);
} else if (other is VersionRange) {
if (!allowsAny(other)) return this;
VersionRange before;
if (!allowsLower(this, other)) {
before = null;
} else if (min == other.min) {
assert(includeMin && !other.includeMin);
assert(min != null);
before = min;
} else {
before = VersionRange(
min: min,
max: other.min,
includeMin: includeMin,
includeMax: !other.includeMin,
alwaysIncludeMaxPreRelease: true);
}
VersionRange after;
if (!allowsHigher(this, other)) {
after = null;
} else if (max == other.max) {
assert(includeMax && !other.includeMax);
assert(max != null);
after = max;
} else {
after = VersionRange(
min: other.max,
max: max,
includeMin: !other.includeMax,
includeMax: includeMax,
alwaysIncludeMaxPreRelease: true);
}
if (before == null && after == null) return VersionConstraint.empty;
if (before == null) return after;
if (after == null) return before;
return VersionUnion.fromRanges([before, after]);
} else if (other is VersionUnion) {
var ranges = <VersionRange>[];
var current = this;
for (var range in other.ranges) {
// Skip any ranges that are strictly lower than [current].
if (strictlyLower(range, current)) continue;
// If we reach a range strictly higher than [current], no more ranges
// will be relevant so we can bail early.
if (strictlyHigher(range, current)) break;
var difference = current.difference(range);
if (difference.isEmpty) {
return VersionConstraint.empty;
} else if (difference is VersionUnion) {
// If [range] split [current] in half, we only need to continue
// checking future ranges against the latter half.
assert(difference.ranges.length == 2);
ranges.add(difference.ranges.first);
current = difference.ranges.last;
} else {
current = difference as VersionRange;
}
}
if (ranges.isEmpty) return current;
return VersionUnion.fromRanges(ranges..add(current));
}
throw ArgumentError('Unknown VersionConstraint type $other.');
}
@override
int compareTo(VersionRange other) {
if (min == null) {
if (other.min == null) return _compareMax(other);
return -1;
} else if (other.min == null) {
return 1;
}
var result = min.compareTo(other.min);
if (result != 0) return result;
if (includeMin != other.includeMin) return includeMin ? -1 : 1;
return _compareMax(other);
}
/// Compares the maximum values of `this` and [other].
int _compareMax(VersionRange other) {
if (max == null) {
if (other.max == null) return 0;
return 1;
} else if (other.max == null) {
return -1;
}
var result = max.compareTo(other.max);
if (result != 0) return result;
if (includeMax != other.includeMax) return includeMax ? 1 : -1;
return 0;
}
@override
String toString() {
var buffer = StringBuffer();
if (min != null) {
buffer..write(includeMin ? '>=' : '>')..write(min);
}
if (max != null) {
if (min != null) buffer.write(' ');
if (includeMax) {
buffer..write('<=')..write(max);
} else {
buffer.write('<');
if (max.isFirstPreRelease) {
// Since `"<$max"` would parse the same as `"<$max-0"`, we just emit
// `<$max` to avoid confusing "-0" suffixes.
buffer.write('${max.major}.${max.minor}.${max.patch}');
} else {
buffer.write(max);
// If `">=$min <$max"` would parse as `">=$min <$max-0"`, add `-*` to
// indicate that actually does allow pre-release versions.
var minIsPreReleaseOfMax = min != null &&
min.isPreRelease &&
equalsWithoutPreRelease(min, max);
if (!max.isPreRelease && max.build.isEmpty && !minIsPreReleaseOfMax) {
buffer.write('-∞');
}
}
}
}
if (min == null && max == null) buffer.write('any');
return buffer.toString();
}
}
class CompatibleWithVersionRange extends VersionRange {
CompatibleWithVersionRange(Version version)
: super._(version, version.nextBreaking.firstPreRelease, true, false);
@override
String toString() => '^$min';
}