| // Copyright (c) 2017, 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 'package:pub_semver/pub_semver.dart'; | |
| import '../package_name.dart'; | |
| import 'set_relation.dart'; | |
| /// A statement about a package which is true or false for a given selection of | |
| /// package versions. | |
| /// | |
| /// See https://github.com/dart-lang/pub/tree/main/doc/solver.md#term. | |
| class Term { | |
| /// Whether the term is positive or not. | |
| /// | |
| /// A positive constraint is true when a package version that matches | |
| /// [package] is selected; a negative constraint is true when no package | |
| /// versions that match [package] are selected. | |
| final bool isPositive; | |
| /// The range of package versions referred to by this term. | |
| final PackageRange package; | |
| /// A copy of this term with the opposite [isPositive] value. | |
| Term get inverse => Term(package, !isPositive); | |
| Term(this.package, this.isPositive); | |
| VersionConstraint get constraint => package.constraint; | |
| /// Returns whether `this` satisfies [other]. | |
| /// | |
| /// That is, whether `this` being true means that [other] must also be true. | |
| bool satisfies(Term other) => | |
| package.name == other.package.name && | |
| relation(other) == SetRelation.subset; | |
| /// Returns the relationship between the package versions allowed by `this` | |
| /// and by [other]. | |
| /// | |
| /// Throws an [ArgumentError] if [other] doesn't refer to a package with the | |
| /// same name as [package]. | |
| SetRelation relation(Term other) { | |
| if (package.name != other.package.name) { | |
| throw ArgumentError.value( | |
| other, | |
| 'other', | |
| 'should refer to package ${package.name}', | |
| ); | |
| } | |
| final otherConstraint = other.constraint; | |
| if (other.isPositive) { | |
| if (isPositive) { | |
| // foo from hosted is disjoint with foo from git | |
| if (!_compatiblePackage(other.package)) return SetRelation.disjoint; | |
| // foo ^1.5.0 is a subset of foo ^1.0.0 | |
| if (otherConstraint.allowsAll(constraint)) return SetRelation.subset; | |
| // foo ^2.0.0 is disjoint with foo ^1.0.0 | |
| if (!constraint.allowsAny(otherConstraint)) { | |
| return SetRelation.disjoint; | |
| } | |
| // foo >=1.5.0 <3.0.0 overlaps foo ^1.0.0 | |
| return SetRelation.overlapping; | |
| } else { | |
| // not foo from hosted is a superset foo from git | |
| if (!_compatiblePackage(other.package)) return SetRelation.overlapping; | |
| // not foo ^1.0.0 is disjoint with foo ^1.5.0 | |
| if (constraint.allowsAll(otherConstraint)) { | |
| return SetRelation.disjoint; | |
| } | |
| // not foo ^1.5.0 overlaps foo ^1.0.0 | |
| // not foo ^2.0.0 is a superset of foo ^1.5.0 | |
| return SetRelation.overlapping; | |
| } | |
| } else { | |
| if (isPositive) { | |
| // foo from hosted is a subset of not foo from git | |
| if (!_compatiblePackage(other.package)) return SetRelation.subset; | |
| // foo ^2.0.0 is a subset of not foo ^1.0.0 | |
| if (!otherConstraint.allowsAny(constraint)) return SetRelation.subset; | |
| // foo ^1.5.0 is disjoint with not foo ^1.0.0 | |
| if (otherConstraint.allowsAll(constraint)) return SetRelation.disjoint; | |
| // foo ^1.0.0 overlaps not foo ^1.5.0 | |
| return SetRelation.overlapping; | |
| } else { | |
| // not foo from hosted overlaps not foo from git | |
| if (!_compatiblePackage(other.package)) return SetRelation.overlapping; | |
| // not foo ^1.0.0 is a subset of not foo ^1.5.0 | |
| if (constraint.allowsAll(otherConstraint)) return SetRelation.subset; | |
| // not foo ^2.0.0 overlaps not foo ^1.0.0 | |
| // not foo ^1.5.0 is a superset of not foo ^1.0.0 | |
| return SetRelation.overlapping; | |
| } | |
| } | |
| } | |
| /// Returns a [Term] that represents the packages allowed by both `this` and | |
| /// [other]. | |
| /// | |
| /// If there is no such single [Term], for example because `this` is | |
| /// incompatible with [other], returns `null`. | |
| /// | |
| /// Throws an [ArgumentError] if [other] doesn't refer to a package with the | |
| /// same name as [package]. | |
| Term? intersect(Term other) { | |
| if (package.name != other.package.name) { | |
| throw ArgumentError.value( | |
| other, | |
| 'other', | |
| 'should refer to package ${package.name}', | |
| ); | |
| } | |
| if (_compatiblePackage(other.package)) { | |
| if (isPositive != other.isPositive) { | |
| // foo ^1.0.0 ∩ not foo ^1.5.0 → foo >=1.0.0 <1.5.0 | |
| final positive = isPositive ? this : other; | |
| final negative = isPositive ? other : this; | |
| return _nonEmptyTerm( | |
| positive.constraint.difference(negative.constraint), | |
| true, | |
| ); | |
| } else if (isPositive) { | |
| // foo ^1.0.0 ∩ foo >=1.5.0 <3.0.0 → foo ^1.5.0 | |
| return _nonEmptyTerm(constraint.intersect(other.constraint), true); | |
| } else { | |
| // not foo ^1.0.0 ∩ not foo >=1.5.0 <3.0.0 → not foo >=1.0.0 <3.0.0 | |
| return _nonEmptyTerm(constraint.union(other.constraint), false); | |
| } | |
| } else if (isPositive != other.isPositive) { | |
| // foo from git ∩ not foo from hosted → foo from git | |
| return isPositive ? this : other; | |
| } else { | |
| // foo from git ∩ foo from hosted → empty | |
| // not foo from git ∩ not foo from hosted → no single term | |
| return null; | |
| } | |
| } | |
| /// Returns a [Term] that represents packages allowed by `this` and not by | |
| /// [other]. | |
| /// | |
| /// If there is no such single [Term], for example because all packages | |
| /// allowed by `this` are allowed by [other], returns `null`. | |
| /// | |
| /// Throws an [ArgumentError] if [other] doesn't refer to a package with the | |
| /// same name as [package]. | |
| Term? difference(Term other) => intersect(other.inverse); // A ∖ B → A ∩ not B | |
| /// Returns whether [other] is compatible with [package]. | |
| bool _compatiblePackage(PackageRange other) { | |
| return package.isRoot || other.isRoot || other.toRef() == package.toRef(); | |
| } | |
| /// Returns a new [Term] with the same package as `this` and with | |
| /// [constraint], unless that would produce a term that allows no packages, | |
| /// in which case this returns `null`. | |
| Term? _nonEmptyTerm(VersionConstraint constraint, bool isPositive) => | |
| constraint.isEmpty | |
| ? null | |
| : Term(package.toRef().withConstraint(constraint), isPositive); | |
| @override | |
| String toString() => "${isPositive ? '' : 'not '}$package"; | |
| } |