diff --git a/bin/dependency_services.dart b/bin/dependency_services.dart
index f117afd..90e528a 100644
--- a/bin/dependency_services.dart
+++ b/bin/dependency_services.dart
@@ -20,7 +20,7 @@
 class _DependencyServicesCommandRunner extends CommandRunner<int>
     implements PubTopLevel {
   @override
-  String? get directory => argResults['directory'];
+  String get directory => argResults['directory'];
 
   @override
   bool get captureStackChains => argResults['verbose'];
diff --git a/lib/src/command.dart b/lib/src/command.dart
index e86dd50..31f0b17 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -55,11 +55,12 @@
     return a;
   }
 
-  String get directory =>
-      (argResults.options.contains('directory')
-          ? argResults['directory']
-          : null) ??
-      _pubTopLevel.directory;
+  String get directory {
+    return (argResults.options.contains('directory')
+            ? argResults['directory']
+            : null) ??
+        _pubTopLevel.directory;
+  }
 
   late final SystemCache cache = SystemCache(isOffline: isOffline);
 
@@ -194,7 +195,6 @@
       return exit_codes.SUCCESS;
     } catch (error, chain) {
       log.exception(error, chain);
-
       if (_pubTopLevel.trace) {
         log.dumpTranscriptToStdErr();
       } else if (!isUserFacingException(error)) {
@@ -351,7 +351,7 @@
   }
 
   /// The directory containing the pubspec.yaml of the project to work on.
-  String? get directory;
+  String get directory;
 
   /// The argResults from the level of parsing of the 'pub' command.
   ArgResults get argResults;
diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index e45a735..e516168 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -155,9 +155,9 @@
   Future<void> _publish(List<int> packageBytes) async {
     try {
       final officialPubServers = {
-        'https://pub.dev',
-        // [validateAndNormalizeHostedUrl] normalizes https://pub.dartlang.org
-        // to https://pub.dev, so we don't need to do allow that here.
+        'https://pub.dartlang.org',
+        // [validateAndNormalizeHostedUrl] normalizes https://pub.dev
+        // to https://pub.dartlang.org, so we don't need to do allow that here.
 
         // Pub uses oauth2 credentials only for authenticating official pub
         // servers for security purposes (to not expose pub.dev access token to
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 92d4cf8..150de16 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -45,7 +45,7 @@
 
 class PubCommandRunner extends CommandRunner<int> implements PubTopLevel {
   @override
-  String? get directory => argResults['directory'];
+  String get directory => argResults['directory'];
 
   @override
   bool get captureStackChains {
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index b536cca..2683cdf 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -12,6 +12,7 @@
 import 'package:path/path.dart' as p;
 import 'package:pool/pool.dart';
 import 'package:pub_semver/pub_semver.dart';
+import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
 
 import 'command_runner.dart';
@@ -19,7 +20,6 @@
 import 'exceptions.dart';
 import 'executable.dart';
 import 'io.dart';
-import 'language_version.dart';
 import 'lock_file.dart';
 import 'log.dart' as log;
 import 'package.dart';
@@ -101,7 +101,17 @@
     if (!fileExists(lockFilePath)) {
       return _lockFile = LockFile.empty();
     } else {
-      return _lockFile = LockFile.load(lockFilePath, cache.sources);
+      try {
+        return _lockFile = LockFile.load(lockFilePath, cache.sources);
+      } on SourceSpanException catch (e) {
+        throw SourceSpanApplicationException(
+          e.message,
+          e.span,
+          explanation: 'Failed parsing lock file:',
+          hint:
+              'Consider deleting the file and running `$topLevelProgram pub get` to recreate it.',
+        );
+      }
     }
   }
 
@@ -270,7 +280,8 @@
       await lockFile.packageConfigFile(
         cache,
         entrypoint: entrypointName,
-        entrypointSdkConstraint: root.pubspec.sdkConstraints[sdk.identifier],
+        entrypointSdkConstraint:
+            root.pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
         relativeFrom: isGlobal ? null : root.dir,
       ),
     );
@@ -805,9 +816,7 @@
       try {
         // Load `pubspec.yaml` and extract language version to compare with the
         // language version from `package_config.json`.
-        final languageVersion = LanguageVersion.fromSdkConstraint(
-          cache.load(id).pubspec.sdkConstraints[sdk.identifier],
-        );
+        final languageVersion = cache.load(id).pubspec.languageVersion;
         if (pkg.languageVersion != languageVersion) {
           final relativePubspecPath = p.join(
             cache.getDirectory(id, relativeFrom: '.'),
@@ -863,7 +872,7 @@
   ///
   /// We don't allow unknown sdks.
   void _checkSdkConstraint(Pubspec pubspec) {
-    final dartSdkConstraint = pubspec.sdkConstraints['dart'];
+    final dartSdkConstraint = pubspec.dartSdkConstraint.effectiveConstraint;
     if (dartSdkConstraint is! VersionRange || dartSdkConstraint.min == null) {
       // Suggest version range '>=2.10.0 <3.0.0', we avoid using:
       // [CompatibleWithVersionRange] because some pub versions don't support
@@ -898,7 +907,7 @@
         final keyNode = environment.nodes.entries
             .firstWhere((e) => (e.key as YamlNode).value == sdk)
             .key as YamlNode;
-        throw PubspecException('''
+        throw SourceSpanApplicationException('''
 $pubspecPath refers to an unknown sdk '$sdk'.
 
 Did you mean to add it as a dependency?
diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart
index ec9f876..39e9c51 100644
--- a/lib/src/exceptions.dart
+++ b/lib/src/exceptions.dart
@@ -7,6 +7,7 @@
 
 import 'package:args/command_runner.dart';
 import 'package:http/http.dart' as http;
+import 'package:source_span/source_span.dart';
 import 'package:stack_trace/stack_trace.dart';
 import 'package:yaml/yaml.dart';
 
@@ -127,3 +128,34 @@
       error is YamlException ||
       error is UsageException;
 }
+
+/// An exception thrown when parsing a `pubspec.yaml` or a `pubspec.lock`.
+///
+/// These exceptions are often thrown lazily while accessing pubspec properties.
+///
+/// By being an [ApplicationException] this will not trigger a stack-trace on
+/// normal operations.
+///
+/// Works as a [SourceSpanFormatException], but can contain more context:
+/// An optional [explanation] that explains the operation that failed.
+/// An optional [hint] that gives suggestions how to proceed.
+class SourceSpanApplicationException extends SourceSpanFormatException
+    implements ApplicationException {
+  final String? explanation;
+  final String? hint;
+
+  SourceSpanApplicationException(String message, SourceSpan? span,
+      {this.hint, this.explanation})
+      : super(message, span);
+
+  @override
+  String toString({color}) {
+    return [
+      if (explanation != null) explanation,
+      span == null
+          ? message
+          : 'Error on ${span?.message(message, color: color)}',
+      if (hint != null) hint,
+    ].join('\n\n');
+  }
+}
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index bb97023..9d79a16 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -362,7 +362,9 @@
         dataError('${log.bold(name)} ${entrypoint.root.version} requires '
             'unknown SDK "$name".');
       } else if (sdkName == 'dart') {
-        if (constraint.allows((sdk as DartSdk).version)) return;
+        if (constraint.effectiveConstraint.allows((sdk as DartSdk).version)) {
+          return;
+        }
         dataError("${log.bold(name)} ${entrypoint.root.version} doesn't "
             'support Dart ${sdk.version}.');
       } else {
@@ -730,35 +732,18 @@
       }
     }
 
-    // If the script was built to a snapshot, just try to invoke that
-    // directly and skip pub global run entirely.
-    String invocation;
     late String binstub;
+    // Batch files behave in funky ways if they are modified while updating.
+    // To ensure that the byte-offsets of everything stays the same even if the
+    // snapshot filename changes we insert some padding in lines containing the
+    // snapshot.
+    // 260 is the maximal short path length on Windows. Hopefully that is
+    // enough.
+    final padding = ' ' * (260 - snapshot.length);
+    // We need an absolute path since relative ones won't be relative to the
+    // right directory when the user runs this.
+    snapshot = p.absolute(snapshot);
     if (Platform.isWindows) {
-      if (fileExists(snapshot)) {
-        // We expect absolute paths from the precompiler since relative ones
-        // won't be relative to the right directory when the user runs this.
-        assert(p.isAbsolute(snapshot));
-        invocation = '''
-if exist "$snapshot" (
-  call dart "$snapshot" %*
-  rem The VM exits with code 253 if the snapshot version is out-of-date.
-  rem If it is, we need to delete it and run "pub global" manually.
-  if not errorlevel 253 (
-    goto error
-  )
-  dart pub global run ${package.name}:$script %*
-) else (
-  dart pub global run ${package.name}:$script %*
-)
-goto eof
-:error
-exit /b %errorlevel%
-:eof
-''';
-      } else {
-        invocation = 'dart pub global run ${package.name}:$script %*';
-      }
       binstub = '''
 @echo off
 rem This file was created by pub v${sdk.version}.
@@ -766,14 +751,30 @@
 rem Version: ${package.version}
 rem Executable: $executable
 rem Script: $script
-$invocation
+if exist "$snapshot" $padding(
+  call dart "$snapshot" $padding%*
+  rem The VM exits with code 253 if the snapshot version is out-of-date.
+  rem If it is, we need to delete it and run "pub global" manually.
+  if not errorlevel 253 (
+    goto error
+  )
+  call dart pub global run ${package.name}:$script %*
+) else (
+  call dart pub global run ${package.name}:$script %*
+)
+goto eof
+:error
+exit /b %errorlevel%
+:eof
 ''';
     } else {
-      if (fileExists(snapshot)) {
-        // We expect absolute paths from the precompiler since relative ones
-        // won't be relative to the right directory when the user runs this.
-        assert(p.isAbsolute(snapshot));
-        invocation = '''
+      binstub = '''
+#!/usr/bin/env sh
+# This file was created by pub v${sdk.version}.
+# Package: ${package.name}
+# Version: ${package.version}
+# Executable: $executable
+# Script: $script
 if [ -f $snapshot ]; then
   dart "$snapshot" "\$@"
   # The VM exits with code 253 if the snapshot version is out-of-date.
@@ -787,18 +788,6 @@
   dart pub global run ${package.name}:$script "\$@"
 fi
 ''';
-      } else {
-        invocation = 'dart pub global run ${package.name}:$script "\$@"';
-      }
-      binstub = '''
-#!/usr/bin/env sh
-# This file was created by pub v${sdk.version}.
-# Package: ${package.name}
-# Version: ${package.version}
-# Executable: $executable
-# Script: $script
-$invocation
-''';
     }
 
     // Write the binstub to a temporary location, make it executable and move
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index 46be2bf..6c66224 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -98,50 +98,54 @@
 
     Uri? sourceUrl;
     if (filePath != null) sourceUrl = p.toUri(filePath);
-    var parsed = loadYamlNode(contents, sourceUrl: sourceUrl);
+    final parsed = _parseNode<YamlMap>(
+      loadYamlNode(contents, sourceUrl: sourceUrl),
+      'YAML mapping',
+    );
 
-    _validate(parsed is Map, 'The lockfile must be a YAML mapping.', parsed);
-    var parsedMap = parsed as YamlMap;
-
-    var sdkConstraints = <String, VersionConstraint>{};
-    var sdkNode = parsedMap.nodes['sdk'];
+    final sdkConstraints = <String, VersionConstraint>{};
+    final sdkNode =
+        _getEntry<YamlScalar?>(parsed, 'sdk', 'string', required: false);
     if (sdkNode != null) {
       // Lockfiles produced by pub versions from 1.14.0 through 1.18.0 included
       // a top-level "sdk" field which encoded the unified constraint on the
       // Dart SDK. They had no way of specifying constraints on other SDKs.
       sdkConstraints['dart'] = _parseVersionConstraint(sdkNode);
-    } else if (parsedMap.containsKey('sdks')) {
-      var sdksField = parsedMap['sdks'];
-      _validate(sdksField is Map, 'The "sdks" field must be a mapping.',
-          parsedMap.nodes['sdks']);
-
-      sdksField.nodes.forEach((name, constraint) {
-        _validate(name.value is String, 'SDK names must be strings.', name);
-        sdkConstraints[name.value as String] =
-            _parseVersionConstraint(constraint);
-      });
     }
 
-    var packages = <String, PackageId>{};
-    var packageEntries = parsedMap['packages'];
-    if (packageEntries != null) {
-      _validate(packageEntries is Map, 'The "packages" field must be a map.',
-          parsedMap.nodes['packages']);
+    final sdksField =
+        _getEntry<YamlMap?>(parsed, 'sdks', 'map', required: false);
 
-      packageEntries.forEach((name, spec) {
+    if (sdksField != null) {
+      _parseEachEntry<String, YamlScalar>(sdksField, (name, constraint) {
+        sdkConstraints[name] = _parseVersionConstraint(constraint);
+      }, 'string', 'string');
+    }
+
+    final packages = <String, PackageId>{};
+
+    final mainDependencies = <String>{};
+    final devDependencies = <String>{};
+    final overriddenDependencies = <String>{};
+
+    final packageEntries =
+        _getEntry<YamlMap?>(parsed, 'packages', 'map', required: false);
+
+    if (packageEntries != null) {
+      _parseEachEntry<String, YamlMap>(packageEntries, (name, spec) {
         // Parse the version.
-        _validate(spec.containsKey('version'),
-            'Package $name is missing a version.', spec);
-        var version = Version.parse(spec['version']);
+        final versionEntry = _getStringEntry(spec, 'version');
+        final version = Version.parse(versionEntry);
 
         // Parse the source.
-        _validate(spec.containsKey('source'),
-            'Package $name is missing a source.', spec);
-        var sourceName = spec['source'];
+        final sourceName = _getStringEntry(spec, 'source');
 
-        _validate(spec.containsKey('description'),
-            'Package $name is missing a description.', spec);
-        var description = spec['description'];
+        var descriptionNode =
+            _getEntry<YamlNode>(spec, 'description', 'description');
+
+        dynamic description = descriptionNode is YamlScalar
+            ? descriptionNode.value
+            : descriptionNode;
 
         // Let the source parse the description.
         var source = sources(sourceName);
@@ -150,18 +154,30 @@
           id = source.parseId(name, version, description,
               containingDir: filePath == null ? null : p.dirname(filePath));
         } on FormatException catch (ex) {
-          throw SourceSpanFormatException(
-              ex.message, spec.nodes['description'].span);
+          _failAt(ex.message, spec.nodes['description']!);
         }
 
         // Validate the name.
-        _validate(name == id.name,
-            "Package name $name doesn't match ${id.name}.", spec);
+        if (name != id.name) {
+          _failAt("Package name $name doesn't match ${id.name}.", spec);
+        }
 
         packages[name] = id;
-      });
+        if (spec.containsKey('dependency')) {
+          final dependencyKind = _getStringEntry(spec, 'dependency');
+          switch (dependencyKind) {
+            case _directMain:
+              mainDependencies.add(name);
+              break;
+            case _directDev:
+              devDependencies.add(name);
+              break;
+            case _directOverridden:
+              overriddenDependencies.add(name);
+          }
+        }
+      }, 'string', 'map');
     }
-
     return LockFile._(
         packages,
         sdkConstraints,
@@ -170,15 +186,6 @@
         const UnmodifiableSetView.empty());
   }
 
-  /// Asserts that [node] is a version constraint, and parses it.
-  static VersionConstraint _parseVersionConstraint(YamlNode node) {
-    _validate(node.value is String,
-        'Invalid version constraint: must be a string.', node);
-
-    return _wrapFormatException('version constraint', node.span,
-        () => VersionConstraint.parse(node.value));
-  }
-
   /// Runs [fn] and wraps any [FormatException] it throws in a
   /// [SourceSpanFormatException].
   ///
@@ -195,10 +202,68 @@
     }
   }
 
-  /// If [condition] is `false` throws a format error with [message] for [node].
-  static void _validate(bool condition, String message, YamlNode? node) {
-    if (condition) return;
-    throw SourceSpanFormatException(message, node!.span);
+  static VersionConstraint _parseVersionConstraint(YamlNode node) {
+    return _parseNode(node, 'version constraint',
+        parse: VersionConstraint.parse);
+  }
+
+  static String _getStringEntry(YamlMap map, String key) {
+    return _parseNode<String>(
+        _getEntry<YamlScalar>(map, key, 'string'), 'string');
+  }
+
+  static T _parseNode<T>(YamlNode node, String typeDescription,
+      {T Function(String)? parse}) {
+    if (node is T) {
+      return node as T;
+    } else if (node is YamlScalar) {
+      final value = node.value;
+      if (parse != null) {
+        if (value is! String) {
+          _failAt('Expected a $typeDescription.', node);
+        }
+        return _wrapFormatException(
+            'Expected a $typeDescription.', node.span, () => parse(node.value));
+      } else if (value is T) {
+        return value;
+      }
+      _failAt('Expected a $typeDescription.', node);
+    }
+    _failAt('Expected a $typeDescription.', node);
+  }
+
+  static void _parseEachEntry<K, V>(
+      YamlMap map,
+      void Function(K key, V value) f,
+      String keyTypeDescription,
+      String valueTypeDescription) {
+    map.nodes.forEach((key, value) {
+      f(_parseNode(key, keyTypeDescription),
+          _parseNode(value, valueTypeDescription));
+    });
+  }
+
+  static T _getEntry<T>(
+    YamlMap map,
+    String key,
+    String type, {
+    bool required = true,
+  }) {
+    final entry = map.nodes[key];
+    // `null` here always means not present. A value explicitly mapped to `null`
+    // would be a `YamlScalar(null)`.
+    if (entry == null) {
+      if (required) {
+        _failAt('Expected a `$key` entry.', map);
+      } else {
+        return null as T;
+      }
+    }
+    return _parseNode(entry, type);
+  }
+
+  static Never _failAt(String message, YamlNode node) {
+    throw SourceSpanFormatException(message, node.span);
   }
 
   /// Returns a copy of this LockFile with a package named [name] removed.
@@ -241,12 +306,11 @@
         rootUri = p.toUri(rootPath);
       }
       final pubspec = await cache.describe(id);
-      final sdkConstraint = pubspec.sdkConstraints[sdk.identifier];
       entries.add(PackageConfigEntry(
         name: name,
         rootUri: rootUri,
         packageUri: p.toUri('lib/'),
-        languageVersion: LanguageVersion.fromSdkConstraint(sdkConstraint),
+        languageVersion: pubspec.languageVersion,
       ));
     }
 
@@ -304,17 +368,22 @@
 ''';
   }
 
+  static const _directMain = 'direct main';
+  static const _directDev = 'direct dev';
+  static const _directOverridden = 'direct overridden';
+  static const _transitive = 'transitive';
+
   /// Returns the dependency classification for [package].
   String _dependencyType(String package) {
-    if (_mainDependencies.contains(package)) return 'direct main';
-    if (_devDependencies.contains(package)) return 'direct dev';
+    if (_mainDependencies.contains(package)) return _directMain;
+    if (_devDependencies.contains(package)) return _directDev;
 
     // If a package appears in `dependency_overrides` and another dependency
     // section, the main section it appears in takes precedence.
     if (_overriddenDependencies.contains(package)) {
-      return 'direct overridden';
+      return _directOverridden;
     }
-    return 'transitive';
+    return _transitive;
   }
 
   /// `true` if [other] has the same packages as `this` in the same versions
diff --git a/lib/src/log.dart b/lib/src/log.dart
index f845aa6..61c6768 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -8,7 +8,6 @@
 import 'dart:io';
 
 import 'package:args/command_runner.dart';
-import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_span/source_span.dart';
 import 'package:stack_trace/stack_trace.dart';
@@ -341,7 +340,6 @@
 /// replacing it with '[...]' if it is too long.
 ///
 /// [limit] must be more than 5.
-@visibleForTesting
 String limitLength(String input, int limit) {
   const snip = '[...]';
   assert(limit > snip.length);
diff --git a/lib/src/null_safety_analysis.dart b/lib/src/null_safety_analysis.dart
index fe89b55..284e845 100644
--- a/lib/src/null_safety_analysis.dart
+++ b/lib/src/null_safety_analysis.dart
@@ -91,7 +91,7 @@
           }
         },
         sources: _systemCache.sources,
-        sdkConstraints: {'dart': rootPubspec.sdkConstraints['dart']!}));
+        sdkConstraints: {'dart': rootPubspec.dartSdkConstraint}));
 
     final rootLanguageVersion = rootPubspec.languageVersion;
     if (!rootLanguageVersion.supportsNullSafety) {
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 0b6cd5b..69b5557 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -120,6 +120,9 @@
 
   Map<String, PackageRange>? _devDependencies;
 
+  /// The Dart sdk version this is parsed against.
+  final Version _dartSdkVersion;
+
   /// The dependency constraints that this package overrides when it is the
   /// root package.
   ///
@@ -133,7 +136,7 @@
     if (pubspecOverridesFields != null) {
       pubspecOverridesFields.nodes.forEach((key, _) {
         if (!const {'dependency_overrides'}.contains(key.value)) {
-          throw PubspecException(
+          throw SourceSpanApplicationException(
             'pubspec_overrides.yaml only supports the `dependency_overrides` field.',
             key.span,
           );
@@ -163,13 +166,13 @@
 
   Map<String, PackageRange>? _dependencyOverrides;
 
-  /// A map from SDK identifiers to constraints on those SDK versions.
-  Map<String, VersionConstraint> get sdkConstraints {
-    _ensureEnvironment();
-    return _sdkConstraints!;
-  }
+  SdkConstraint get dartSdkConstraint => sdkConstraints['dart']!;
 
-  Map<String, VersionConstraint>? _sdkConstraints;
+  /// A map from SDK identifiers to constraints on those SDK versions.
+  late final Map<String, SdkConstraint> sdkConstraints =
+      _givenSdkConstraints ?? UnmodifiableMapView(_parseEnvironment(fields));
+
+  final Map<String, SdkConstraint>? _givenSdkConstraints;
 
   /// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this
   /// pubspec.
@@ -177,41 +180,8 @@
 
   /// Whether or not the SDK version was overridden from <2.0.0 to
   /// <2.0.0-dev.infinity.
-  bool get dartSdkWasOverridden => _dartSdkWasOverridden;
-  bool _dartSdkWasOverridden = false;
-
-  /// The original Dart SDK constraint as written in the pubspec.
-  ///
-  /// If [dartSdkWasOverridden] is `false`, this will be identical to
-  /// `sdkConstraints["dart"]`.
-  VersionConstraint get originalDartSdkConstraint {
-    _ensureEnvironment();
-    return _originalDartSdkConstraint ?? sdkConstraints['dart']!;
-  }
-
-  VersionConstraint? _originalDartSdkConstraint;
-
-  /// Ensures that the top-level "environment" field has been parsed and
-  /// [_sdkConstraints] is set accordingly.
-  void _ensureEnvironment() {
-    if (_sdkConstraints != null) return;
-
-    var sdkConstraints = _parseEnvironment(fields);
-    var parsedDartSdkConstraint = sdkConstraints['dart'];
-
-    if (parsedDartSdkConstraint is VersionRange &&
-        _shouldEnableCurrentSdk(parsedDartSdkConstraint)) {
-      _originalDartSdkConstraint = parsedDartSdkConstraint;
-      _dartSdkWasOverridden = true;
-      sdkConstraints['dart'] = VersionRange(
-          min: parsedDartSdkConstraint.min,
-          includeMin: parsedDartSdkConstraint.includeMin,
-          max: sdk.version,
-          includeMax: true);
-    }
-
-    _sdkConstraints = UnmodifiableMapView(sdkConstraints);
-  }
+  bool get dartSdkWasOverridden => _dartSdkWasOverriddenToAllowPrerelease;
+  bool _dartSdkWasOverriddenToAllowPrerelease = false;
 
   /// Whether or not we should override [sdkConstraint] to be <= the user's
   /// current SDK version.
@@ -227,12 +197,12 @@
   ///     patch versions as the user's current SDK.
   bool _shouldEnableCurrentSdk(VersionRange sdkConstraint) {
     if (!_allowPreReleaseSdk) return false;
-    if (!sdk.version.isPreRelease) return false;
+    if (!_dartSdkVersion.isPreRelease) return false;
     if (sdkConstraint.includeMax) return false;
     var minSdkConstraint = sdkConstraint.min;
     if (minSdkConstraint != null &&
         minSdkConstraint.isPreRelease &&
-        equalsIgnoringPreRelease(sdkConstraint.min!, sdk.version)) {
+        equalsIgnoringPreRelease(sdkConstraint.min!, _dartSdkVersion)) {
       return false;
     }
     var maxSdkConstraint = sdkConstraint.max;
@@ -241,59 +211,126 @@
         !maxSdkConstraint.isFirstPreRelease) {
       return false;
     }
-    return equalsIgnoringPreRelease(maxSdkConstraint, sdk.version);
+    return equalsIgnoringPreRelease(maxSdkConstraint, _dartSdkVersion);
+  }
+
+  SdkConstraint _interpretDartSdkConstraint(
+    VersionConstraint originalConstraint, {
+    required VersionConstraint? defaultUpperBoundConstraint,
+  }) {
+    VersionConstraint constraint = originalConstraint;
+    if (constraint is VersionRange && _shouldEnableCurrentSdk(constraint)) {
+      _dartSdkWasOverriddenToAllowPrerelease = true;
+      constraint = VersionRange(
+          min: constraint.min,
+          includeMin: constraint.includeMin,
+          max: _dartSdkVersion,
+          includeMax: true);
+    }
+    if (defaultUpperBoundConstraint != null &&
+        constraint is VersionRange &&
+        constraint.max == null &&
+        defaultUpperBoundConstraint.allowsAny(constraint)) {
+      constraint = VersionConstraint.intersection(
+          [constraint, defaultUpperBoundConstraint]);
+    }
+    // If a package is null safe it should also be compatible with dart 3.
+    // Therefore we rewrite a null-safety enabled constraint with the upper
+    // bound <3.0.0 to be have upper bound <4.0.0
+    if (constraint is VersionRange &&
+        LanguageVersion.fromSdkConstraint(constraint) >=
+            LanguageVersion.firstVersionWithNullSafety &&
+        // <3.0.0 is parsed into a max of 3.0.0-0, so that is what we look for
+        // here.
+        constraint.max == Version(3, 0, 0).firstPreRelease &&
+        constraint.includeMax == false) {
+      constraint = VersionRange(
+        min: constraint.min,
+        includeMin: constraint.includeMin,
+        // We don't have to use .firstPreRelease as the constructor will do that
+        // if needed.
+        max: Version(4, 0, 0),
+      );
+    }
+    return SdkConstraint(constraint, originalConstraint: originalConstraint);
+  }
+
+  // Flutter constraints get special treatment, as Flutter won't be using
+  // semantic versioning to mark breaking releases. We simply ignore upper
+  // bounds.
+  SdkConstraint _interpretFlutterSdkConstraint(VersionConstraint constraint) {
+    if (constraint is VersionRange) {
+      return SdkConstraint(
+        VersionRange(min: constraint.min, includeMin: constraint.includeMin),
+        originalConstraint: constraint,
+      );
+    }
+    return SdkConstraint(constraint);
   }
 
   /// Parses the "environment" field in [parent] and returns a map from SDK
   /// identifiers to constraints on those SDKs.
-  Map<String, VersionConstraint> _parseEnvironment(YamlMap parent) {
+  Map<String, SdkConstraint> _parseEnvironment(YamlMap parent) {
     var yaml = parent['environment'];
+    final VersionConstraint originalDartSdkConstraint;
     if (yaml == null) {
-      return {
-        'dart': _includeDefaultSdkConstraint
-            ? _defaultUpperBoundSdkConstraint
-            : VersionConstraint.any
-      };
-    }
-
-    if (yaml is! YamlMap) {
+      originalDartSdkConstraint = _includeDefaultSdkConstraint
+          ? _defaultUpperBoundSdkConstraint
+          : VersionConstraint.any;
+    } else if (yaml is! YamlMap) {
       _error('"environment" field must be a map.',
           parent.nodes['environment']!.span);
+    } else {
+      originalDartSdkConstraint = _parseVersionConstraint(
+        yaml.nodes['sdk'],
+        _packageName,
+        _FileType.pubspec,
+      );
     }
 
     var constraints = {
-      'dart': _parseVersionConstraint(
-          yaml.nodes['sdk'], _packageName, _FileType.pubspec,
-          defaultUpperBoundConstraint: _includeDefaultSdkConstraint
-              ? _defaultUpperBoundSdkConstraint
-              : null)
+      'dart': _interpretDartSdkConstraint(
+        originalDartSdkConstraint,
+        defaultUpperBoundConstraint: _includeDefaultSdkConstraint
+            ? _defaultUpperBoundSdkConstraint
+            : null,
+      )
     };
-    yaml.nodes.forEach((name, constraint) {
-      if (name.value is! String) {
-        _error('SDK names must be strings.', name.span);
-      } else if (name.value == 'dart') {
-        _error('Use "sdk" to for Dart SDK constraints.', name.span);
-      }
-      if (name.value == 'sdk') return;
 
-      constraints[name.value as String] =
-          _parseVersionConstraint(constraint, _packageName, _FileType.pubspec,
-              // Flutter constraints get special treatment, as Flutter won't be
-              // using semantic versioning to mark breaking releases.
-              ignoreUpperBound: name.value == 'flutter');
-    });
+    if (yaml is YamlMap) {
+      yaml.nodes.forEach((nameNode, constraintNode) {
+        final name = nameNode.value;
+        if (name is! String) {
+          _error('SDK names must be strings.', nameNode.span);
+        } else if (name == 'dart') {
+          _error('Use "sdk" to for Dart SDK constraints.', nameNode.span);
+        }
+        if (name == 'sdk') return;
 
+        final constraint = _parseVersionConstraint(
+          constraintNode,
+          _packageName,
+          _FileType.pubspec,
+        );
+        constraints[name] = name == 'flutter'
+            ? _interpretFlutterSdkConstraint(constraint)
+            : SdkConstraint(constraint);
+      });
+    }
     return constraints;
   }
 
   /// The language version implied by the sdk constraint.
-  LanguageVersion get languageVersion =>
-      LanguageVersion.fromSdkConstraint(originalDartSdkConstraint);
+  LanguageVersion get languageVersion {
+    return LanguageVersion.fromSdkConstraint(
+      dartSdkConstraint.originalConstraint,
+    );
+  }
 
   /// Loads the pubspec for a package located in [packageDir].
   ///
   /// If [expectedName] is passed and the pubspec doesn't have a matching name
-  /// field, this will throw a [PubspecException].
+  /// field, this will throw a [SourceSpanApplicationException].
   ///
   /// If [allowOverridesFile] is `true` [pubspecOverridesFilename] is loaded and
   /// is allowed to override dependency_overrides from `pubspec.yaml`.
@@ -332,7 +369,8 @@
     Iterable<PackageRange>? dependencyOverrides,
     Map? fields,
     SourceRegistry? sources,
-    Map<String, VersionConstraint>? sdkConstraints,
+    Map<String, SdkConstraint>? sdkConstraints,
+    Version? dartSdkVersion,
   })  : _dependencies = dependencies == null
             ? null
             : Map.fromIterable(dependencies, key: (range) => range.name),
@@ -342,12 +380,13 @@
         _dependencyOverrides = dependencyOverrides == null
             ? null
             : Map.fromIterable(dependencyOverrides, key: (range) => range.name),
-        _sdkConstraints = sdkConstraints ??
-            UnmodifiableMapView({'dart': VersionConstraint.any}),
+        _givenSdkConstraints = sdkConstraints ??
+            UnmodifiableMapView({'dart': SdkConstraint(VersionConstraint.any)}),
         _includeDefaultSdkConstraint = false,
         _sources = sources ??
             ((String? name) => throw StateError('No source registry given')),
         _overridesFileFields = null,
+        _dartSdkVersion = dartSdkVersion ?? sdk.version,
         super(
           fields == null ? YamlMap() : YamlMap.wrap(fields),
           name: name,
@@ -361,10 +400,17 @@
   /// field, this will throw a [PubspecError].
   ///
   /// [location] is the location from which this pubspec was loaded.
-  Pubspec.fromMap(Map fields, this._sources,
-      {YamlMap? overridesFields, String? expectedName, Uri? location})
-      : _overridesFileFields = overridesFields,
+  Pubspec.fromMap(
+    Map fields,
+    this._sources, {
+    YamlMap? overridesFields,
+    String? expectedName,
+    Uri? location,
+    Version? dartSdkVersion,
+  })  : _overridesFileFields = overridesFields,
         _includeDefaultSdkConstraint = true,
+        _givenSdkConstraints = null,
+        _dartSdkVersion = dartSdkVersion ?? sdk.version,
         super(fields is YamlMap
             ? fields
             : YamlMap.wrap(fields, sourceUrl: location)) {
@@ -373,7 +419,7 @@
     if (expectedName == null) return;
     if (name == expectedName) return;
 
-    throw PubspecException(
+    throw SourceSpanApplicationException(
         '"name" field doesn\'t match expected name '
         '"$expectedName".',
         this.fields.nodes['name']!.span);
@@ -390,6 +436,7 @@
     Uri? location,
     String? overridesFileContents,
     Uri? overridesLocation,
+    Version? dartSdkVersion,
   }) {
     late final YamlMap pubspecMap;
     YamlMap? overridesFileMap;
@@ -400,13 +447,17 @@
             loadYamlNode(overridesFileContents, sourceUrl: overridesLocation));
       }
     } on YamlException catch (error) {
-      throw PubspecException(error.message, error.span);
+      throw SourceSpanApplicationException(error.message, error.span);
     }
 
-    return Pubspec.fromMap(pubspecMap, sources,
-        overridesFields: overridesFileMap,
-        expectedName: expectedName,
-        location: location);
+    return Pubspec.fromMap(
+      pubspecMap,
+      sources,
+      overridesFields: overridesFileMap,
+      expectedName: expectedName,
+      location: location,
+      dartSdkVersion: dartSdkVersion,
+    );
   }
 
   /// Ensures that [node] is a mapping.
@@ -420,19 +471,20 @@
     } else if (node is YamlMap) {
       return node;
     } else {
-      throw PubspecException('The pubspec must be a YAML mapping.', node.span);
+      throw SourceSpanApplicationException(
+          'The pubspec must be a YAML mapping.', node.span);
     }
   }
 
   /// Returns a list of most errors in this pubspec.
   ///
   /// This will return at most one error for each field.
-  List<PubspecException> get allErrors {
-    var errors = <PubspecException>[];
+  List<SourceSpanApplicationException> get allErrors {
+    var errors = <SourceSpanApplicationException>[];
     void collectError(void Function() fn) {
       try {
         fn();
-      } on PubspecException catch (e) {
+      } on SourceSpanApplicationException catch (e) {
         errors.add(e);
       }
     }
@@ -444,7 +496,7 @@
     collectError(() => publishTo);
     collectError(() => executables);
     collectError(() => falseSecrets);
-    collectError(_ensureEnvironment);
+    collectError(() => sdkConstraints);
     return errors;
   }
 }
@@ -557,17 +609,13 @@
 
 /// Parses [node] to a [VersionConstraint].
 ///
-/// If or [defaultUpperBoundConstraint] is specified then it will be set as
-/// the max constraint if the original constraint doesn't have an upper
-/// bound and it is compatible with [defaultUpperBoundConstraint].
-///
-/// If [ignoreUpperBound] the max constraint is ignored.
+/// If or [defaultUpperBoundConstraint] is specified then it will be set as the
+/// max constraint if the original constraint doesn't have an upper bound and it
+/// is compatible with [defaultUpperBoundConstraint].
 VersionConstraint _parseVersionConstraint(
-    YamlNode? node, String? packageName, _FileType fileType,
-    {VersionConstraint? defaultUpperBoundConstraint,
-    bool ignoreUpperBound = false}) {
+    YamlNode? node, String? packageName, _FileType fileType) {
   if (node?.value == null) {
-    return defaultUpperBoundConstraint ?? VersionConstraint.any;
+    return VersionConstraint.any;
   }
   if (node!.value is! String) {
     _error('A version constraint must be a string.', node.span);
@@ -575,23 +623,12 @@
 
   return _wrapFormatException('version constraint', node.span, () {
     var constraint = VersionConstraint.parse(node.value);
-    if (defaultUpperBoundConstraint != null &&
-        constraint is VersionRange &&
-        constraint.max == null &&
-        defaultUpperBoundConstraint.allowsAny(constraint)) {
-      constraint = VersionConstraint.intersection(
-          [constraint, defaultUpperBoundConstraint]);
-    }
-    if (ignoreUpperBound && constraint is VersionRange) {
-      return VersionRange(
-          min: constraint.min, includeMin: constraint.includeMin);
-    }
     return constraint;
   }, packageName, fileType);
 }
 
 /// Runs [fn] and wraps any [FormatException] it throws in a
-/// [PubspecException].
+/// [SourceSpanApplicationException].
 ///
 /// [description] should be a noun phrase that describes whatever's being
 /// parsed or processed by [fn]. [span] should be the location of whatever's
@@ -611,7 +648,7 @@
     return fn();
   } on FormatException catch (e) {
     // If we already have a pub exception with a span, re-use that
-    if (e is PubspecException) rethrow;
+    if (e is SourceSpanApplicationException) rethrow;
 
     var msg = 'Invalid $description';
     final typeName = _fileTypeName(fileType);
@@ -624,9 +661,9 @@
   }
 }
 
-/// Throws a [PubspecException] with the given message.
+/// Throws a [SourceSpanApplicationException] with the given message.
 Never _error(String message, SourceSpan? span) {
-  throw PubspecException(message, span);
+  throw SourceSpanApplicationException(message, span);
 }
 
 enum _FileType {
@@ -642,3 +679,40 @@
       return 'pubspec override';
   }
 }
+
+/// There are special rules or interpreting SDK constraints, we take care to
+/// save the original constraint as found in pubspec.yaml.
+class SdkConstraint {
+  /// The constraint as written in the pubspec.yaml.
+  final VersionConstraint originalConstraint;
+
+  /// The constraint as interpreted by pub.
+  final VersionConstraint effectiveConstraint;
+
+  SdkConstraint(this.effectiveConstraint,
+      {VersionConstraint? originalConstraint})
+      : originalConstraint = originalConstraint ?? effectiveConstraint;
+
+  /// The language version of a constraint is determined from how it is written.
+  LanguageVersion get languageVersion =>
+      LanguageVersion.fromSdkConstraint(originalConstraint);
+
+  // We currently don't call this anywhere - so this is only for debugging
+  // purposes.
+  @override
+  String toString() {
+    if (effectiveConstraint != originalConstraint) {
+      return '$originalConstraint (interpreted as $effectiveConstraint)';
+    }
+    return effectiveConstraint.toString();
+  }
+
+  @override
+  operator ==(other) =>
+      other is SdkConstraint &&
+      other.effectiveConstraint == effectiveConstraint &&
+      other.originalConstraint == originalConstraint;
+
+  @override
+  int get hashCode => Object.hash(effectiveConstraint, originalConstraint);
+}
diff --git a/lib/src/pubspec_parse.dart b/lib/src/pubspec_parse.dart
index c86a706..c6babfb 100644
--- a/lib/src/pubspec_parse.dart
+++ b/lib/src/pubspec_parse.dart
@@ -7,7 +7,7 @@
 import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
 
-import 'exceptions.dart' show ApplicationException;
+import 'exceptions.dart';
 import 'utils.dart' show identifierRegExp, reservedWords;
 
 /// A regular expression matching allowed package names.
@@ -42,15 +42,18 @@
   String _lookupName() {
     final name = fields['name'];
     if (name == null) {
-      throw PubspecException('Missing the required "name" field.', fields.span);
+      throw SourceSpanApplicationException(
+          'Missing the required "name" field.', fields.span);
     } else if (name is! String) {
-      throw PubspecException(
+      throw SourceSpanApplicationException(
           '"name" field must be a string.', fields.nodes['name']?.span);
     } else if (!packageNameRegExp.hasMatch(name)) {
-      throw PubspecException('"name" field must be a valid Dart identifier.',
+      throw SourceSpanApplicationException(
+          '"name" field must be a valid Dart identifier.',
           fields.nodes['name']?.span);
     } else if (reservedWords.contains(name)) {
-      throw PubspecException('"name" field may not be a Dart reserved word.',
+      throw SourceSpanApplicationException(
+          '"name" field may not be a Dart reserved word.',
           fields.nodes['name']?.span);
     }
 
@@ -223,7 +226,7 @@
   bool get isPrivate => publishTo == 'none';
 
   /// Runs [fn] and wraps any [FormatException] it throws in a
-  /// [PubspecException].
+  /// [SourceSpanApplicationException].
   ///
   /// [description] should be a noun phrase that describes whatever's being
   /// parsed or processed by [fn]. [span] should be the location of whatever's
@@ -242,21 +245,13 @@
         msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency';
       }
       msg = '$msg: ${e.message}';
-      throw PubspecException(msg, span);
+      throw SourceSpanApplicationException(msg, span);
     }
   }
 
-  /// Throws a [PubspecException] with the given message.
+  /// Throws a [SourceSpanApplicationException] with the given message.
   @alwaysThrows
   void _error(String message, SourceSpan? span) {
-    throw PubspecException(message, span);
+    throw SourceSpanApplicationException(message, span);
   }
 }
-
-/// An exception thrown when parsing a pubspec.
-///
-/// These exceptions are often thrown lazily while accessing pubspec properties.
-class PubspecException extends SourceSpanFormatException
-    implements ApplicationException {
-  PubspecException(String message, SourceSpan? span) : super(message, span);
-}
diff --git a/lib/src/solver/package_lister.dart b/lib/src/solver/package_lister.dart
index ce2b4c1..f3b63b6 100644
--- a/lib/src/solver/package_lister.dart
+++ b/lib/src/solver/package_lister.dart
@@ -190,7 +190,7 @@
     try {
       pubspec = await withDependencyType(
           _dependencyType, () => _systemCache.describe(id));
-    } on PubspecException catch (error) {
+    } on SourceSpanApplicationException catch (error) {
       // The lockfile for the pubspec couldn't be parsed,
       log.fine('Failed to parse pubspec for $id:\n$error');
       _knownInvalidVersions = _knownInvalidVersions.union(id.version);
@@ -218,8 +218,11 @@
       for (var sdk in sdks.values) {
         if (!_matchesSdkConstraint(pubspec, sdk)) {
           return [
-            Incompatibility([Term(depender, true)],
-                SdkCause(pubspec.sdkConstraints[sdk.identifier], sdk))
+            Incompatibility(
+                [Term(depender, true)],
+                SdkCause(
+                    pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint,
+                    sdk))
           ];
         }
       }
@@ -321,12 +324,13 @@
         alwaysIncludeMaxPreRelease: true);
     _knownInvalidVersions = incompatibleVersions.union(_knownInvalidVersions);
 
-    var sdkConstraint = await foldAsync(
+    var sdkConstraint = await foldAsync<VersionConstraint, PackageId>(
         slice(versions, bounds.first, bounds.last + 1), VersionConstraint.empty,
-        (dynamic previous, dynamic version) async {
+        (previous, version) async {
       var pubspec = await _describeSafe(version);
       return previous.union(
-          pubspec.sdkConstraints[sdk.identifier] ?? VersionConstraint.any);
+          pubspec.sdkConstraints[sdk.identifier]?.effectiveConstraint ??
+              VersionConstraint.any);
     });
 
     return Incompatibility(
@@ -428,6 +432,7 @@
     var constraint = pubspec.sdkConstraints[sdk.identifier];
     if (constraint == null) return true;
 
-    return sdk.isAvailable && constraint.allows(sdk.version!);
+    return sdk.isAvailable &&
+        constraint.effectiveConstraint.allows(sdk.version!);
   }
 }
diff --git a/lib/src/solver/result.dart b/lib/src/solver/result.dart
index 7d57c36..df3aa4a 100644
--- a/lib/src/solver/result.dart
+++ b/lib/src/solver/result.dart
@@ -63,7 +63,7 @@
     var sdkConstraints = <String, VersionConstraint>{};
     for (var pubspec in nonOverrides) {
       pubspec.sdkConstraints.forEach((identifier, constraint) {
-        sdkConstraints[identifier] = constraint
+        sdkConstraints[identifier] = constraint.effectiveConstraint
             .intersect(sdkConstraints[identifier] ?? VersionConstraint.any);
       });
     }
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index ef04bc4..cbf930f 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -11,6 +11,7 @@
 import 'entrypoint.dart';
 import 'log.dart' as log;
 import 'sdk.dart';
+import 'validator/analyze.dart';
 import 'validator/changelog.dart';
 import 'validator/compiled_dartdoc.dart';
 import 'validator/dependency.dart';
@@ -74,7 +75,7 @@
   void validateSdkConstraint(Version firstSdkVersion, String message) {
     // If the SDK constraint disallowed all versions before [firstSdkVersion],
     // no error is necessary.
-    if (entrypoint.root.pubspec.originalDartSdkConstraint
+    if (entrypoint.root.pubspec.dartSdkConstraint.originalConstraint
         .intersect(VersionRange(max: firstSdkVersion))
         .isEmpty) {
       return;
@@ -95,7 +96,8 @@
             ? firstSdkVersion.nextPatch
             : firstSdkVersion.nextBreaking);
 
-    var newSdkConstraint = entrypoint.root.pubspec.originalDartSdkConstraint
+    var newSdkConstraint = entrypoint
+        .root.pubspec.dartSdkConstraint.originalConstraint
         .intersect(allowedSdks);
     if (newSdkConstraint.isEmpty) newSdkConstraint = allowedSdks;
 
@@ -130,6 +132,7 @@
       required List<String> warnings,
       required List<String> errors}) async {
     var validators = [
+      AnalyzeValidator(),
       GitignoreValidator(),
       PubspecValidator(),
       LicenseValidator(),
diff --git a/lib/src/validator/analyze.dart b/lib/src/validator/analyze.dart
new file mode 100644
index 0000000..b9af306
--- /dev/null
+++ b/lib/src/validator/analyze.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2022, 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:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../io.dart';
+
+import '../log.dart';
+import '../validator.dart';
+
+/// Runs `dart analyze` and gives a warning if it returns non-zero.
+class AnalyzeValidator extends Validator {
+  @override
+  Future<void> validate() async {
+    final result = await runProcess(Platform.resolvedExecutable, [
+      'analyze',
+      '--fatal-infos',
+      if (!p.equals(entrypoint.root.dir, p.current)) entrypoint.root.dir,
+    ]);
+    if (result.exitCode != 0) {
+      final limitedOutput = limitLength(result.stdout.join('\n'), 1000);
+      warnings
+          .add('`dart analyze` found the following issue(s):\n$limitedOutput');
+    }
+  }
+}
diff --git a/lib/src/validator/flutter_plugin_format.dart b/lib/src/validator/flutter_plugin_format.dart
index a56f7df..00160ca 100644
--- a/lib/src/validator/flutter_plugin_format.dart
+++ b/lib/src/validator/flutter_plugin_format.dart
@@ -46,7 +46,7 @@
     final flutterConstraint = pubspec.sdkConstraints['flutter'];
     if (usesNewPluginFormat &&
         (flutterConstraint == null ||
-            flutterConstraint.allowsAny(VersionRange(
+            flutterConstraint.effectiveConstraint.allowsAny(VersionRange(
               min: Version.parse('0.0.0'),
               max: Version.parse('1.10.0'),
               includeMin: true,
diff --git a/pubspec.yaml b/pubspec.yaml
index 4f52414..eb2983f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -6,7 +6,7 @@
 dependencies:
   # Note: Pub's test infrastructure assumes that any dependencies used in tests
   # will be hosted dependencies.
-  analyzer: ^4.0.0
+  analyzer: ^5.1.0
   args: ^2.1.0
   async: ^2.6.1
   cli_util: ^0.3.5
diff --git a/test/dart3_sdk_constraint_hack_test.dart b/test/dart3_sdk_constraint_hack_test.dart
new file mode 100644
index 0000000..7df3ee8
--- /dev/null
+++ b/test/dart3_sdk_constraint_hack_test.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:test/test.dart';
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+void main() {
+  test('The bound of ">=2.11.0 <3.0.0" is not modified', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.11.0 <3.0.0'}
+      }),
+    ]).create();
+
+    await pubGet(
+      error: contains(
+          'Because myapp requires SDK version >=2.11.0 <3.0.0, version solving failed'),
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
+  test('The bound of ">=2.12.0 <3.1.0" is not modified', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.12.0 <3.1.0'}
+      }),
+    ]).create();
+
+    await pubGet(
+      error: contains(
+          'Because myapp requires SDK version >=2.12.0 <3.1.0, version solving failed'),
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
+
+  test('The bound of ">=2.11.0 <2.999.0" is not modified', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.11.0 <2.999.0'}
+      }),
+    ]).create();
+
+    await pubGet(
+      error: contains(
+          'Because myapp requires SDK version >=2.11.0 <2.999.0, version solving failed'),
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
+
+  test('The bound of ">=2.11.0 <3.0.0-0.0" is not modified', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.11.0 <3.0.0-0.0'}
+      }),
+    ]).create();
+
+    await pubGet(
+      error: contains(
+          'Because myapp requires SDK version >=2.11.0 <3.0.0-0.0, version solving failed'),
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
+
+  test('The bound of ">=2.12.0 <3.0.0" is modified', () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.12.0 <3.0.0'}
+      }),
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'});
+  });
+
+  test('The bound of ">=2.12.0 <3.0.0-0" is modified', () async {
+    // For the upper bound <3.0.0 is treated as <3.0.0-0, so they both have
+    // the rewrite applied.
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.12.0 <3.0.0-0'}
+      }),
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'});
+  });
+
+  test(
+      'The bound of ">=2.12.0 <3.0.0" is not compatible with prereleases of dart 4',
+      () async {
+    await d.dir(appPath, [
+      d.pubspec({
+        'name': 'myapp',
+        'environment': {'sdk': '>=2.12.0 <3.0.0'}
+      }),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '4.0.0-alpha'},
+      error: contains(
+        'Because myapp requires SDK version >=2.12.0 <4.0.0, version solving failed.',
+      ),
+    );
+  });
+}
diff --git a/test/list_package_dirs/lockfile_error_test.dart b/test/list_package_dirs/lockfile_error_test.dart
index f4440ec..5c7bff9 100644
--- a/test/list_package_dirs/lockfile_error_test.dart
+++ b/test/list_package_dirs/lockfile_error_test.dart
@@ -19,7 +19,7 @@
       'list-package-dirs',
       '--format=json'
     ], outputJson: {
-      'error': contains('The lockfile must be a YAML mapping.'),
+      'error': contains('Expected a YAML mapping.'),
       'path': canonicalize(path.join(d.sandbox, appPath, 'pubspec.lock'))
     }, exitCode: exit_codes.DATA);
   });
