Suggest resolution updates (#3569)

diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
index 7bb02a1..8c38636 100644
--- a/lib/src/command/add.dart
+++ b/lib/src/command/add.dart
@@ -18,6 +18,7 @@
 import '../package.dart';
 import '../package_name.dart';
 import '../pubspec.dart';
+import '../sdk.dart';
 import '../solver.dart';
 import '../source/git.dart';
 import '../source/hosted.dart';
@@ -612,6 +613,9 @@
             {
               'dependencies': {
                 packageName: parsedDescriptor,
+              },
+              'environment': {
+                'sdk': sdk.version.toString(),
               }
             },
             cache.sources,
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 0ddf6ef..ced8c00 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -53,7 +53,7 @@
   Future<void> runProtected() async {
     final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec);
 
-    final breakingPubspec = stripVersionUpperBounds(compatiblePubspec);
+    final breakingPubspec = stripVersionBounds(compatiblePubspec);
 
     final compatiblePackagesResult =
         await _tryResolve(compatiblePubspec, cache);
@@ -85,7 +85,7 @@
       if (package == null) return [];
       final lockFile = entrypoint.lockFile;
       final pubspec = upgradeType == _UpgradeType.multiBreaking
-          ? stripVersionUpperBounds(rootPubspec)
+          ? stripVersionBounds(rootPubspec)
           : Pubspec(
               rootPubspec.name,
               dependencies: rootPubspec.dependencies.values,
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index c9e45df..dee80a1 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -736,7 +736,7 @@
 
   @override
   Future<Pubspec> resolvablePubspec(Pubspec? pubspec) async {
-    return stripVersionUpperBounds(pubspec!);
+    return stripVersionBounds(pubspec!);
   }
 }
 
diff --git a/lib/src/command/upgrade.dart b/lib/src/command/upgrade.dart
index 84fe11e..1cfcb59 100644
--- a/lib/src/command/upgrade.dart
+++ b/lib/src/command/upgrade.dart
@@ -176,7 +176,7 @@
   Future<void> _runUpgradeMajorVersions() async {
     final toUpgrade = _directDependenciesToUpgrade();
 
-    final resolvablePubspec = stripVersionUpperBounds(
+    final resolvablePubspec = stripVersionBounds(
       entrypoint.root.pubspec,
       stripOnly: toUpgrade,
     );
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 177d6ef..6f8aa75 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -23,8 +23,8 @@
 import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
-import 'package_config.dart';
 import 'package_config.dart' show PackageConfig;
+import 'package_config.dart';
 import 'package_graph.dart';
 import 'package_name.dart';
 import 'pub_embeddable_command.dart';
@@ -32,6 +32,7 @@
 import 'sdk.dart';
 import 'solver.dart';
 import 'solver/report.dart';
+import 'solver/solve_suggestions.dart';
 import 'source/cached.dart';
 import 'source/unknown.dart';
 import 'system_cache.dart';
@@ -355,16 +356,30 @@
     }
 
     SolveResult result;
-    result = await log.progress('Resolving dependencies$suffix', () async {
-      _checkSdkConstraint(root.pubspec);
-      return resolveVersions(
-        type,
-        cache,
-        root,
-        lockFile: lockFile,
-        unlock: unlock ?? [],
+
+    try {
+      result = await log.progress('Resolving dependencies$suffix', () async {
+        _checkSdkConstraint(root.pubspec);
+        return resolveVersions(
+          type,
+          cache,
+          root,
+          lockFile: lockFile,
+          unlock: unlock ?? [],
+        );
+      });
+    } on SolveFailure catch (e) {
+      throw SolveFailure(
+        e.incompatibility,
+        suggestions: await suggestResolutionAlternatives(
+          this,
+          type,
+          e.incompatibility,
+          unlock ?? [],
+          cache,
+        ),
       );
-    });
+    }
 
     // We have to download files also with --dry-run to ensure we know the
     // archive hashes for downloaded files.
diff --git a/lib/src/flutter_releases.dart b/lib/src/flutter_releases.dart
new file mode 100644
index 0000000..38e8ca8
--- /dev/null
+++ b/lib/src/flutter_releases.dart
@@ -0,0 +1,105 @@
+// Copyright (c) 2012, 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 'dart:convert';
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:http/http.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'http.dart';
+import 'log.dart';
+
+String get flutterReleasesUrl =>
+    Platform.environment['_PUB_TEST_FLUTTER_RELEASES_URL'] ??
+    'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json';
+
+// Retrieves all released versions of Flutter.
+Future<List<FlutterRelease>> _flutterReleases = () async {
+  final response = await retryForHttp(
+    'fetching available Flutter releases',
+    () => globalHttpClient.fetch(Request('GET', Uri.parse(flutterReleasesUrl))),
+  );
+  final decoded = jsonDecode(response.body);
+  if (decoded is! Map) throw FormatException('Bad response - should be a Map');
+  final releases = decoded['releases'];
+  if (releases is! List) {
+    throw FormatException('Bad response - releases should be a list.');
+  }
+  final result = <FlutterRelease>[];
+  for (final release in releases) {
+    final channel = {
+      'beta': Channel.beta,
+      'stable': Channel.stable,
+      'dev': Channel.dev
+    }[release['channel']];
+    if (channel == null) throw FormatException('Release with bad channel');
+    final dartVersion = release['dart_sdk_version'];
+    // Some releases don't have an associated dart version, ignore.
+    if (dartVersion is! String) continue;
+    final flutterVersion = release['version'];
+    if (flutterVersion is! String) throw FormatException('Not a string');
+    result.add(
+      FlutterRelease(
+        flutterVersion: Version.parse(flutterVersion),
+        dartVersion: Version.parse(dartVersion.split(' ').first),
+        channel: channel,
+      ),
+    );
+  }
+  return result
+      // Sort releases by channel and version.
+      .sorted((a, b) {
+        final compareChannels = b.channel.index - a.channel.index;
+        if (compareChannels != 0) return compareChannels;
+        return a.flutterVersion.compareTo(b.flutterVersion);
+      })
+      // Newest first.
+      .reversed
+      .toList();
+}();
+
+/// The "best" Flutter release for a given set of constraints is the first one
+/// in [_flutterReleases] that matches both the flutter and dart constraint.
+///
+/// Returns if no such release could be found.
+Future<FlutterRelease?> inferBestFlutterRelease(
+  Map<String, VersionConstraint> sdkConstraints,
+) async {
+  final List<FlutterRelease> flutterReleases;
+  try {
+    flutterReleases = await _flutterReleases;
+  } on Exception catch (e) {
+    fine('Failed retrieving the list of flutter-releases: $e');
+    return null;
+  }
+  return flutterReleases.firstWhereOrNull(
+    (release) =>
+        (sdkConstraints['flutter'] ?? VersionConstraint.any)
+            .allows(release.flutterVersion) &&
+        (sdkConstraints['dart'] ?? VersionConstraint.any)
+            .allows(release.dartVersion),
+  );
+}
+
+enum Channel {
+  stable,
+  beta,
+  dev,
+}
+
+/// A version of the Flutter SDK and its related Dart SDK.
+class FlutterRelease {
+  final Version flutterVersion;
+  final Version dartVersion;
+  final Channel channel;
+  FlutterRelease({
+    required this.flutterVersion,
+    required this.dartVersion,
+    required this.channel,
+  });
+  @override
+  toString() =>
+      'FlutterRelease(flutter=$flutterVersion, dart=$dartVersion, channel=$channel)';
+}
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 8f510f6..086f93f 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -1190,6 +1190,6 @@
 ///
 /// Otherwise, wrap with single quotation, and use '\'' to insert single quote.
 String escapeShellArgument(String x) =>
-    RegExp(r'^[a-zA-Z0-9-_=@.]+$').stringMatch(x) == null
+    RegExp(r'^[a-zA-Z0-9-_=@.^]+$').stringMatch(x) == null
         ? "'${x.replaceAll(r'\', r'\\').replaceAll("'", r"'\''")}'"
         : x;
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index c46573a..3f57976 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -36,20 +36,22 @@
 }
 
 /// Returns new pubspec with the same dependencies as [original] but with the
-/// upper bounds of the constraints removed.
+/// the 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(
+/// If [stripLower] is `false` (the default) only the upper bound is removed.
+///
+/// If [stripOnly] is provided, only the packages whose names are in [stripOnly]
+/// will have their bounds removed. If [stripOnly] is not specified or empty,
+/// then all packages will have their bounds removed.
+Pubspec stripVersionBounds(
   Pubspec original, {
   Iterable<String>? stripOnly,
+  bool stripLowerBound = false,
 }) {
   ArgumentError.checkNotNull(original, 'original');
   stripOnly ??= [];
 
-  List<PackageRange> stripUpperBounds(
+  List<PackageRange> stripBounds(
     Map<String, PackageRange> constrained,
   ) {
     final result = <PackageRange>[];
@@ -61,7 +63,9 @@
       if (stripOnly!.isEmpty || stripOnly.contains(packageRange.name)) {
         unconstrainedRange = PackageRange(
           packageRange.toRef(),
-          stripUpperBound(packageRange.constraint),
+          stripLowerBound
+              ? VersionConstraint.any
+              : stripUpperBound(packageRange.constraint),
         );
       }
       result.add(unconstrainedRange);
@@ -74,8 +78,8 @@
     original.name,
     version: original.version,
     sdkConstraints: original.sdkConstraints,
-    dependencies: stripUpperBounds(original.dependencies),
-    devDependencies: stripUpperBounds(original.devDependencies),
+    dependencies: stripBounds(original.dependencies),
+    devDependencies: stripBounds(original.devDependencies),
     dependencyOverrides: original.dependencyOverrides.values,
   );
 }
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index 74dbfa8..b8f68e7 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -4,6 +4,8 @@
 
 import 'dart:async';
 
+import 'package:pub_semver/pub_semver.dart';
+
 import 'lock_file.dart';
 import 'package.dart';
 import 'solver/failure.dart';
@@ -33,6 +35,7 @@
   Package root, {
   LockFile? lockFile,
   Iterable<String> unlock = const [],
+  Map<String, Version> sdkOverrides = const {},
 }) {
   lockFile ??= LockFile.empty();
   return VersionSolver(
@@ -41,6 +44,7 @@
     root,
     lockFile,
     unlock,
+    sdkOverrides: sdkOverrides,
   ).solve();
 }
 
diff --git a/lib/src/solver/failure.dart b/lib/src/solver/failure.dart
index ba4e69f..7ccf7a8 100644
--- a/lib/src/solver/failure.dart
+++ b/lib/src/solver/failure.dart
@@ -19,6 +19,8 @@
   /// it will have one term, which will be the root package.
   final Incompatibility incompatibility;
 
+  final String? suggestions;
+
   @override
   String get message => toString();
 
@@ -35,7 +37,7 @@
     return null;
   }
 
-  SolveFailure(this.incompatibility)
+  SolveFailure(this.incompatibility, {this.suggestions})
       : assert(
           incompatibility.terms.isEmpty ||
               incompatibility.terms.single.package.isRoot,
@@ -44,7 +46,10 @@
   /// Describes how [incompatibility] was derived, and thus why version solving
   /// failed.
   @override
-  String toString() => _Writer(incompatibility).write();
+  String toString() => [
+        _Writer(incompatibility).write(),
+        if (suggestions != null) suggestions
+      ].join('\n');
 }
 
 /// A class that writes a human-readable description of the cause of a
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart
index 3dccd37..053e287 100644
--- a/lib/src/solver/package_lister.dart
+++ b/lib/src/solver/package_lister.dart
@@ -54,6 +54,8 @@
   /// reversed.
   final bool _isDowngrade;
 
+  final Map<String, Version> sdkOverrides;
+
   /// A map from dependency names to constraints indicating which versions of
   /// [_ref] have already had their dependencies on the given versions returned
   /// by [incompatibilitiesFor].
@@ -107,11 +109,15 @@
     this._overriddenPackages,
     this._allowedRetractedVersion, {
     bool downgrade = false,
+    this.sdkOverrides = const {},
   }) : _isDowngrade = downgrade;
 
   /// Creates a package lister for the root [package].
