diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 897af2286b1e1a96a36f7bbf2ad657e2fb1400af..b52a139ed670156c1ad2ef4d8d2bef0ba840ab19 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -100,7 +100,7 @@ class Entrypoint {
   /// directory, respecting the [LockFile] if present. Returns a [Future] that
   /// completes when all dependencies are installed.
   Future installDependencies() {
-    return _loadLockFile()
+    return loadLockFile()
       .chain((lockFile) => resolveVersions(cache.sources, root, lockFile))
       .chain(_installDependencies);
   }
@@ -117,7 +117,7 @@ class Entrypoint {
   /// other dependencies as specified by the [LockFile] if possible. Returns a
   /// [Future] that completes when all dependencies are installed.
   Future updateDependencies(List<String> dependencies) {
-    return _loadLockFile().chain((lockFile) {
+    return loadLockFile().chain((lockFile) {
       var versionSolver = new VersionSolver(cache.sources, root, lockFile);
       for (var dependency in dependencies) {
         versionSolver.useLatestVersion(dependency);
@@ -141,10 +141,7 @@ class Entrypoint {
 
   /// Loads the list of concrete package versions from the `pubspec.lock`, if it
   /// exists. If it doesn't, this completes to an empty [LockFile].
-  ///
-  /// If there's an error reading the `pubspec.lock` file, this will print a
-  /// warning message and act as though the file doesn't exist.
-  Future<LockFile> _loadLockFile() {
+  Future<LockFile> loadLockFile() {
     var lockFilePath = join(root.dir, 'pubspec.lock');
 
     log.fine("Loading lockfile.");
diff --git a/lib/src/http.dart b/lib/src/http.dart
index abc3c435c3594329f44cfabce182264ba9d85d6c..0665c355cd26d6bfd203a163c379c10109d3b72e 100644
--- a/lib/src/http.dart
+++ b/lib/src/http.dart
@@ -22,10 +22,10 @@ final HTTP_TIMEOUT = 30 * 1000;
 /// An HTTP client that transforms 40* errors and socket exceptions into more
 /// user-friendly error messages.
 class PubHttpClient extends http.BaseClient {
-  final http.Client _inner;
+  http.Client inner;
 
   PubHttpClient([http.Client inner])
-    : _inner = inner == null ? new http.Client() : inner;
+    : this.inner = inner == null ? new http.Client() : inner;
 
   Future<http.StreamedResponse> send(http.BaseRequest request) {
     // TODO(rnystrom): Log request body when it's available and plaintext, but
@@ -41,7 +41,7 @@ class PubHttpClient extends http.BaseClient {
 
     // TODO(nweiz): Ideally the timeout would extend to reading from the
     // response input stream, but until issue 3657 is fixed that's not feasible.
-    return timeout(_inner.send(request).chain((streamedResponse) {
+    return timeout(inner.send(request).chain((streamedResponse) {
       log.fine("Got response ${streamedResponse.statusCode} "
                "${streamedResponse.reasonPhrase}.");
 
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 5c0c8507ce5e430629c677341f4737d2aedb681b..48f7768dd09c5e77fef39a325f08a98ef3b6b544 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -9,6 +9,7 @@ import 'log.dart' as log;
 import 'io.dart';
 import 'system_cache.dart';
 import 'utils.dart';
+import 'validator/dependency.dart';
 import 'validator/lib.dart';
 import 'validator/license.dart';
 import 'validator/name.dart';
@@ -44,7 +45,8 @@ abstract class Validator {
       new LibValidator(entrypoint),
       new LicenseValidator(entrypoint),
       new NameValidator(entrypoint),
-      new PubspecFieldValidator(entrypoint)
+      new PubspecFieldValidator(entrypoint),
+      new DependencyValidator(entrypoint)
     ];
 
     // TODO(nweiz): The sleep 0 here forces us to go async. This works around
diff --git a/lib/src/validator/dependency.dart b/lib/src/validator/dependency.dart
new file mode 100644
index 0000000000000000000000000000000000000000..66d2ae22f53cddb804d42096f8d7c2cb46674fc3
--- /dev/null
+++ b/lib/src/validator/dependency.dart
@@ -0,0 +1,91 @@
+// 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.
+
+library dependency_validator;
+
+import '../entrypoint.dart';
+import '../hosted_source.dart';
+import '../http.dart';
+import '../package.dart';
+import '../utils.dart';
+import '../validator.dart';
+import '../version.dart';
+
+/// A validator that validates a package's dependencies.
+class DependencyValidator extends Validator {
+  DependencyValidator(Entrypoint entrypoint)
+    : super(entrypoint);
+
+  Future validate() {
+    return Futures.forEach(entrypoint.root.pubspec.dependencies, (dependency) {
+      if (dependency.source is! HostedSource) {
+        return _warnAboutSource(dependency);
+      }
+
+      if (dependency.constraint.isAny &&
+          // TODO(nweiz): once we have development dependencies (issue 5358), we
+          // should warn about unittest. Until then, it's reasonable not to put
+          // a constraint on it.
+          dependency.name != 'unittest') {
+        return _warnAboutConstraint(dependency);
+      }
+
+      return new Future.immediate(null);
+    });
+  }
+
+  /// Warn that dependencies should use the hosted source.
+  Future _warnAboutSource(PackageRef ref) {
+    return entrypoint.cache.sources['hosted']
+        .getVersions(ref.name, ref.name)
+        .transformException((e) => <Version>[])
+        .transform((versions) {
+      var constraint;
+      var primary = Version.primary(versions);
+      if (primary != null) {
+        constraint = _constraintForVersion(primary);
+      } else {
+        constraint = ref.constraint.toString();
+        if (!ref.constraint.isAny && ref.constraint is! Version) {
+          constraint = '"$constraint"';
+        }
+      }
+
+      warnings.add('Don\'t depend on "${ref.name}" from the ${ref.source.name} '
+              'source. Use the hosted source instead. For example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${ref.name}: $constraint\n'
+          '\n'
+          'Using the hosted source ensures that everyone can download your '
+              'package\'s dependencies along with your package.');
+    });
+  }
+
+  /// Warn that dependencies should have version constraints.
+  Future _warnAboutConstraint(PackageRef ref) {
+    return entrypoint.loadLockFile().transform((lockFile) {
+      var message = 'Your dependency on "${ref.name}" should have a version '
+          'constraint.';
+      var locked = lockFile.packages[ref.name];
+      if (locked != null) {
+        message = '$message For example:\n'
+          '\n'
+          'dependencies:\n'
+          '  ${ref.name}: ${_constraintForVersion(locked.version)}\n';
+      }
+      warnings.add("$message\n"
+          "Without a constraint, you're promising to support all future "
+          "versions of ${ref.name}.");
+    });
+  }
+
+  /// Returns the suggested version constraint for a dependency that was tested
+  /// against [version].
+  String _constraintForVersion(Version version) {
+    if (version.major != 0) return '">=$version <${version.major + 1}.0.0"';
+    return '">=$version <${version.major}.${version.minor}.'
+        '${version.patch + 1}"';
+  }
+}
diff --git a/lib/src/version.dart b/lib/src/version.dart
index 567a7a7117434e6fbeca619036740dfab9e264fb..f60ad19cf6a77cdd50c5eaae55568ae7c6f427cf 100644
--- a/lib/src/version.dart
+++ b/lib/src/version.dart
@@ -70,6 +70,20 @@ class Version implements Comparable, VersionConstraint {
     }
   }
 
+  /// Returns the primary version out of a list of candidates. This is the
+  /// highest-numbered stable (non-prerelease) version. If there are no stable
+  /// versions, it's just the highest-numbered version.
+  static Version primary(List<Version> versions) {
+    var primary;
+    for (var version in versions) {
+      if (primary == null || (!version.isPreRelease && primary.isPreRelease) ||
+          (version.isPreRelease == primary.isPreRelease && version > primary)) {
+        primary = version;
+      }
+    }
+    return primary;
+  }
+
   bool operator ==(other) {
     if (other is! Version) return false;
     return compareTo(other) == 0;
@@ -80,8 +94,12 @@ class Version implements Comparable, VersionConstraint {
   bool operator <=(Version other) => compareTo(other) <= 0;
   bool operator >=(Version other) => compareTo(other) >= 0;
 
+  bool get isAny => false;
   bool get isEmpty => false;
 
+  /// Whether or not this is a pre-release version.
+  bool get isPreRelease => preRelease != null;
+
   /// Tests if [other] matches this version exactly.
   bool allows(Version other) => this == other;
 
@@ -234,6 +252,9 @@ abstract class VersionConstraint {
   /// Returns `true` if this constraint allows no versions.
   bool get isEmpty;
 
+  /// Returns `true` if this constraint allows all versions.
+  bool get isAny;
+
   /// Returns `true` if this constraint allows [version].
   bool allows(Version version);
 
@@ -297,6 +318,8 @@ class VersionRange implements VersionConstraint {
 
   bool get isEmpty => false;
 
+  bool get isAny => !includeMin && !includeMax;
+
   /// Tests if [other] matches falls within this version range.
   bool allows(Version other) {
     if (min != null && other < min) return false;
@@ -391,6 +414,7 @@ class _EmptyVersion implements VersionConstraint {
   const _EmptyVersion();
 
   bool get isEmpty => true;
+  bool get isAny => false;
   bool allows(Version other) => false;
   VersionConstraint intersect(VersionConstraint other) => this;
   String toString() => '<empty>';
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 44290b02e5c01cb0021a2386468ca0adb714c018..c9c8cc02c23807aa6bdc07d9265240cddcdcde7b 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -19,10 +19,12 @@ import 'dart:uri';
 import '../../../pkg/oauth2/lib/oauth2.dart' as oauth2;
 import '../../../pkg/path/lib/path.dart' as path;
 import '../../../pkg/unittest/lib/unittest.dart';
+import '../../../pkg/http/lib/testing.dart';
 import '../../lib/file_system.dart' as fs;
 import '../../pub/entrypoint.dart';
 import '../../pub/git_source.dart';
 import '../../pub/hosted_source.dart';
+import '../../pub/http.dart';
 import '../../pub/io.dart';
 import '../../pub/sdk_source.dart';
 import '../../pub/system_cache.dart';
@@ -712,6 +714,18 @@ void ensureGit() {
   });
 }
 
+/// Use [client] as the mock HTTP client for this test.
+///
+/// Note that this will only affect HTTP requests made via http.dart in the
+/// parent process.
+void useMockClient(MockClient client) {
+  var oldInnerClient = httpClient.inner;
+  httpClient.inner = client;
+  _scheduleCleanup((_) {
+    httpClient.inner = oldInnerClient;
+  });
+}
+
 Future<Directory> _setUpSandbox() => createTempDir();
 
 Future _runScheduled(Directory parentDir, List<_ScheduledEvent> scheduled) {
diff --git a/test/validator_test.dart b/test/validator_test.dart
index 40576ac34ca7ee23a42cca53364a9103441d1008..6f5764a95a4beabf134f6f640d83c019a743f784 100644
--- a/test/validator_test.dart
+++ b/test/validator_test.dart
@@ -9,9 +9,12 @@ import 'dart:json';
 
 import 'test_pub.dart';
 import '../../../pkg/unittest/lib/unittest.dart';
+import '../../../pkg/http/lib/http.dart' as http;
+import '../../../pkg/http/lib/testing.dart';
 import '../../pub/entrypoint.dart';
 import '../../pub/io.dart';
 import '../../pub/validator.dart';
+import '../../pub/validator/dependency.dart';
 import '../../pub/validator/lib.dart';
 import '../../pub/validator/license.dart';
 import '../../pub/validator/name.dart';
@@ -29,6 +32,9 @@ void expectValidationWarning(ValidatorCreator fn) {
   expectLater(schedulePackageValidation(fn), pairOf(isEmpty, isNot(isEmpty)));
 }
 
+Validator dependency(Entrypoint entrypoint) =>
+  new DependencyValidator(entrypoint);
+
 Validator lib(Entrypoint entrypoint) => new LibValidator(entrypoint);
 
 Validator license(Entrypoint entrypoint) => new LicenseValidator(entrypoint);
@@ -46,7 +52,10 @@ main() {
 
     test('looks normal', () {
       dir(appPath, [libPubspec("test_pkg", "1.0.0")]).scheduleCreate();
+      expectNoValidationError(dependency);
+      expectNoValidationError(lib);
       expectNoValidationError(license);
+      expectNoValidationError(name);
       expectNoValidationError(pubspecField);
       run();
     });
@@ -102,6 +111,16 @@ main() {
       expectNoValidationError(lib);
       run();
     });
+
+    test('has an unconstrained dependency on "unittest"', () {
+      dir(appPath, [
+        libPubspec("test_pkg", "1.0.0", [
+          {'hosted': 'unittest'}
+        ])
+      ]).scheduleCreate();
+      expectNoValidationError(dependency);
+      run();
+    });
   });
 
   group('should consider a package invalid if it', () {
@@ -283,5 +302,234 @@ main() {
       expectValidationError(lib);
       run();
     });
+
+    group('has a dependency with a non-hosted source', () {
+      group('where a hosted version of that dependency exists', () {
+        test("and should suggest the hosted package's primary version", () {
+          useMockClient(new MockClient((request) {
+            expect(request.method, equals("GET"));
+            expect(request.url.path, equals("/packages/foo.json"));
+
+            return new Future.immediate(new http.Response(JSON.stringify({
+              "name": "foo",
+              "uploaders": ["nweiz@google.com"],
+              "versions": ["3.0.0-pre", "2.0.0", "1.0.0"]
+            }), 200));
+          }));
+
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'git': 'git://github.com/dart-lang/foo'}
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=2.0.0 <3.0.0"'))));
+
+          run();
+        });
+
+        test("and should suggest the hosted package's prerelease version if "
+            "it's the only version available", () {
+          useMockClient(new MockClient((request) {
+            expect(request.method, equals("GET"));
+            expect(request.url.path, equals("/packages/foo.json"));
+
+            return new Future.immediate(new http.Response(JSON.stringify({
+              "name": "foo",
+              "uploaders": ["nweiz@google.com"],
+              "versions": ["3.0.0-pre", "2.0.0-pre"]
+            }), 200));
+          }));
+
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'git': 'git://github.com/dart-lang/foo'}
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=3.0.0-pre <4.0.0"'))));
+
+          run();
+        });
+
+        test("and should suggest a tighter constraint if the primary version "
+            "is pre-1.0.0", () {
+          useMockClient(new MockClient((request) {
+            expect(request.method, equals("GET"));
+            expect(request.url.path, equals("/packages/foo.json"));
+
+            return new Future.immediate(new http.Response(JSON.stringify({
+              "name": "foo",
+              "uploaders": ["nweiz@google.com"],
+              "versions": ["0.0.1", "0.0.2"]
+            }), 200));
+          }));
+
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'git': 'git://github.com/dart-lang/foo'}
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=0.0.2 <0.0.3"'))));
+
+          run();
+        });
+      });
+
+      group('where no hosted version of that dependency exists', () {
+        test("and should use the other source's version", () {
+          useMockClient(new MockClient((request) {
+            expect(request.method, equals("GET"));
+            expect(request.url.path, equals("/packages/foo.json"));
+
+            return new Future.immediate(new http.Response("not found", 404));
+          }));
+
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {
+                'git': {'url': 'git://github.com/dart-lang/foo'},
+                'version': '>=1.0.0 <2.0.0'
+              }
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=1.0.0 <2.0.0"'))));
+
+          run();
+        });
+
+        test("and should use the other source's unquoted version if it's "
+            "concrete", () {
+          useMockClient(new MockClient((request) {
+            expect(request.method, equals("GET"));
+            expect(request.url.path, equals("/packages/foo.json"));
+
+            return new Future.immediate(new http.Response("not found", 404));
+          }));
+
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {
+                'git': {'url': 'git://github.com/dart-lang/foo'},
+                'version': '0.2.3'
+              }
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains('  foo: 0.2.3'))));
+
+          run();
+        });
+      });
+    });
+
+    group('has an unconstrained dependency', () {
+      group('and it should not suggest a version', () {
+        test("if there's no lockfile", () {
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'hosted': 'foo'}
+            ])
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, everyElement(isNot(contains("\n  foo:")))));
+
+          run();
+        });
+
+        test("if the lockfile doesn't have an entry for the dependency", () {
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'hosted': 'foo'}
+            ]),
+            file("pubspec.lock", JSON.stringify({
+              'packages': {
+                'bar': {
+                  'version': '1.2.3',
+                  'source': 'hosted',
+                  'description': {
+                    'name': 'bar',
+                    'url': 'http://pub.dartlang.org'
+                  }
+                }
+              }
+            }))
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, everyElement(isNot(contains("\n  foo:")))));
+
+          run();
+        });
+      });
+
+      group('with a lockfile', () {
+        test('and it should suggest a constraint based on the locked '
+            'version', () {
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'hosted': 'foo'}
+            ]),
+            file("pubspec.lock", JSON.stringify({
+              'packages': {
+                'foo': {
+                  'version': '1.2.3',
+                  'source': 'hosted',
+                  'description': {
+                    'name': 'foo',
+                    'url': 'http://pub.dartlang.org'
+                  }
+                }
+              }
+            }))
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=1.2.3 <2.0.0"'))));
+
+          run();
+        });
+
+        test('and it should suggest a concrete constraint if the locked '
+            'version is pre-1.0.0', () {
+          dir(appPath, [
+            libPubspec("test_pkg", "1.0.0", [
+              {'hosted': 'foo'}
+            ]),
+            file("pubspec.lock", JSON.stringify({
+              'packages': {
+                'foo': {
+                  'version': '0.1.2',
+                  'source': 'hosted',
+                  'description': {
+                    'name': 'foo',
+                    'url': 'http://pub.dartlang.org'
+                  }
+                }
+              }
+            }))
+          ]).scheduleCreate();
+
+          expectLater(schedulePackageValidation(dependency),
+              pairOf(isEmpty, someElement(contains(
+                  '  foo: ">=0.1.2 <0.1.3"'))));
+
+          run();
+        });
+      });
+    });
   });
 }