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);
+    });
+  });
+}