-  PackageLister.root(Package package, this._systemCache)
-      : _ref = PackageRef.root(package),
+  PackageLister.root(
+    Package package,
+    this._systemCache, {
+    required Map<String, Version>? sdkOverrides,
+  })  : _ref = PackageRef.root(package),
         // Treat the package as locked so we avoid the logic for finding the
         // boundaries of various constraints, which is useless for the root
         // package.
@@ -120,7 +126,8 @@
         _overriddenPackages =
             Set.unmodifiable(package.dependencyOverrides.keys),
         _isDowngrade = false,
-        _allowedRetractedVersion = null;
+        _allowedRetractedVersion = null,
+        sdkOverrides = sdkOverrides ?? {};
 
   /// Returns the number of versions of this package that match [constraint].
   Future<int> countVersions(VersionConstraint constraint) async {
@@ -461,6 +468,7 @@
     if (constraint == null) return true;
 
     return sdk.isAvailable &&
-        constraint.effectiveConstraint.allows(sdk.version!);
+        constraint.effectiveConstraint
+            .allows(sdkOverrides[sdk.identifier] ?? sdk.version!);
   }
 }
diff --git a/lib/src/solver/solve_suggestions.dart b/lib/src/solver/solve_suggestions.dart
new file mode 100644
index 0000000..0b381d3
--- /dev/null
+++ b/lib/src/solver/solve_suggestions.dart
@@ -0,0 +1,276 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import '../command_runner.dart';
+import '../entrypoint.dart';
+import '../flutter_releases.dart';
+import '../io.dart';
+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 'incompatibility.dart';
+import 'incompatibility_cause.dart';
+
+/// Looks through the root-[incompability] of a solve-failure and tries to see if
+/// the conflict could resolved by any of the following suggestions:
+/// * An update of the current SDK.
+/// * Any single change to a package constraint.
+/// * Removing the bounds on all constraints, changing less than 5 dependencies.
+/// * Running `pub upgrade --major versions`.
+///
+/// Returns a formatted list of suggestions, or the empty String if no
+/// suggestions were found.
+Future<String?> suggestResolutionAlternatives(
+  Entrypoint entrypoint,
+  SolveType type,
+  Incompatibility incompatibility,
+  Iterable<String> unlock,
+  SystemCache cache,
+) async {
+  final resolutionContext = _ResolutionContext(
+    entrypoint: entrypoint,
+    type: type,
+    cache: cache,
+    unlock: unlock,
+  );
+
+  final visited = <String>{};
+  final stopwatch = Stopwatch()..start();
+  final suggestions = <_ResolutionSuggestion>[];
+  void addSuggestionIfPresent(_ResolutionSuggestion? suggestion) {
+    if (suggestion != null) suggestions.add(suggestion);
+  }
+
+  for (final externalIncompatibility
+      in incompatibility.externalIncompatibilities) {
+    if (stopwatch.elapsed > Duration(seconds: 3)) {
+      // Never spend more than 3 seconds computing suggestions.
+      break;
+    }
+    final cause = externalIncompatibility.cause;
+    if (cause is SdkCause) {
+      addSuggestionIfPresent(await resolutionContext.suggestSdkUpdate(cause));
+    } else {
+      for (final term in externalIncompatibility.terms) {
+        final name = term.package.name;
+
+        if (!visited.add(name)) {
+          continue;
+        }
+        addSuggestionIfPresent(
+          await resolutionContext.suggestSinglePackageUpdate(name),
+        );
+      }
+    }
+  }
+  if (suggestions.isEmpty) {
+    addSuggestionIfPresent(
+      await resolutionContext.suggestUnlockingAll(stripLowerBound: true) ??
+          await resolutionContext.suggestUnlockingAll(stripLowerBound: false),
+    );
+  }
+
+  if (suggestions.isEmpty) return null;
+  final tryOne = suggestions.length == 1
+      ? 'You can try the following suggestion to make the pubspec resolve:'
+      : 'You can try one of the following suggestions to make the pubspec resolve:';
+
+  suggestions.sort((a, b) => a.priority.compareTo(b.priority));
+
+  return '\n$tryOne\n${suggestions.take(5).map((e) => e.suggestion).join('\n')}';
+}
+
+class _ResolutionSuggestion {
+  final String suggestion;
+  final int priority;
+  _ResolutionSuggestion(this.suggestion, {this.priority = 0});
+}
+
+String packageAddDescription(Entrypoint entrypoint, PackageId id) {
+  final name = id.name;
+  final isDev = entrypoint.root.pubspec.devDependencies.containsKey(name);
+  final resolvedDescription = id.description;
+  final String descriptor;
+  final d = resolvedDescription.description.serializeForPubspec(
+    containingDir: Directory.current
+        .path // The add command will resolve file names relative to CWD.
+    // This currently should have no implications as we don't create suggestions
+    // for path-packages.
+    ,
+    languageVersion: entrypoint.root.pubspec.languageVersion,
+  );
+  if (d == null) {
+    descriptor = VersionConstraint.compatibleWith(id.version).toString();
+  } else {
+    descriptor = json.encode({
+      'version': VersionConstraint.compatibleWith(id.version).toString(),
+      id.source.name: d
+    });
+  }
+
+  final devPart = isDev ? 'dev:' : '';
+  return '$devPart$name:${escapeShellArgument(descriptor)}';
+}
+
+class _ResolutionContext {
+  final Entrypoint entrypoint;
+  final SolveType type;
+  final Iterable<String> unlock;
+  final SystemCache cache;
+  _ResolutionContext({
+    required this.entrypoint,
+    required this.type,
+    required this.cache,
+    required this.unlock,
+  });
+
+  /// If [cause] mentions an sdk, attempt resolving using another released
+  /// version of Flutter/Dart. Return that as a suggestion if found.
+  Future<_ResolutionSuggestion?> suggestSdkUpdate(SdkCause cause) async {
+    final sdkName = cause.sdk.identifier;
+    if (!(sdkName == 'dart' || (sdkName == 'flutter' && runningFromFlutter))) {
+      // Only make sdk upgrade suggestions for Flutter and Dart.
+      return null;
+    }
+
+    final constraint = cause.constraint;
+    if (constraint == null) return null;
+
+    /// Find the most relevant Flutter release fullfilling the constraint.
+    final bestRelease =
+        await inferBestFlutterRelease({cause.sdk.identifier: constraint});
+    if (bestRelease == null) return null;
+    final result = await _tryResolve(
+      entrypoint.root.pubspec,
+      sdkOverrides: {
+        'dart': bestRelease.dartVersion,
+        'flutter': bestRelease.flutterVersion
+      },
+    );
+    if (result == null) {
+      return null;
+    }
+    return _ResolutionSuggestion(
+      runningFromFlutter
+          ? '* Try using the Flutter SDK version: ${bestRelease.flutterVersion}. '
+          :
+          // Here we assume that any Dart version included in a Flutter
+          // release can also be found as a released Dart SDK.
+          '* Try using the Dart SDK version: ${bestRelease.dartVersion}. See https://dart.dev/get-dart.',
+    );
+  }
+
+  /// Attempt another resolution with a relaxed constraint on [name]. If that
+  /// resolves, suggest upgrading to that version.
+  Future<_ResolutionSuggestion?> suggestSinglePackageUpdate(String name) async {
+    final originalRange = entrypoint.root.dependencies[name] ??
+        entrypoint.root.devDependencies[name];
+    if (originalRange == null ||
+        originalRange.description is! HostedDescription) {
+      // We can only relax constraints on hosted dependencies.
+      return null;
+    }
+    final originalConstraint = originalRange.constraint;
+    final relaxedPubspec = stripVersionBounds(
+      entrypoint.root.pubspec,
+      stripOnly: [name],
+      stripLowerBound: true,
+    );
+
+    final result = await _tryResolve(relaxedPubspec);
+    if (result == null) {
+      return null;
+    }
+    final resolvingPackage = result.packages.firstWhere((p) => p.name == name);
+
+    final addDescription = packageAddDescription(entrypoint, resolvingPackage);
+
+    var priority = 1;
+    var suggestion =
+        '* Try updating your constraint on $name: $topLevelProgram pub add $addDescription';
+    if (originalConstraint is VersionRange) {
+      final min = originalConstraint.min;
+      if (min != null) {
+        if (resolvingPackage.version < min) {
+          priority = 3;
+          suggestion =
+              '* Consider downgrading your constraint on $name: $topLevelProgram pub add $addDescription';
+        } else {
+          priority = 2;
+          suggestion =
+              '* Try upgrading your constraint on $name: $topLevelProgram pub add $addDescription';
+        }
+      }
+    }
+
+    return _ResolutionSuggestion(suggestion, priority: priority);
+  }
+
+  /// Attempt resolving with all version constraints relaxed. If that resolves,
+  /// return a corresponding suggestion to update.
+  Future<_ResolutionSuggestion?> suggestUnlockingAll({
+    required bool stripLowerBound,
+  }) async {
+    final originalPubspec = entrypoint.root.pubspec;
+    final relaxedPubspec =
+        stripVersionBounds(originalPubspec, stripLowerBound: stripLowerBound);
+
+    final result = await _tryResolve(relaxedPubspec);
+    if (result == null) {
+      return null;
+    }
+    final updatedPackageVersions = <PackageId>[];
+    for (final id in result.packages) {
+      final originalConstraint = (originalPubspec.dependencies[id.name] ??
+              originalPubspec.devDependencies[id.name])
+          ?.constraint;
+      if (originalConstraint != null) {
+        updatedPackageVersions.add(id);
+      }
+    }
+    if (stripLowerBound && updatedPackageVersions.length > 5) {
+      // Too complex, don't suggest.
+      return null;
+    }
+    if (stripLowerBound) {
+      updatedPackageVersions.sort((a, b) => a.name.compareTo(b.name));
+      final formattedConstraints = updatedPackageVersions
+          .map((e) => packageAddDescription(entrypoint, e))
+          .join(' ');
+      return _ResolutionSuggestion(
+        '* Try updating the following constraints: $topLevelProgram pub add $formattedConstraints',
+        priority: 4,
+      );
+    } else {
+      return _ResolutionSuggestion(
+        '* Try an upgrade of your constraints: $topLevelProgram pub upgrade --major-versions',
+        priority: 4,
+      );
+    }
+  }
+
+  /// Attempt resolving
+  Future<SolveResult?> _tryResolve(
+    Pubspec pubspec, {
+    Map<String, Version> sdkOverrides = const {},
+  }) async {
+    try {
+      return await resolveVersions(
+        type,
+        cache,
+        Package.inMemory(pubspec),
+        sdkOverrides: sdkOverrides,
+        lockFile: entrypoint.lockFile,
+        unlock: unlock,
+      );
+    } on SolveFailure {
+      return null;
+    }
+  }
+}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index e8a2346..c08d1ca 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -75,6 +75,10 @@
   /// The set of packages for which the lockfile should be ignored.
   final Set<String> _unlock;
 
