| // 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 'patterns.dart'; | |
| import 'utils.dart'; | |
| import 'version.dart'; | |
| import 'version_range.dart'; | |
| import 'version_union.dart'; | |
| /// A [VersionConstraint] is a predicate that can determine whether a given | |
| /// version is valid or not. | |
| /// | |
| /// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or | |
| /// greater. Version objects themselves implement this to match a specific | |
| /// version. | |
| abstract class VersionConstraint { | |
| /// A [VersionConstraint] that allows all versions. | |
| /// | |
| /// **Note:** This property shouldn't be reassigned. | |
| /// It will be made `final` in a future release. | |
| static VersionConstraint any = VersionRange(); | |
| /// A [VersionConstraint] that allows no versions -- the empty set. | |
| /// | |
| /// **Note:** This property shouldn't be reassigned. | |
| /// It will be made `final` in a future release. | |
| static VersionConstraint empty = const _EmptyVersion(); | |
| /// Parses a version constraint. | |
| /// | |
| /// This string is one of: | |
| /// | |
| /// * "any". [any] version. | |
| /// * "^" followed by a version string. Versions compatible with | |
| /// ([VersionConstraint.compatibleWith]) the version. | |
| /// * a series of version parts. Each part can be one of: | |
| /// * A version string like `1.2.3`. In other words, anything that can be | |
| /// parsed by [Version.parse()]. | |
| /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a | |
| /// version string. | |
| /// | |
| /// Whitespace is ignored. | |
| /// | |
| /// Examples: | |
| /// | |
| /// any | |
| /// ^0.7.2 | |
| /// ^1.0.0-alpha | |
| /// 1.2.3-alpha | |
| /// <=5.1.4 | |
| /// >2.0.4 <= 2.4.6 | |
| factory VersionConstraint.parse(String text) { | |
| var originalText = text; | |
| void skipWhitespace() { | |
| text = text.trim(); | |
| } | |
| skipWhitespace(); | |
| // Handle the "any" constraint. | |
| if (text == 'any') return any; | |
| // Try to parse and consume a version number. | |
| Version? matchVersion() { | |
| var version = startVersion.firstMatch(text); | |
| if (version == null) return null; | |
| text = text.substring(version.end); | |
| return Version.parse(version[0]!); | |
| } | |
| // Try to parse and consume a comparison operator followed by a version. | |
| VersionRange? matchComparison() { | |
| var comparison = startComparison.firstMatch(text); | |
| if (comparison == null) return null; | |
| var op = comparison[0]!; | |
| text = text.substring(comparison.end); | |
| skipWhitespace(); | |
| var version = matchVersion(); | |
| if (version == null) { | |
| throw FormatException('Expected version number after "$op" in ' | |
| '"$originalText", got "$text".'); | |
| } | |
| return switch (op) { | |
| '<=' => VersionRange(max: version, includeMax: true), | |
| '<' => VersionRange(max: version, alwaysIncludeMaxPreRelease: true), | |
| '>=' => VersionRange(min: version, includeMin: true), | |
| '>' => VersionRange(min: version), | |
| _ => throw UnsupportedError(op), | |
| }; | |
| } | |
| // Try to parse the "^" operator followed by a version. | |
| VersionConstraint? matchCompatibleWith() { | |
| if (!text.startsWith(compatibleWithChar)) return null; | |
| text = text.substring(compatibleWithChar.length); | |
| skipWhitespace(); | |
| var version = matchVersion(); | |
| if (version == null) { | |
| throw FormatException('Expected version number after ' | |
| '"$compatibleWithChar" in "$originalText", got "$text".'); | |
| } | |
| if (text.isNotEmpty) { | |
| throw FormatException('Cannot include other constraints with ' | |
| '"$compatibleWithChar" constraint in "$originalText".'); | |
| } | |
| return VersionConstraint.compatibleWith(version); | |
| } | |
| var compatibleWith = matchCompatibleWith(); | |
| if (compatibleWith != null) return compatibleWith; | |
| Version? min; | |
| var includeMin = false; | |
| Version? max; | |
| var includeMax = false; | |
| for (;;) { | |
| skipWhitespace(); | |
| if (text.isEmpty) break; | |
| var newRange = matchVersion() ?? matchComparison(); | |
| if (newRange == null) { | |
| throw FormatException('Could not parse version "$originalText". ' | |
| 'Unknown text at "$text".'); | |
| } | |
| if (newRange.min != null) { | |
| if (min == null || newRange.min! > min) { | |
| min = newRange.min; | |
| includeMin = newRange.includeMin; | |
| } else if (newRange.min == min && !newRange.includeMin) { | |
| includeMin = false; | |
| } | |
| } | |
| if (newRange.max != null) { | |
| if (max == null || newRange.max! < max) { | |
| max = newRange.max; | |
| includeMax = newRange.includeMax; | |
| } else if (newRange.max == max && !newRange.includeMax) { | |
| includeMax = false; | |
| } | |
| } | |
| } | |
| if (min == null && max == null) { | |
| throw const FormatException('Cannot parse an empty string.'); | |
| } | |
| if (min != null && max != null) { | |
| if (min > max) return VersionConstraint.empty; | |
| if (min == max) { | |
| if (includeMin && includeMax) return min; | |
| return VersionConstraint.empty; | |
| } | |
| } | |
| return VersionRange( | |
| min: min, includeMin: includeMin, max: max, includeMax: includeMax); | |
| } | |
| /// Creates a version constraint which allows all versions that are | |
| /// backward compatible with [version]. | |
| /// | |
| /// Versions are considered backward compatible with [version] if they | |
| /// are greater than or equal to [version], but less than the next breaking | |
| /// version ([Version.nextBreaking]) of [version]. | |
| factory VersionConstraint.compatibleWith(Version version) => | |
| CompatibleWithVersionRange(version); | |
| /// Creates a new version constraint that is the intersection of | |
| /// [constraints]. | |
| /// | |
| /// It only allows versions that all of those constraints allow. If | |
| /// constraints is empty, then it returns a VersionConstraint that allows | |
| /// all versions. | |
| factory VersionConstraint.intersection( | |
| Iterable<VersionConstraint> constraints) { | |
| VersionConstraint constraint = VersionRange(); | |
| for (var other in constraints) { | |
| constraint = constraint.intersect(other); | |
| // Once the intersection is empty it cannot become non-empty again. | |
| if (constraint.isEmpty) return VersionConstraint.empty; | |
| } | |
| return constraint; | |
| } | |
| /// Creates a new version constraint that is the union of [constraints]. | |
| /// | |
| /// It allows any versions that any of those constraints allows. If | |
| /// [constraints] is empty, this returns a constraint that allows no versions. | |
| factory VersionConstraint.unionOf(Iterable<VersionConstraint> constraints) { | |
| var flattened = constraints.expand((constraint) { | |
| if (constraint.isEmpty) return <VersionRange>[]; | |
| if (constraint is VersionUnion) return constraint.ranges; | |
| if (constraint is VersionRange) return [constraint]; | |
| throw ArgumentError('Unknown VersionConstraint type $constraint.'); | |
| }).toList(); | |
| if (flattened.isEmpty) return VersionConstraint.empty; | |
| if (flattened.any((constraint) => constraint.isAny)) { | |
| return VersionConstraint.any; | |
| } | |
| flattened.sort(); | |
| var merged = <VersionRange>[]; | |
| for (var constraint in flattened) { | |
| // Merge this constraint with the previous one, but only if they touch. | |
| if (merged.isEmpty || | |
| (!merged.last.allowsAny(constraint) && | |
| !areAdjacent(merged.last, constraint))) { | |
| merged.add(constraint); | |
| } else { | |
| merged[merged.length - 1] = | |
| merged.last.union(constraint) as VersionRange; | |
| } | |
| } | |
| if (merged.length == 1) return merged.single; | |
| return VersionUnion.fromRanges(merged); | |
| } | |
| /// Returns `true` if this constraint allows no versions. | |
| bool get isEmpty; | |
| /// Returns `true` if this constraint allows all versions. | |
| bool get isAny; | |
| /// Returns `true` if this constraint allows [version]. | |
| bool allows(Version version); | |
| /// Returns `true` if this constraint allows all the versions that [other] | |
| /// allows. | |
| bool allowsAll(VersionConstraint other); | |
| /// Returns `true` if this constraint allows any of the versions that [other] | |
| /// allows. | |
| bool allowsAny(VersionConstraint other); | |
| /// Returns a [VersionConstraint] that only allows [Version]s allowed by both | |
| /// this and [other]. | |
| VersionConstraint intersect(VersionConstraint other); | |
| /// Returns a [VersionConstraint] that allows [Version]s allowed by either | |
| /// this or [other]. | |
| VersionConstraint union(VersionConstraint other); | |
| /// Returns a [VersionConstraint] that allows [Version]s allowed by this but | |
| /// not [other]. | |
| VersionConstraint difference(VersionConstraint other); | |
| } | |
| class _EmptyVersion implements VersionConstraint { | |
| const _EmptyVersion(); | |
| @override | |
| bool get isEmpty => true; | |
| @override | |
| bool get isAny => false; | |
| @override | |
| bool allows(Version other) => false; | |
| @override | |
| bool allowsAll(VersionConstraint other) => other.isEmpty; | |
| @override | |
| bool allowsAny(VersionConstraint other) => false; | |
| @override | |
| VersionConstraint intersect(VersionConstraint other) => this; | |
| @override | |
| VersionConstraint union(VersionConstraint other) => other; | |
| @override | |
| VersionConstraint difference(VersionConstraint other) => this; | |
| @override | |
| String toString() => '<empty>'; | |
| } |