diff --git a/test/pubspec_test.dart b/test/pubspec_test.dart
index 2ccb30f..23079c5 100644
--- a/test/pubspec_test.dart
+++ b/test/pubspec_test.dart
@@ -4,8 +4,8 @@
 
 import 'dart:io';
 
+import 'package:pub/src/exceptions.dart';
 import 'package:pub/src/pubspec.dart';
-import 'package:pub/src/sdk.dart';
 import 'package:pub/src/source/hosted.dart';
 import 'package:pub/src/system_cache.dart';
 import 'package:pub_semver/pub_semver.dart';
@@ -15,11 +15,12 @@
   group('parse()', () {
     final sources = SystemCache().sources;
 
-    var throwsPubspecException = throwsA(const TypeMatcher<PubspecException>());
+    var throwsPubspecException =
+        throwsA(const TypeMatcher<SourceSpanApplicationException>());
 
     void expectPubspecException(String contents, void Function(Pubspec) fn,
         [String? expectedContains]) {
-      var expectation = const TypeMatcher<PubspecException>();
+      var expectation = const TypeMatcher<SourceSpanApplicationException>();
       if (expectedContains != null) {
         expectation = expectation.having(
             (error) => error.message, 'message', contains(expectedContains));
@@ -390,7 +391,7 @@
           expect(
             () => pubspec.dependencies,
             throwsA(
-              isA<PubspecException>()
+              isA<SourceSpanApplicationException>()
                   .having((e) => e.span!.text, 'span.text', 'invalid value'),
             ),
           );
@@ -493,36 +494,23 @@
     });
 
     group('environment', () {
-      /// Checking for the default SDK constraint based on the current SDK.
-      void expectDefaultSdkConstraint(Pubspec pubspec) {
-        var sdkVersionString = sdk.version.toString();
-        if (sdkVersionString.startsWith('2.0.0') && sdk.version.isPreRelease) {
-          expect(
-              pubspec.sdkConstraints,
-              containsPair(
-                  'dart',
-                  VersionConstraint.parse(
-                      '${pubspec.sdkConstraints["dart"]} <=$sdkVersionString')));
-        } else {
-          expect(
-              pubspec.sdkConstraints,
-              containsPair(
-                  'dart',
-                  VersionConstraint.parse(
-                      "${pubspec.sdkConstraints["dart"]} <2.0.0")));
-        }
-      }
-
       test('allows an omitted environment', () {
         var pubspec = Pubspec.parse('name: testing', sources);
-        expectDefaultSdkConstraint(pubspec);
+        expect(
+          pubspec.dartSdkConstraint.effectiveConstraint,
+          VersionConstraint.parse('<2.0.0'),
+        );
+
         expect(pubspec.sdkConstraints, isNot(contains('flutter')));
         expect(pubspec.sdkConstraints, isNot(contains('fuchsia')));
       });
 
       test('default SDK constraint can be omitted with empty environment', () {
         var pubspec = Pubspec.parse('', sources);
-        expectDefaultSdkConstraint(pubspec);
+        expect(
+          pubspec.dartSdkConstraint.effectiveConstraint,
+          VersionConstraint.parse('<2.0.0'),
+        );
         expect(pubspec.sdkConstraints, isNot(contains('flutter')));
         expect(pubspec.sdkConstraints, isNot(contains('fuchsia')));
       });
@@ -533,7 +521,10 @@
   environment:
     sdk: ">1.0.0"
   ''', sources);
-        expectDefaultSdkConstraint(pubspec);
+        expect(
+          pubspec.dartSdkConstraint.effectiveConstraint,
+          VersionConstraint.parse('>1.0.0 <2.0.0'),
+        );
         expect(pubspec.sdkConstraints, isNot(contains('flutter')));
         expect(pubspec.sdkConstraints, isNot(contains('fuchsia')));
       });
@@ -545,8 +536,12 @@
   environment:
     sdk: ">3.0.0"
   ''', sources);
-        expect(pubspec.sdkConstraints,
-            containsPair('dart', VersionConstraint.parse('>3.0.0')));
+        expect(
+            pubspec.sdkConstraints,
+            containsPair(
+              'dart',
+              SdkConstraint(VersionConstraint.parse('>3.0.0')),
+            ));
         expect(pubspec.sdkConstraints, isNot(contains('flutter')));
         expect(pubspec.sdkConstraints, isNot(contains('fuchsia')));
       });
@@ -563,12 +558,23 @@
   flutter: ^0.1.2
   fuchsia: ^5.6.7
 ''', sources);
-        expect(pubspec.sdkConstraints,
-            containsPair('dart', VersionConstraint.parse('>=1.2.3 <2.3.4')));
-        expect(pubspec.sdkConstraints,
-            containsPair('flutter', VersionConstraint.parse('>=0.1.2')));
-        expect(pubspec.sdkConstraints,
-            containsPair('fuchsia', VersionConstraint.parse('^5.6.7')));
+        expect(
+            pubspec.sdkConstraints,
+            containsPair('dart',
+                SdkConstraint(VersionConstraint.parse('>=1.2.3 <2.3.4'))));
+        expect(
+            pubspec.sdkConstraints,
+            containsPair(
+                'flutter',
+                SdkConstraint(
+                  VersionConstraint.parse('>=0.1.2'),
+                  originalConstraint: VersionConstraint.parse('^0.1.2'),
+                )));
+        expect(
+          pubspec.sdkConstraints,
+          containsPair(
+              'fuchsia', SdkConstraint(VersionConstraint.parse('^5.6.7'))),
+        );
       });
 
       test("throws if the sdk isn't a string", () {
@@ -700,7 +706,7 @@
         void Function(Pubspec) fn, [
         String? expectedContains,
       ]) {
-        var expectation = isA<PubspecException>();
+        var expectation = isA<SourceSpanApplicationException>();
         if (expectedContains != null) {
           expectation = expectation.having((error) => error.toString(),
               'toString()', contains(expectedContains));
diff --git a/test/upgrade/upgrade_null_safety_test.dart b/test/upgrade/upgrade_null_safety_test.dart
index 557c589..ad210d8 100644
--- a/test/upgrade/upgrade_null_safety_test.dart
+++ b/test/upgrade/upgrade_null_safety_test.dart
@@ -441,7 +441,7 @@
         },
         error: allOf(
           contains('Because myapp depends on has_conflict >=2.0.0 which'),
-          contains('requires SDK version >=2.13.0 <3.0.0,'),
+          contains('requires SDK version >=2.13.0 <4.0.0,'),
           contains('version solving failed.'),
         ),
       );
diff --git a/test/validator/analyze_test.dart b/test/validator/analyze_test.dart
new file mode 100644
index 0000000..0717b3c
--- /dev/null
+++ b/test/validator/analyze_test.dart
@@ -0,0 +1,102 @@
+// Copyright (c) 2022, 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/exit_codes.dart';
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+
+Future<void> expectValidation(
+  error,
+  int exitCode, {
+  List<String> extraArgs = const [],
+  Map<String, String> environment = const {},
+  String? workingDirectory,
+}) async {
+  await runPub(
+    error: error,
+    args: ['publish', '--dry-run', ...extraArgs],
+    environment: {'_PUB_TEST_SDK_VERSION': '1.12.0', ...environment},
+    workingDirectory: workingDirectory ?? d.path(appPath),
+    exitCode: exitCode,
+  );
+}
+
+void main() {
+  test('should consider a package valid if it contains no warnings or errors',
+      () async {
+    await d.dir(appPath, [
+      d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'),
+      d.file('LICENSE', 'Eh, do what you want.'),
+      d.file('README.md', "This package isn't real."),
+      d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'),
+      d.dir('lib', [d.file('test_pkg.dart', 'int i = 1;')])
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'});
+
+    await expectValidation(contains('Package has 0 warnings.'), 0);
+  });
+
+  test('should warn if package contains errors, and works with --directory',
+      () async {
+    await d.dir(appPath, [
+      d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'),
+      d.file('LICENSE', 'Eh, do what you want.'),
+      d.file('README.md', "This package isn't real."),
+      d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'),
+      d.dir('lib', [
+        d.file('test_pkg.dart', '''
+void main() {
+// Missing }
+''')
+      ])
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'});
+
+    await expectValidation(
+      allOf([
+        contains('`dart analyze` found the following issue(s):'),
+        contains('Analyzing myapp...'),
+        contains('error -'),
+        contains("Expected to find '}'."),
+        contains('Package has 1 warning.')
+      ]),
+      DATA,
+      extraArgs: ['--directory', appPath],
+      workingDirectory: d.sandbox,
+    );
+  });
+
+  test('should warn if package contains infos', () async {
+    await d.dir(appPath, [
+      d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <=2.0.0'),
+      d.file('LICENSE', 'Eh, do what you want.'),
+      d.file('README.md', "This package isn't real."),
+      d.file('CHANGELOG.md', '# 1.0.0\nFirst version\n'),
+      d.dir('lib', [
+        d.file('test_pkg.dart', '''
+void main() {
+  final a = 10; // Unused.
+}
+''')
+      ]),
+    ]).create();
+
+    await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '1.12.0'});
+
+    await expectValidation(
+      allOf([
+        contains('`dart analyze` found the following issue(s):'),
+        contains('Analyzing myapp...'),
+        contains('info -'),
+        contains("The value of the local variable 'a' isn't used"),
+        contains('Package has 1 warning.')
+      ]),
+      DATA,
+    );
+  });
+}
diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart
index 69fc4a8..b3adc34 100644
--- a/test/validator/dependency_test.dart
+++ b/test/validator/dependency_test.dart
@@ -39,10 +39,10 @@
 }
 
 Future<void> expectValidationWarning(error,
-    {Map<String, String> environment = const {}}) async {
+    {int count = 1, Map<String, String> environment = const {}}) async {
   if (error is String) error = contains(error);
   await expectValidation(
-    error: allOf([error, contains('Package has 1 warning.')]),
+    error: allOf([error, contains('Package has $count warning')]),
     exitCode: DATA,
     environment: environment,
   );
@@ -106,7 +106,9 @@
       await expectValidationWarning(
           allOf([
             contains('  foo: any'),
+            contains("Publishable packages can't have 'git' dependencies"),
           ]),
+          count: 2,
           environment: {'_PUB_TEST_SDK_VERSION': '2.0.0'});
     });
 
diff --git a/test/version_solver_test.dart b/test/version_solver_test.dart
index 69489a3..8ade3f6 100644
--- a/test/version_solver_test.dart
+++ b/test/version_solver_test.dart
@@ -1172,7 +1172,7 @@
 
         await expectResolves(
           environment: {'_PUB_TEST_SDK_VERSION': '2.0.0-dev.99'},
-          // Log output should mention the PUB_ALLOW_RELEASE_SDK environment
+          // Log output should mention the PUB_ALLOW_PRERELEASE_SDK environment
           // variable and mention the foo and bar packages specifically.
           output: allOf(contains('PUB_ALLOW_PRERELEASE_SDK'),
               anyOf(contains('bar, foo'))),