+  /// If present these represents the version of an SDK to assume during
+  /// resolution.
+  final Map<String, Version> _sdkOverrides;
+
   final _stopwatch = Stopwatch();
 
   VersionSolver(
@@ -82,8 +86,10 @@
     this._systemCache,
     this._root,
     this._lockFile,
-    Iterable<String> unlock,
-  )   : _dependencyOverrides = _root.dependencyOverrides,
+    Iterable<String> unlock, {
+    Map<String, Version> sdkOverrides = const {},
+  })  : _sdkOverrides = sdkOverrides,
+        _dependencyOverrides = _root.dependencyOverrides,
         _unlock = {...unlock};
 
   /// Finds a set of dependencies that match the root package's constraints, or
@@ -496,7 +502,13 @@
   PackageLister _packageLister(PackageRange package) {
     var ref = package.toRef();
     return _packageListers.putIfAbsent(ref, () {
-      if (ref.isRoot) return PackageLister.root(_root, _systemCache);
+      if (ref.isRoot) {
+        return PackageLister.root(
+          _root,
+          _systemCache,
+          sdkOverrides: _sdkOverrides,
+        );
+      }
 
       var locked = _getLocked(ref.name);
       if (locked != null && locked.toRef() != ref) locked = null;
@@ -516,6 +528,7 @@
         overridden,
         _getAllowedRetracted(ref.name),
         downgrade: _type == SolveType.downgrade,
+        sdkOverrides: _sdkOverrides,
       );
     });
   }
