Drop upper bound instead of using "any" while resolving in "pub outdated" (#2623)
* Initial commit
* Updated as per comments
* Fixed documentation typo
* Fix test typo
* Updated stripVersionUpperBounds upgradeOnly -> stripOnly
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 22bb17e..c29f643 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -19,8 +19,8 @@
import '../package.dart';
import '../package_name.dart';
import '../pubspec.dart';
+import '../pubspec_utils.dart';
import '../solver.dart';
-import '../source/hosted.dart';
import '../system_cache.dart';
import '../utils.dart';
@@ -94,13 +94,13 @@
final rootPubspec = includeDependencyOverrides
? entrypoint.root.pubspec
- : _stripDependencyOverrides(entrypoint.root.pubspec);
+ : stripDependencyOverrides(entrypoint.root.pubspec);
final upgradablePubspec = includeDevDependencies
? rootPubspec
: stripDevDependencies(rootPubspec);
- final resolvablePubspec = _stripVersionConstraints(upgradablePubspec);
+ final resolvablePubspec = stripVersionUpperBounds(upgradablePubspec);
List<PackageId> upgradablePackages;
List<PackageId> resolvablePackages;
@@ -322,71 +322,15 @@
}
}
-/// Try to solve [pubspec] return [PackageId]'s in the resolution or `null`.
+/// Try to solve [pubspec] return [PackageId]s in the resolution or `[]`.
Future<List<PackageId>> _tryResolve(Pubspec pubspec, SystemCache cache) async {
- try {
- return (await resolveVersions(
- SolveType.UPGRADE,
- cache,
- Package.inMemory(pubspec),
- ))
- .packages;
- } on SolveFailure {
+ final solveResult = await tryResolveVersions(
+ SolveType.UPGRADE, cache, Package.inMemory(pubspec));
+ if (solveResult == null) {
return [];
}
-}
-Pubspec stripDevDependencies(Pubspec original) {
- return Pubspec(
- original.name,
- version: original.version,
- sdkConstraints: original.sdkConstraints,
- dependencies: original.dependencies.values,
- devDependencies: [], // explicitly give empty list, to prevent lazy parsing
- dependencyOverrides: original.dependencyOverrides.values,
- );
-}
-
-Pubspec _stripDependencyOverrides(Pubspec original) {
- return Pubspec(
- original.name,
- version: original.version,
- sdkConstraints: original.sdkConstraints,
- dependencies: original.dependencies.values,
- devDependencies: original.devDependencies.values,
- dependencyOverrides: [],
- );
-}
-
-/// Returns new pubspec with the same dependencies as [original] but with no
-/// version constraints on hosted packages.
-Pubspec _stripVersionConstraints(Pubspec original) {
- List<PackageRange> _unconstrained(Map<String, PackageRange> constrained) {
- final result = <PackageRange>[];
- for (final name in constrained.keys) {
- final packageRange = constrained[name];
- var unconstrainedRange = packageRange;
- if (packageRange.source is HostedSource) {
- unconstrainedRange = PackageRange(
- packageRange.name,
- packageRange.source,
- VersionConstraint.any,
- packageRange.description,
- features: packageRange.features);
- }
- result.add(unconstrainedRange);
- }
- return result;
- }
-
- return Pubspec(
- original.name,
- version: original.version,
- sdkConstraints: original.sdkConstraints,
- dependencies: _unconstrained(original.dependencies),
- devDependencies: _unconstrained(original.devDependencies),
- dependencyOverrides: original.dependencyOverrides.values,
- );
+ return solveResult.packages;
}
Future<void> _outputJson(
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
new file mode 100644
index 0000000..032ce63
--- /dev/null
+++ b/lib/src/pubspec_utils.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2020, 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:meta/meta.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'package_name.dart';
+import 'pubspec.dart';
+import 'source/hosted.dart';
+
+/// Returns a new [Pubspec] without [original]'s dev_dependencies.
+Pubspec stripDevDependencies(Pubspec original) {
+ ArgumentError.checkNotNull(original, 'original');
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: original.dependencies.values,
+ devDependencies: [], // explicitly give empty list, to prevent lazy parsing
+ dependencyOverrides: original.dependencyOverrides.values,
+ );
+}
+
+/// Returns a new [Pubspec] without [original]'s dependency_overrides.
+Pubspec stripDependencyOverrides(Pubspec original) {
+ ArgumentError.checkNotNull(original, 'original');
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: original.dependencies.values,
+ devDependencies: original.devDependencies.values,
+ dependencyOverrides: [],
+ );
+}
+
+/// Returns new pubspec with the same dependencies as [original] but with the
+/// upper bounds of the constraints removed.
+///
+/// If [stripOnly] is provided, only the packages whose names are in
+/// [stripOnly] will have their upper bounds removed. If [stripOnly] is
+/// not specified or empty, then all packages will have their upper bounds
+/// removed.
+Pubspec stripVersionUpperBounds(Pubspec original,
+ {Iterable<String> stripOnly}) {
+ ArgumentError.checkNotNull(original, 'original');
+ stripOnly ??= [];
+
+ List<PackageRange> _stripUpperBounds(
+ Map<String, PackageRange> constrained,
+ ) {
+ final result = <PackageRange>[];
+
+ for (final name in constrained.keys) {
+ final packageRange = constrained[name];
+ var unconstrainedRange = packageRange;
+
+ /// We only need to remove the upper bound if it is a hosted package.
+ if (packageRange.source is HostedSource &&
+ (stripOnly.isEmpty || stripOnly.contains(packageRange.name))) {
+ unconstrainedRange = PackageRange(
+ packageRange.name,
+ packageRange.source,
+ stripUpperBound(packageRange.constraint),
+ packageRange.description,
+ features: packageRange.features);
+ }
+ result.add(unconstrainedRange);
+ }
+
+ return result;
+ }
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: _stripUpperBounds(original.dependencies),
+ devDependencies: _stripUpperBounds(original.devDependencies),
+ dependencyOverrides: original.dependencyOverrides.values,
+ );
+}
+
+/// Removes the upper bound of [constraint]. If [constraint] is the
+/// empty version constraint, [VersionConstraint.empty] will be returned.
+@visibleForTesting
+VersionConstraint stripUpperBound(VersionConstraint constraint) {
+ ArgumentError.checkNotNull(constraint, 'constraint');
+
+ /// A [VersionConstraint] has to either be a [VersionRange], [VersionUnion],
+ /// or the empty [VersionConstraint].
+ if (constraint is VersionRange) {
+ return VersionRange(min: constraint.min, includeMin: constraint.includeMin);
+ }
+
+ if (constraint is VersionUnion) {
+ if (constraint.ranges.isEmpty) return VersionConstraint.empty;
+
+ final firstRange = constraint.ranges.first;
+ return VersionRange(min: firstRange.min, includeMin: firstRange.includeMin);
+ }
+
+ assert(constraint == VersionConstraint.empty, 'unknown constraint type');
+
+ /// If it gets here, [constraint] is the empty version constraint, so we
+ /// just return an empty version constraint.
+ return VersionConstraint.empty;
+}
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index d7230c1..e5ddf55 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -6,6 +6,7 @@
import 'lock_file.dart';
import 'package.dart';
+import 'solver/failure.dart';
import 'solver/result.dart';
import 'solver/type.dart';
import 'solver/version_solver.dart';
@@ -36,3 +37,27 @@
useLatest ?? const [],
).solve();
}
+
+/// Attempts to select the best concrete versions for all of the transitive
+/// dependencies of [root] taking into account all of the [VersionConstraint]s
+/// that those dependencies place on each other and the requirements imposed by
+/// [lockFile].
+///
+/// Like [resolveVersions] except that this function returns `null` where a
+/// similar call to [resolveVersions] would throw a [SolveFailure].
+///
+/// If [useLatest] is given, then only the latest versions of the referenced
+/// packages will be used. This is for forcing an upgrade to one or more
+/// packages.
+///
+/// If [upgradeAll] is true, the contents of [lockFile] are ignored.
+Future<SolveResult> tryResolveVersions(
+ SolveType type, SystemCache cache, Package root,
+ {LockFile lockFile, Iterable<String> useLatest}) async {
+ try {
+ return await resolveVersions(type, cache, root,
+ lockFile: lockFile, useLatest: useLatest);
+ } on SolveFailure {
+ return null;
+ }
+}
diff --git a/test/outdated/goldens/prereleases.txt b/test/outdated/goldens/prereleases.txt
index 9a3d559..da07c5e 100644
--- a/test/outdated/goldens/prereleases.txt
+++ b/test/outdated/goldens/prereleases.txt
@@ -10,7 +10,7 @@
"version": "1.0.0-dev.1"
},
"resolvable": {
- "version": "0.9.0"
+ "version": "1.0.0-dev.2"
},
"latest": {
"version": "1.0.0-dev.2"
@@ -35,9 +35,9 @@
}
$ pub outdated --no-color
-Dependencies Current Upgradable Resolvable Latest
-foo *1.0.0-dev.1 *1.0.0-dev.1 *0.9.0 1.0.0-dev.2
-mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
+Dependencies Current Upgradable Resolvable Latest
+foo *1.0.0-dev.1 *1.0.0-dev.1 1.0.0-dev.2 1.0.0-dev.2
+mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
dev_dependencies: all up-to-date
@@ -49,10 +49,10 @@
To update these dependencies, edit pubspec.yaml.
$ pub outdated --no-color --up-to-date
-Dependencies Current Upgradable Resolvable Latest
-bar 0.9.0 0.9.0 0.9.0 0.9.0
-foo *1.0.0-dev.1 *1.0.0-dev.1 *0.9.0 1.0.0-dev.2
-mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
+Dependencies Current Upgradable Resolvable Latest
+bar 0.9.0 0.9.0 0.9.0 0.9.0
+foo *1.0.0-dev.1 *1.0.0-dev.1 1.0.0-dev.2 1.0.0-dev.2
+mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
dev_dependencies: all up-to-date
@@ -64,10 +64,10 @@
To update these dependencies, edit pubspec.yaml.
$ pub outdated --no-color --prereleases
-Dependencies Current Upgradable Resolvable Latest
-bar *0.9.0 *0.9.0 *0.9.0 1.0.0-dev.2
-foo *1.0.0-dev.1 *1.0.0-dev.1 *0.9.0 1.0.0-dev.2
-mop *0.10.0-dev *0.10.0-dev *0.10.0 1.0.0-dev
+Dependencies Current Upgradable Resolvable Latest
+bar *0.9.0 *0.9.0 *0.9.0 1.0.0-dev.2
+foo *1.0.0-dev.1 *1.0.0-dev.1 1.0.0-dev.2 1.0.0-dev.2
+mop *0.10.0-dev *0.10.0-dev *0.10.0 1.0.0-dev
dev_dependencies: all up-to-date
@@ -79,9 +79,9 @@
To update these dependencies, edit pubspec.yaml.
$ pub outdated --no-color --no-dev-dependencies
-Dependencies Current Upgradable Resolvable Latest
-foo *1.0.0-dev.1 *1.0.0-dev.1 *0.9.0 1.0.0-dev.2
-mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
+Dependencies Current Upgradable Resolvable Latest
+foo *1.0.0-dev.1 *1.0.0-dev.1 1.0.0-dev.2 1.0.0-dev.2
+mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
transitive dependencies: all up-to-date
@@ -89,9 +89,9 @@
To update these dependencies, edit pubspec.yaml.
$ pub outdated --no-color --no-dependency-overrides
-Dependencies Current Upgradable Resolvable Latest
-foo *1.0.0-dev.1 *1.0.0-dev.1 *0.9.0 1.0.0-dev.2
-mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
+Dependencies Current Upgradable Resolvable Latest
+foo *1.0.0-dev.1 *1.0.0-dev.1 1.0.0-dev.2 1.0.0-dev.2
+mop *0.10.0-dev *0.10.0-dev 0.10.0 0.10.0
dev_dependencies: all up-to-date
@@ -106,10 +106,10 @@
Running in 'null safety' mode.
Showing packages where the current version doesn't fully support null safety.
-Dependencies Current Upgradable Resolvable Latest
-bar ✗0.9.0 ✗0.9.0 ✗0.9.0 ✗0.9.0
-foo ✗1.0.0-dev.1 ✗1.0.0-dev.1 ✗0.9.0 ✗1.0.0-dev.2
-mop ✗0.10.0-dev ✗0.10.0-dev ✗0.10.0 ✗0.10.0
+Dependencies Current Upgradable Resolvable Latest
+bar ✗0.9.0 ✗0.9.0 ✗0.9.0 ✗0.9.0
+foo ✗1.0.0-dev.1 ✗1.0.0-dev.1 ✗1.0.0-dev.2 ✗1.0.0-dev.2
+mop ✗0.10.0-dev ✗0.10.0-dev ✗0.10.0 ✗0.10.0
dev_dependencies: all fully support null safety
@@ -153,7 +153,7 @@
"nullSafety": false
},
"resolvable": {
- "version": "0.9.0",
+ "version": "1.0.0-dev.2",
"nullSafety": false
},
"latest": {
@@ -195,7 +195,7 @@
"version": "1.0.0-dev.1"
},
"resolvable": {
- "version": "0.9.0"
+ "version": "1.0.0-dev.2"
},
"latest": {
"version": "1.0.0-dev.2"
diff --git a/test/pubspec_utils_test.dart b/test/pubspec_utils_test.dart
new file mode 100644
index 0000000..d343cf7
--- /dev/null
+++ b/test/pubspec_utils_test.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, 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/src/pubspec_utils.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('stripUpperBound', () {
+ test('works on version range', () {
+ final constraint = VersionConstraint.parse('>=1.0.0 <3.0.0');
+ final removedUpperBound = stripUpperBound(constraint) as VersionRange;
+
+ expect(removedUpperBound.min, equals(Version(1, 0, 0)));
+ expect(removedUpperBound.includeMin, isTrue);
+ expect(removedUpperBound.max, isNull);
+ });
+
+ test('works on version range exclude min', () {
+ final constraint = VersionConstraint.parse('>0.0.1 <5.0.0');
+ final removedUpperBound = stripUpperBound(constraint) as VersionRange;
+
+ expect(removedUpperBound.min, equals(Version(0, 0, 1)));
+ expect(removedUpperBound.includeMin, isFalse);
+ expect(removedUpperBound.max, isNull);
+ });
+
+ test('works on specific version constraint', () {
+ final constraint = VersionConstraint.parse('1.2.3');
+ final removedUpperBound = stripUpperBound(constraint) as VersionRange;
+
+ expect(removedUpperBound.min, equals(Version(1, 2, 3)));
+ expect(removedUpperBound.includeMin, isTrue);
+ expect(removedUpperBound.max, isNull);
+ });
+
+ test('works on compatible version constraint', () {
+ final constraint = VersionConstraint.parse('^1.2.3');
+ final removedUpperBound = stripUpperBound(constraint) as VersionRange;
+
+ expect(removedUpperBound.min, equals(Version(1, 2, 3)));
+ expect(removedUpperBound.includeMin, isTrue);
+ expect(removedUpperBound.max, isNull);
+ });
+
+ test('works on compatible version union', () {
+ final constraint1 = VersionConstraint.parse('>=1.2.3 <2.0.0');
+ final constraint2 = VersionConstraint.parse('>2.2.3 <=4.0.0');
+ final constraint = VersionUnion.fromRanges([constraint1, constraint2]);
+
+ final removedUpperBound = stripUpperBound(constraint) as VersionRange;
+
+ expect(removedUpperBound.min, equals(Version(1, 2, 3)));
+ expect(removedUpperBound.includeMin, isTrue);
+ expect(removedUpperBound.max, isNull);
+ });
+
+ test(
+ 'returns the empty version constraint when an empty version constraint '
+ 'is provided', () {
+ final constraint = VersionConstraint.empty;
+
+ expect(stripUpperBound(constraint), VersionConstraint.empty);
+ });
+
+ test('returns the empty version constraint on empty version union', () {
+ final constraint = VersionUnion.fromRanges([]);
+ expect(stripUpperBound(constraint), VersionConstraint.empty);
+ });
+ });
+}