Warn when non-prerelease depends on pre-release SDK or package (#2356)

diff --git a/lib/src/validator/dependency.dart b/lib/src/validator/dependency.dart
index ba5da98..054fe9c 100644
--- a/lib/src/validator/dependency.dart
+++ b/lib/src/validator/dependency.dart
@@ -75,18 +75,22 @@
           validateSdkConstraint(_firstGitPathVersion,
               "Older versions of pub don't support Git path dependencies.");
         }
-      } else if (constraint.isAny) {
-        _warnAboutNoConstraint(dependency);
-      } else if (constraint is Version) {
-        _warnAboutSingleVersionConstraint(dependency);
-      } else if (constraint is VersionRange) {
-        if (constraint.min == null) {
-          _warnAboutNoConstraintLowerBound(dependency);
-        } else if (constraint.max == null) {
-          _warnAboutNoConstraintUpperBound(dependency);
+      } else {
+        if (constraint.isAny) {
+          _warnAboutNoConstraint(dependency);
+        } else if (constraint is VersionRange) {
+          if (constraint is Version) {
+            _warnAboutSingleVersionConstraint(dependency);
+          } else {
+            _warnAboutPrerelease(dependency.name, constraint);
+            if (constraint.min == null) {
+              _warnAboutNoConstraintLowerBound(dependency);
+            } else if (constraint.max == null) {
+              _warnAboutNoConstraintUpperBound(dependency);
+            }
+          }
+          _hasCaretDep = _hasCaretDep || constraint.toString().startsWith('^');
         }
-
-        _hasCaretDep = _hasCaretDep || constraint.toString().startsWith('^');
       }
 
       _hasFeatures = _hasFeatures || dependency.features.isNotEmpty;
@@ -231,4 +235,18 @@
             'Without an upper bound, you\'re promising to support '
             '${log.bold("all")} future versions of ${dep.name}.');
   }
+
+  void _warnAboutPrerelease(String dependencyName, VersionRange constraint) {
+    final packageVersion = entrypoint.root.version;
+    if (constraint.min != null &&
+        constraint.min.isPreRelease &&
+        !packageVersion.isPreRelease) {
+      warnings.add('Packages dependent on a pre-release of another package '
+          'should themselves be published as a pre-release version. '
+          'If this package needs $dependencyName version ${constraint.min}, '
+          'consider publishing the package as a pre-release instead.\n'
+          'See https://dart.dev/tools/pub/publishing#publishing-prereleases '
+          'For more information on pre-releases.');
+    }
+  }
 }
diff --git a/lib/src/validator/sdk_constraint.dart b/lib/src/validator/sdk_constraint.dart
index 7465a37..7799cf0 100644
--- a/lib/src/validator/sdk_constraint.dart
+++ b/lib/src/validator/sdk_constraint.dart
@@ -10,8 +10,13 @@
 import '../sdk.dart';
 import '../validator.dart';
 
-/// A validator that validates that a package's SDK constraint doesn't use the
-/// "^" syntax.
+/// A validator of the SDK constraint.
+///
+/// Validates that a package's SDK constraint:
+/// * doesn't use the "^" syntax.
+/// * has an upper bound.
+/// * is not depending on a prerelease, unless the package itself is a
+/// prerelease.
 class SdkConstraintValidator extends Validator {
   SdkConstraintValidator(Entrypoint entrypoint) : super(entrypoint);
 
@@ -42,6 +47,21 @@
             'See https://dart.dev/tools/pub/pubspec#sdk-constraints for '
             'instructions on setting an sdk version constraint.');
       }
+
+      final constraintMin = dartConstraint.min;
+      final packageVersion = entrypoint.root.version;
+
+      if (constraintMin != null &&
+          constraintMin.isPreRelease &&
+          !packageVersion.isPreRelease) {
+        warnings.add(
+            'Packages with an SDK constraint on a pre-release of the Dart SDK '
+            'should themselves be published as a pre-release version. '
+            'If this package needs Dart version $constraintMin, consider '
+            'publishing the package as a pre-release instead.\n'
+            'See https://dart.dev/tools/pub/publishing#publishing-prereleases '
+            'For more information on pre-releases.');
+      }
     }
 
     for (var sdk in sdks.values) {
diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart
index 31ed937..3d93b68 100644
--- a/test/validator/dependency_test.dart
+++ b/test/validator/dependency_test.dart
@@ -73,6 +73,19 @@
       expectNoValidationError(dependency);
     });
 
+    test('with a dependency on a pre-release while being one', () async {
+      await d.dir(appPath, [
+        d.libPubspec(
+          'test_pkg',
+          '1.0.0-dev',
+          deps: {'foo': '^1.2.3-dev'},
+          sdk: '>=1.19.0 <2.0.0',
+        )
+      ]).create();
+
+      expectNoValidationError(dependency);
+    });
+
     test('has a git path dependency with an appropriate SDK constraint',
         () async {
       await d.dir(appPath, [
@@ -325,6 +338,18 @@
       });
     });
 
+    test('with a dependency on a pre-release without being one', () async {
+      await d.dir(appPath, [
+        d.libPubspec(
+          'test_pkg',
+          '1.0.0',
+          deps: {'foo': '^1.2.3-dev'},
+          sdk: '>=1.19.0 <2.0.0',
+        )
+      ]).create();
+
+      expectDependencyValidationWarning('Packages dependent on a pre-release');
+    });
     test(
         'with a single-version dependency and it should suggest a '
         'constraint based on the version', () async {
diff --git a/test/validator/sdk_constraint_test.dart b/test/validator/sdk_constraint_test.dart
index 554df49..d610bb8 100644
--- a/test/validator/sdk_constraint_test.dart
+++ b/test/validator/sdk_constraint_test.dart
@@ -28,6 +28,13 @@
       expectNoValidationError(sdkConstraint);
     });
 
+    test('depends on a pre-release Dart SDK from a pre-release', () async {
+      await d.dir(appPath, [
+        d.libPubspec('test_pkg', '1.0.0-dev.1', sdk: '>=1.8.0-dev.1 <2.0.0')
+      ]).create();
+      expectNoValidationError(sdkConstraint);
+    });
+
     test(
         'has a Flutter SDK constraint with an appropriate Dart SDK '
         'constraint', () async {
@@ -47,7 +54,7 @@
       await d.dir(appPath, [
         d.pubspec({
           'name': 'test_pkg',
-          'version': '1.0.0',
+          'version': '1.0.0-dev.1',
           'environment': {'sdk': '>=2.0.0-dev.51.0 <2.0.0', 'fuchsia': '^1.2.3'}
         })
       ]).create();
@@ -120,7 +127,7 @@
       await d.dir(appPath, [
         d.pubspec({
           'name': 'test_pkg',
-          'version': '1.0.0',
+          'version': '1.0.0-dev.1',
           'environment': {'sdk': '>=2.0.0-dev.50.0 <2.0.0', 'fuchsia': '^1.2.3'}
         })
       ]).create();
@@ -143,5 +150,21 @@
           completion(
               pairOf(anyElement(contains('">=2.0.0 <3.0.0"')), isEmpty)));
     });
+
+    test('depends on a pre-release sdk from a non-pre-release', () async {
+      await d.dir(appPath, [
+        d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0-dev.1 <2.0.0')
+      ]).create();
+      expect(
+        validatePackage(sdkConstraint),
+        completion(
+          pairOf(
+            isEmpty,
+            anyElement(contains(
+                'consider publishing the package as a pre-release instead')),
+          ),
+        ),
+      );
+    });
   });
 }