diff --git a/lib/src/source/hosted.dart b/lib/src/source/hosted.dart
index ae034d9..e8611d4 100644
--- a/lib/src/source/hosted.dart
+++ b/lib/src/source/hosted.dart
@@ -1399,6 +1399,10 @@
     if (url == source.defaultUrl) {
       return null;
     }
+    if (languageVersion >=
+        LanguageVersion.firstVersionWithShorterHostedSyntax) {
+      return url;
+    }
     return {'url': url, 'name': packageName};
   }
 
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart
index f6767a0..7be0095 100644
--- a/test/add/hosted/non_default_pub_server_test.dart
+++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -33,11 +33,43 @@
 
     await d.appDir(
       dependencies: {
+        'foo': {'version': '1.2.3', 'hosted': url}
+      },
+    ).validate();
+  });
+
+  test('Uses old syntax when needed', () async {
+    // Make the default server serve errors. Only the custom server should
+    // be accessed.
+    (await servePackages()).serveErrors();
+
+    final server = await startPackageServer();
+    server.serve('foo', '0.2.5');
+    server.serve('foo', '1.1.0');
+    server.serve('foo', '1.2.3');
+    final oldSyntaxSdkConstraint = {
+      'environment': {
+        'sdk': '>=2.14.0 <3.0.0' // Language version for old syntax.
+      },
+    };
+
+    await d.appDir(
+      dependencies: {},
+      pubspec: oldSyntaxSdkConstraint,
+    ).create();
+
+    final url = server.url;
+
+    await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]);
+
+    await d.appDir(
+      dependencies: {
         'foo': {
           'version': '1.2.3',
           'hosted': {'name': 'foo', 'url': url}
         }
       },
+      pubspec: oldSyntaxSdkConstraint,
     ).validate();
   });
 
@@ -75,18 +107,9 @@
 
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        },
-        'bar': {
-          'version': '3.2.3',
-          'hosted': {'name': 'bar', 'url': url}
-        },
-        'baz': {
-          'version': '1.3.5',
-          'hosted': {'name': 'baz', 'url': url}
-        }
+        'foo': {'version': '1.2.3', 'hosted': url},
+        'bar': {'version': '3.2.3', 'hosted': url},
+        'baz': {'version': '1.3.5', 'hosted': url}
       },
     ).validate();
   });
@@ -139,10 +162,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '^1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': '^1.2.3', 'hosted': url}
       },
     ).validate();
   });
@@ -170,10 +190,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': '^1.2.3',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': '^1.2.3', 'hosted': url}
       },
     ).validate();
   });
@@ -202,10 +219,7 @@
     ]).validate();
     await d.appDir(
       dependencies: {
-        'foo': {
-          'version': 'any',
-          'hosted': {'name': 'foo', 'url': url}
-        }
+        'foo': {'version': 'any', 'hosted': url}
       },
     ).validate();
   });
diff --git a/test/hosted/offline_test.dart b/test/hosted/offline_test.dart
index 5e54c76..da98d0c 100644
--- a/test/hosted/offline_test.dart
+++ b/test/hosted/offline_test.dart
@@ -118,10 +118,8 @@
       await pubCommand(
         command,
         args: ['--offline'],
-        error: equalsIgnoringWhitespace("""
-            Because myapp depends on foo >2.0.0 which doesn't match any
-              versions, version solving failed.
-          """),
+        error: contains('''
+Because myapp depends on foo >2.0.0 which doesn't match any versions, version solving failed.'''),
       );
     });
 
diff --git a/test/sdk_test.dart b/test/sdk_test.dart
index b9a89d5..f409bc7 100644
--- a/test/sdk_test.dart
+++ b/test/sdk_test.dart
@@ -127,10 +127,8 @@
         await pubCommand(
           command,
           environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')},
-          error: equalsIgnoringWhitespace("""
-              Because myapp depends on foo ^1.0.0 from sdk which doesn't match
-                any versions, version solving failed.
-            """),
+          error: contains('''
+Because myapp depends on foo ^1.0.0 from sdk which doesn't match any versions, version solving failed.'''),
         );
       });
 
@@ -142,10 +140,8 @@
         ).create();
         await pubCommand(
           command,
-          error: equalsIgnoringWhitespace("""
-              Because myapp depends on foo from sdk which doesn't exist
-                (unknown SDK "unknown"), version solving failed.
-            """),
+          error: equalsIgnoringWhitespace('''
+Because myapp depends on foo from sdk which doesn't exist (unknown SDK "unknown"), version solving failed.'''),
           exitCode: exit_codes.UNAVAILABLE,
         );
       });
diff --git a/test/solve_suggestions_test.dart b/test/solve_suggestions_test.dart
new file mode 100644
index 0000000..a419243
--- /dev/null
+++ b/test/solve_suggestions_test.dart
@@ -0,0 +1,275 @@
+// Copyright (c) 2012, 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:shelf/shelf.dart';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('suggests an upgrade to the flutter sdk', () async {
+    await d.dir('flutter', [d.file('version', '1.2.3')]).create();
+    final server = await servePackages();
+    server.serve(
+      'foo',
+      '1.0.0',
+      pubspec: {
+        'environment': {'flutter': '>=3.3.0', 'sdk': '^2.17.0'}
+      },
+    );
+    server.handle(
+      '/flutterReleases',
+      (request) => Response.ok(releasesMockResponse),
+    );
+    await d.dir(appPath, [
+      d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0')
+    ]).create();
+    await pubGet(
+      error: contains('* Try using the Flutter SDK version: 3.3.2.'),
+      environment: {
+        '_PUB_TEST_SDK_VERSION': '2.17.0',
+        'FLUTTER_ROOT': path('flutter'),
+        '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases',
+        'PUB_ENVIRONMENT': 'flutter_cli',
+      },
+    );
+  });
+
+  test('suggests an upgrade to the dart sdk', () async {
+    final server = await servePackages();
+    server.serve(
+      'foo',
+      '1.0.0',
+      pubspec: {
+        'environment': {'sdk': '>=2.18.0 <2.18.1'}
+      },
+    );
+    server.handle(
+      '/flutterReleases',
+      (request) => Response.ok(releasesMockResponse),
+    );
+    await d.dir(appPath, [
+      d.libPubspec('myApp', '1.0.0', deps: {'foo': 'any'}, sdk: '^2.17.0')
+    ]).create();
+    await pubGet(
+      error: contains('* Try using the Dart SDK version: 2.18.0'),
+      environment: {
+        '_PUB_TEST_SDK_VERSION': '2.17.0',
+        '_PUB_TEST_FLUTTER_RELEASES_URL': '${server.url}/flutterReleases',
+      },
+    );
+  });
+
+  test('suggests an upgrade or downgrade to a package constraint', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '^2.0.0'});
+    server.serve('foo', '0.9.0', deps: {'bar': '^1.0.0'});
+
+    server.serve('bar', '1.0.0');
+    server.serve('bar', '2.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '^1.0.0'},
+        devDeps: {'bar': '^1.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: allOf(
+        [
+          contains(
+            '* Consider downgrading your constraint on foo: dart pub add foo:^0.9.0',
+          ),
+          contains(
+            '* Try upgrading your constraint on bar: dart pub add dev:bar:^2.0.0',
+          ),
+        ],
+      ),
+    );
+  });
+
+  test('suggests an update to an empty package constraint', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '>1.0.0 <=0.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: allOf(
+        [
+          contains(
+            '* Try updating your constraint on foo: dart pub add foo:^1.0.0',
+          ),
+        ],
+      ),
+    );
+  });
+
+  test('suggests updates to multiple packages', () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'});
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {'foo': '1.0.0'},
+        devDeps: {'bar': '1.0.0'},
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try updating the following constraints: dart pub add dev:bar:^2.0.0 foo:^2.0.0',
+      ),
+    );
+  });
+
+  test('suggests a major upgrade if more than 5 needs to be upgraded',
+      () async {
+    final server = await servePackages();
+    server.serve('foo', '1.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '1.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo', '2.0.0', deps: {'bar': '2.0.0'});
+    server.serve('bar', '2.0.0', deps: {'foo': '2.0.0'});
+    server.serve('foo1', '1.0.0', deps: {'bar1': '2.0.0'});
+    server.serve('bar1', '1.0.0', deps: {'foo1': '2.0.0'});
+    server.serve('foo1', '2.0.0', deps: {'bar1': '2.0.0'});
+    server.serve('bar1', '2.0.0', deps: {'foo1': '2.0.0'});
+    server.serve('foo2', '1.0.0', deps: {'bar2': '2.0.0'});
+    server.serve('bar2', '1.0.0', deps: {'foo2': '2.0.0'});
+    server.serve('foo2', '2.0.0', deps: {'bar2': '2.0.0'});
+    server.serve('bar2', '2.0.0', deps: {'foo2': '2.0.0'});
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '1.0.0',
+          'bar': '1.0.0',
+          'foo1': '1.0.0',
+          'bar1': '1.0.0',
+          'foo2': '1.0.0',
+          'bar2': '1.0.0',
+        },
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try an upgrade of your constraints: dart pub upgrade --major-versions',
+      ),
+    );
+  });
+
+  test('suggests upgrades to non-default servers', () async {
+    final server = await servePackages();
+    final server2 = await startPackageServer();
+    server.serve(
+      'foo',
+      '1.0.0',
+      deps: {
+        'bar': {'version': '2.0.0', 'hosted': server2.url}
+      },
+    );
+
+    server2.serve('bar', '1.0.0');
+    server2.serve('bar', '2.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '^1.0.0',
+          'bar': {'version': '^1.0.0', 'hosted': server2.url},
+        },
+      )
+    ]).create();
+    await pubGet(
+      error: contains(
+        '* Try upgrading your constraint on bar: dart pub add '
+        'bar:\'{"version":"^2.0.0","hosted":"${server2.url}"}\'',
+      ),
+    );
+    await pubAdd(
+      args: ['bar:{"version":"^2.0.0","hosted":"${server2.url}"}'],
+    );
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myApp',
+        '1.0.0',
+        deps: {
+          'foo': '^1.0.0',
+          'bar': {'version': '^2.0.0', 'hosted': server2.url},
+        },
+      )
+    ]).validate();
+  });
+}
+
+const releasesMockResponse = '''
+{
+  "base_url": "https://storage.googleapis.com/flutter_infra_release/releases",
+  "current_release": {
+    "beta": "096162697a9cdc79f4e47f7230d70935fa81fd24",
+    "dev": "13a2fb10b838971ce211230f8ffdd094c14af02c",
+    "stable": "e3c29ec00c9c825c891d75054c63fcc46454dca1"
+  },
+  "releases": [
+    {
+      "hash": "e3c29ec00c9c825c891d75054c63fcc46454dca1",
+      "channel": "stable",
+      "version": "3.3.2",
+      "dart_sdk_version": "2.18.1",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-09-14T15:06:55.724077Z",
+      "archive": "stable/linux/flutter_linux_3.3.2-stable.tar.xz",
+      "sha256": "a733a75ae07c42b2059a31fc9d64fabfae5dccd15770fa6b7f290e3f5f9c98e8"
+    },
+    {
+      "hash": "4f9d92fbbdf072a70a70d2179a9f87392b94104c",
+      "channel": "stable",
+      "version": "3.3.1",
+      "dart_sdk_version": "2.18.0",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-09-07T15:30:42.283999Z",
+      "archive": "stable/linux/flutter_linux_3.3.1-stable.tar.xz",
+      "sha256": "7cbcff0230affbe07a5ce82298044ac437e96aeba69f83656f9ed9a910a392e7"
+    },
+    {
+      "hash": "ffccd96b62ee8cec7740dab303538c5fc26ac543",
+      "channel": "stable",
+      "version": "3.3.0",
+      "dart_sdk_version": "2.18.0",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-08-30T17:22:12.916008Z",
+      "archive": "stable/linux/flutter_linux_3.3.0-stable.tar.xz",
+      "sha256": "a92a27aa6d4454d7a1cf9f8a0a56e0e5d6865f2cfcd21cf52e57f7922ad5d504"
+    },
+    {
+      "hash": "096162697a9cdc79f4e47f7230d70935fa81fd24",
+      "channel": "beta",
+      "version": "3.3.0-0.5.pre",
+      "dart_sdk_version": "2.18.0 (build 2.18.0-271.7.beta)",
+      "dart_sdk_arch": "x64",
+      "release_date": "2022-08-23T17:03:21.525151Z",
+      "archive": "beta/linux/flutter_linux_3.3.0-0.5.pre-beta.tar.xz",
+      "sha256": "8e07158a64a8ce79f9169cffe4ff23a486bdabb29401f13177672fae18de52d2"
+    }
+  ]
+}
+''';
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index 17c5be2..df3ae2b 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -200,8 +200,8 @@
 
   // Issue 1853
   test(
-      "produces a nice message for a locked dependency that's the only "
-      'version of its package', () async {
+      "produces a nice message for a locked dependency that's the only version of its package",
+      () async {
     await servePackages()
       ..serve('foo', '1.0.0', deps: {'bar': '>=2.0.0'})
       ..serve('bar', '1.0.0')
@@ -212,11 +212,9 @@
 
     await d.appDir(dependencies: {'foo': 'any', 'bar': '<2.0.0'}).create();
     await expectResolves(
-      error: equalsIgnoringWhitespace('''
-      Because myapp depends on foo any which depends on bar >=2.0.0,
-        bar >=2.0.0 is required.
-      So, because myapp depends on bar <2.0.0, version solving failed.
-    '''),
+      error: contains('''
+Because myapp depends on foo any which depends on bar >=2.0.0, bar >=2.0.0 is required.
+So, because myapp depends on bar <2.0.0, version solving failed.'''),
     );
   });
 }
@@ -336,11 +334,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because no versions of foo match ^2.0.0 and myapp depends on foo
-          >=1.0.0 <3.0.0, foo ^1.0.0 is required.
-        So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.
-      '''),
+        error: contains('''
+Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required.
+So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''),
       );
     });
 
@@ -357,11 +353,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because no versions of foo match ^2.0.0 and myapp depends on foo
-          >=1.0.0 <3.0.0, foo ^1.0.0 is required.
-        So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.
-      '''),
+        error: contains('''
+Because no versions of foo match ^2.0.0 and myapp depends on foo >=1.0.0 <3.0.0, foo ^1.0.0 is required.
+So, because myapp depends on foo >=2.0.0 <4.0.0, version solving failed.'''),
       );
     });
 
@@ -378,10 +372,9 @@
       ]).create();
 
       await expectResolves(
-        error: equalsIgnoringWhitespace('''
-        Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version
-          solving failed.
-      '''),
+        error: contains(
+          'Because myapp depends on both foo ^1.0.0 and foo ^2.0.0, version solving failed.',
+        ),
       );
     });
 
@@ -441,10 +434,8 @@
 
     await d.appDir(dependencies: {'foo': '>=1.0.0 <2.0.0'}).create();
     await expectResolves(
-      error: equalsIgnoringWhitespace("""
-      Because myapp depends on foo ^1.0.0 which doesn't match any versions,
-        version solving failed.
-    """),
+      error: contains('''
+Because myapp depends on foo ^1.0.0 which doesn't match any versions, version solving failed.'''),
     );
   });
 
@@ -575,11 +566,10 @@
       ..serve('b', '1.0.0');
 
     await d.appDir(dependencies: {'a': 'any', 'b': '>1.0.0'}).create();
+
     await expectResolves(
-      error: equalsIgnoringWhitespace("""
-      Because myapp depends on b >1.0.0 which doesn't match any versions,
-        version solving failed.
-    """),
+      error: contains('''
+Because myapp depends on b >1.0.0 which doesn't match any versions, version solving failed.'''),
     );
   });
 
@@ -1115,11 +1105,10 @@
     ]).create();
 
     await expectResolves(
-      error: equalsIgnoringWhitespace('''
-      The current Dart SDK version is 3.1.2+3.
+      error: contains('''
+The current Dart SDK version is 3.1.2+3.
 
-      Because myapp requires SDK version 2.12.0, version solving failed.
-    '''),
+Because myapp requires SDK version 2.12.0, version solving failed.'''),
     );
   });