Migration: replace `pub outdated` check with a check on transitive imports.

Instead of running `pub outdated` to see if the user's dependencies
have been migrated yet, the migration tool now examines all of the
transitive import dependencies of the user's code to see if they are
opted in to null safety.  This produces a more accurate result than
`pub outdated`, because it is able to ignore files in transitive
package dependencies that aren't reachable via imports
(e.g. references to `package:analyzer` brought in by `package:test`).

Fixes #44061.

Bug: https://github.com/dart-lang/sdk/issues/44061
Change-Id: I38465bcbf35e8552f0060b5d51c0f1cfc5d18c7f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/170561
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Samuel Rawlins <srawlins@google.com>
diff --git a/pkg/dartdev/test/utils.dart b/pkg/dartdev/test/utils.dart
index e1a3cd2..4d67099 100644
--- a/pkg/dartdev/test/utils.dart
+++ b/pkg/dartdev/test/utils.dart
@@ -70,9 +70,6 @@
   }) {
     var arguments = [
       command,
-      if (command == 'migrate')
-        // TODO(srawlins): Enable `pub outdated` in tests.
-        '--skip-pub-outdated',
       ...?args,
     ];
 
diff --git a/pkg/nnbd_migration/lib/migration_cli.dart b/pkg/nnbd_migration/lib/migration_cli.dart
index f6a90af..8f0d07d 100644
--- a/pkg/nnbd_migration/lib/migration_cli.dart
+++ b/pkg/nnbd_migration/lib/migration_cli.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
-import 'dart:convert' show jsonDecode;
 import 'dart:io' hide File;
 
 import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
@@ -32,7 +31,6 @@
 import 'package:nnbd_migration/src/front_end/migration_state.dart';
 import 'package:nnbd_migration/src/front_end/non_nullable_fix.dart';
 import 'package:nnbd_migration/src/messages.dart';
-import 'package:nnbd_migration/src/utilities/json.dart' as json;
 import 'package:nnbd_migration/src/utilities/progress_bar.dart';
 import 'package:nnbd_migration/src/utilities/source_edit_diff_formatter.dart';
 import 'package:path/path.dart' show Context;
@@ -100,6 +98,10 @@
   static const previewHostnameOption = 'preview-hostname';
   static const previewPortOption = 'preview-port';
   static const sdkPathOption = 'sdk-path';
+  static const skipImportCheckFlag = 'skip-import-check';
+
+  /// TODO(paulberry): remove this flag once internal sources have been updated.
+  @Deprecated('The migration tool no longer performs "pub outdated" checks')
   static const skipPubOutdatedFlag = 'skip-pub-outdated';
   static const summaryOption = 'summary';
   static const verboseFlag = 'verbose';
@@ -119,105 +121,43 @@
 
   final String sdkPath;
 
-  final bool skipPubOutdated;
+  final bool skipImportCheck;
 
   final String summary;
 
   final bool webPreview;
 
   CommandLineOptions(
-      {@required this.applyChanges,
-      @required this.directory,
-      @required this.ignoreErrors,
-      @required this.ignoreExceptions,
-      @required this.previewHostname,
-      @required this.previewPort,
-      @required this.sdkPath,
-      @required this.skipPubOutdated,
-      @required this.summary,
-      @required this.webPreview});
-}
-
-@visibleForTesting
-class DependencyChecker {
-  /// The directory which contains the package being migrated.
-  final String _directory;
-  final Context _pathContext;
-  final Logger _logger;
-  final ProcessManager _processManager;
-
-  DependencyChecker(
-      this._directory, this._pathContext, this._logger, this._processManager);
-
-  bool check() {
-    var pubPath = _pathContext.join(getSdkPath(), 'bin', 'dart');
-    var pubArguments = ['pub', 'outdated', '--mode=null-safety', '--json'];
-    var preNullSafetyPackages = <String, String>{};
-    try {
-      var result = _processManager.runSync(pubPath, pubArguments,
-          workingDirectory: _directory);
-      if ((result.stderr as String).isNotEmpty) {
-        throw FormatException(
-            '`dart pub outdated --mode=null-safety` exited with exit code '
-            '${result.exitCode} and stderr:\n\n${result.stderr}');
-      }
-      var outdatedOutput = jsonDecode(result.stdout as String);
-      var outdatedMap = json.expectType<Map>(outdatedOutput, 'root');
-      var packageList =
-          json.expectType<List>(outdatedMap['packages'], 'packages');
-      for (var package_ in packageList) {
-        var package = json.expectType<Map>(package_, '');
-        var current_ = json.expectKey(package, 'current');
-        if (current_ == null) {
-          continue;
-        }
-        var current = json.expectType<Map>(current_, 'current');
-        if (json.expectType<bool>(current['nullSafety'], 'nullSafety')) {
-          // For whatever reason, there is no "current" version of this package.
-          // TODO(srawlins): We may want to report this to the user. But it may
-          // be inconsequential.
-          continue;
-        }
-
-        json.expectKey(package, 'package');
-        json.expectKey(current, 'version');
-        var name = json.expectType<String>(package['package'], 'package');
-        // A version will be given, even if a package was provided with a local
-        // or git path.
-        var version = json.expectType<String>(current['version'], 'version');
-        preNullSafetyPackages[name] = version;
-      }
-    } on ProcessException catch (e) {
-      _logger.stderr(
-          'Warning: Could not execute `$pubPath ${pubArguments.join(' ')}`: '
-          '"${e.message}"');
-      // Allow the program to continue; users should be allowed to attempt to
-      // migrate when `pub outdated` is misbehaving, or if there is a bug above.
-    } on FormatException catch (e) {
-      _logger.stderr('Warning: ${e.message}');
-      // Allow the program to continue; users should be allowed to attempt to
-      // migrate when `pub outdated` is misbehaving, or if there is a bug above.
-    }
-    if (preNullSafetyPackages.isNotEmpty) {
-      _logger.stderr('Warning: not all current dependencies have migrated to '
-          'null safety:');
-      _logger.stderr('');
-      for (var package in preNullSafetyPackages.entries) {
-        _logger.stderr(
-            '  package:${package.key} (currently at version ${package.value})');
-      }
-      _logger.stderr('');
-      _logger.stderr('For the best migration experience, please update to null '
-          'safe versions of these packages before migrating your code. You can '
-          'use \'dart pub outdated --mode=null-safety\' to check the status of '
-          'dependencies.');
-      _logger.stderr('');
-      _logger.stderr('Visit https://dart.dev/tools/pub/cmd/pub-outdated for '
-          'more information.');
-      return false;
-    }
-    return true;
-  }
+      {@required
+          this.applyChanges,
+      @required
+          this.directory,
+      @required
+          this.ignoreErrors,
+      @required
+          this.ignoreExceptions,
+      @required
+          this.previewHostname,
+      @required
+          this.previewPort,
+      @required
+          this.sdkPath,
+      // TODO(paulberry): make this parameter required once internal sources
+      // have been updated.
+      bool skipImportCheck,
+      // TODO(paulberry): remove this flag once internal sources have been
+      // updated.
+      @Deprecated('The migration tool no longer performs "pub outdated" checks')
+          bool skipPubOutdated = false,
+      @required
+          this.summary,
+      @required
+          this.webPreview})
+      // `skipImportCheck` has replaced `skipPubOutdated`, so if the caller
+      // specifies the latter but not the former, carry it over.
+      // TODO(paulberry): remove this logic once internal sources have been
+      // updated.
+      : skipImportCheck = skipImportCheck ?? skipPubOutdated;
 }
 
 // TODO(devoncarew): Refactor so this class extends DartdevCommand.
@@ -312,15 +252,13 @@
                   'analysis errors.',
             )),
     MigrationCliOption(
-        CommandLineOptions.skipPubOutdatedFlag,
+        CommandLineOptions.skipImportCheckFlag,
         (parser, hide) => parser.addFlag(
-              CommandLineOptions.skipPubOutdatedFlag,
+              CommandLineOptions.skipImportCheckFlag,
               defaultsTo: false,
               negatable: false,
-              help:
-                  'Skip the `pub outdated --mode=null-safety` check. This allows a '
-                  'migration to proceed even if some package dependencies have not yet '
-                  'been migrated.',
+              help: 'Go ahead with migration even if some imported files have '
+                  'not yet been migrated.',
             )),
     MigrationCliOption.separator('Web interface options:'),
     MigrationCliOption(
@@ -393,11 +331,6 @@
   /// user.  Used in testing to allow user feedback messages to be tested.
   final Logger Function(bool isVerbose) loggerFactory;
 
-  /// Process manager that should be used to run processes. Used in testing to
-  /// redirect to mock processes.
-  @visibleForTesting
-  final ProcessManager processManager;
-
   /// Resource provider that should be used to access the filesystem.  Used in
   /// testing to redirect to an in-memory filesystem.
   final ResourceProvider resourceProvider;
@@ -414,7 +347,6 @@
     @visibleForTesting this.loggerFactory = _defaultLoggerFactory,
     @visibleForTesting this.defaultSdkPathOverride,
     @visibleForTesting ResourceProvider resourceProvider,
-    @visibleForTesting this.processManager = const ProcessManager.system(),
     @visibleForTesting Map<String, String> environmentVariables,
   })  : logger = loggerFactory(false),
         resourceProvider =
@@ -491,8 +423,8 @@
           sdkPath: argResults[CommandLineOptions.sdkPathOption] as String ??
               defaultSdkPathOverride ??
               getSdkPath(),
-          skipPubOutdated:
-              argResults[CommandLineOptions.skipPubOutdatedFlag] as bool,
+          skipImportCheck:
+              argResults[CommandLineOptions.skipImportCheckFlag] as bool,
           summary: argResults[CommandLineOptions.summaryOption] as String,
           webPreview: webPreview);
       return MigrationCliRunner(this, options,
@@ -708,10 +640,6 @@
   /// If something goes wrong, a message is printed using the logger configured
   /// in the constructor, and [MigrationExit] is thrown.
   Future<void> run() async {
-    if (!options.skipPubOutdated) {
-      _checkDependencies();
-    }
-
     logger.stdout('Migrating ${options.directory}');
     logger.stdout('');
 
@@ -863,15 +791,6 @@
     applyHook();
   }
 
-  void _checkDependencies() {
-    var successful = DependencyChecker(
-            options.directory, pathContext, logger, cli.processManager)
-        .check();
-    if (!successful) {
-      throw MigrationExit(1);
-    }
-  }
-
   void _displayChangeDiff(DartFixListener migrationResults) {
     Map<String, List<DartFixSuggestion>> fileSuggestions = {};
     for (DartFixSuggestion suggestion in migrationResults.suggestions) {
@@ -1149,6 +1068,16 @@
       }
     });
 
+    var unmigratedDependencies = _task.migration.unmigratedDependencies;
+    if (unmigratedDependencies.isNotEmpty) {
+      if (_migrationCli.options.skipImportCheck) {
+        _migrationCli.logger.stdout(unmigratedDependenciesWarning);
+      } else {
+        throw ExperimentStatusException.unmigratedDependencies(
+            unmigratedDependencies);
+      }
+    }
+
     return AnalysisResult(analysisErrors, _migrationCli.lineInfo,
         _migrationCli.pathContext, _migrationCli.options.directory);
   }
diff --git a/pkg/nnbd_migration/lib/nnbd_migration.dart b/pkg/nnbd_migration/lib/nnbd_migration.dart
index 087685d..36027a0 100644
--- a/pkg/nnbd_migration/lib/nnbd_migration.dart
+++ b/pkg/nnbd_migration/lib/nnbd_migration.dart
@@ -334,6 +334,11 @@
 
   void finish();
 
+  /// Use this getter after any calls to [prepareInput] to obtain a list of URIs
+  /// of unmigrated dependencies.  Ideally, this list should be empty before the
+  /// user tries to migrate their package.
+  List<String> get unmigratedDependencies;
+
   void prepareInput(ResolvedUnitResult result);
 
   void processInput(ResolvedUnitResult result);
diff --git a/pkg/nnbd_migration/lib/src/exceptions.dart b/pkg/nnbd_migration/lib/src/exceptions.dart
index 15e26ed..580fcda 100644
--- a/pkg/nnbd_migration/lib/src/exceptions.dart
+++ b/pkg/nnbd_migration/lib/src/exceptions.dart
@@ -13,6 +13,10 @@
   /// The SDK does not contain the NNBD sources, it is the pre-unfork copy.
   ExperimentStatusException.sdkPreforkSources() : super(sdkNnbdOff);
 
+  /// The user's code imports unmigrated dependencies.
+  ExperimentStatusException.unmigratedDependencies(List<String> uris)
+      : super(unmigratedDependenciesError(uris));
+
   /// Throw an [ExperimentStatusException] if the [result] seems to have
   /// incorrectly configured experiment flags/nnbd sources.
   static void sanityCheck(ResolvedUnitResult result) {
diff --git a/pkg/nnbd_migration/lib/src/messages.dart b/pkg/nnbd_migration/lib/src/messages.dart
index 41497f2..1305da4 100644
--- a/pkg/nnbd_migration/lib/src/messages.dart
+++ b/pkg/nnbd_migration/lib/src/messages.dart
@@ -2,6 +2,8 @@
 // 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:nnbd_migration/migration_cli.dart';
+
 const String migratedAlready =
     "Seem to be migrating code that's already migrated";
 const String nnbdExperimentOff =
@@ -10,3 +12,29 @@
 const String sdkPathEnvironmentVariableSet =
     r'Note: $SDK_PATH environment variable is set and may point to outdated '
     'dart:core sources';
+const String _skipImportCheckFlag =
+    '--${CommandLineOptions.skipImportCheckFlag}';
+const String unmigratedDependenciesWarning = '''
+Warning: package has unmigrated dependencies.
+
+Continuing due to the presence of `$_skipImportCheckFlag`.  To see a complete
+ list of these libraries, re-run without the `$_skipImportCheckFlag` flag.
+''';
+
+String unmigratedDependenciesError(List<String> uris) => '''
+Error: package has unmigrated dependencies.
+
+Before migrating your package, we recommend ensuring that every library it
+imports (either directly or indirectly) has been migrated to null safety, so
+that you will be able to run your unit tests in sound null checking mode.  You
+are currently importing the following non-null-safe libraries:
+
+  ${uris.join('\n  ')}
+
+Please upgrade the packages containing these libraries to null safe versions
+before continuing.  To see what null safe package versions are available, run
+the following command: `dart pub outdated --mode=null-safety --prereleases`.
+
+To skip this check and try to migrate anyway, re-run with the flag
+`$_skipImportCheckFlag`.
+''';
diff --git a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
index 3878f61..f80f4c4 100644
--- a/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
+++ b/pkg/nnbd_migration/lib/src/nullability_migration_impl.dart
@@ -4,6 +4,7 @@
 
 import 'package:analyzer/dart/analysis/features.dart';
 import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/element/element.dart';
 import 'package:analyzer/file_system/physical_file_system.dart';
 import 'package:analyzer/src/dart/analysis/session.dart';
 import 'package:analyzer/src/dart/element/type_system.dart';
@@ -63,6 +64,13 @@
   /// parameter into their "OrNull" equivalents if possible.
   final bool transformWhereOrNull;
 
+  /// Map from [Source] object to a boolean indicating whether the source is
+  /// opted in to null safety.
+  final Map<Source, bool> _libraryOptInStatus = {};
+
+  /// Indicates whether the client has used the [unmigratedDependencies] getter.
+  bool _queriedUnmigratedDependencies = false;
+
   /// Prepares to perform nullability migration.
   ///
   /// If [permissive] is `true`, exception handling logic will try to proceed
@@ -115,6 +123,23 @@
   bool get isPermissive => _permissive;
 
   @override
+  List<String> get unmigratedDependencies {
+    _queriedUnmigratedDependencies = true;
+    var unmigratedDependencies = <Source>[];
+    for (var entry in _libraryOptInStatus.entries) {
+      if (_graph.isBeingMigrated(entry.key)) continue;
+      if (!entry.value) {
+        unmigratedDependencies.add(entry.key);
+      }
+    }
+    var badUris = {
+      for (var dependency in unmigratedDependencies) dependency.uri.toString()
+    }.toList();
+    badUris.sort();
+    return badUris;
+  }
+
+  @override
   void finalizeInput(ResolvedUnitResult result) {
     if (result.unit.featureSet.isEnabled(Feature.non_nullable)) {
       // This library has already been migrated; nothing more to do.
@@ -183,11 +208,16 @@
   }
 
   void prepareInput(ResolvedUnitResult result) {
+    assert(
+        !_queriedUnmigratedDependencies,
+        'Should only query unmigratedDependencies after all calls to '
+        'prepareInput');
     if (result.unit.featureSet.isEnabled(Feature.non_nullable)) {
       // This library has already been migrated; nothing more to do.
       return;
     }
     ExperimentStatusException.sanityCheck(result);
+    _recordTransitiveImportOptInStatus(result.libraryElement.importedLibraries);
     if (_variables == null) {
       _variables = Variables(_graph, result.typeProvider, _getLineInfo,
           instrumentation: _instrumentation,
@@ -239,6 +269,18 @@
     _graph.update(_postmortemFileWriter);
   }
 
+  /// Records the opt in/out status of all libraries in [libraries], and any
+  /// libraries they transitively import, in [_libraryOptInStatus].
+  void _recordTransitiveImportOptInStatus(Iterable<LibraryElement> libraries) {
+    var librariesToCheck = libraries.toList();
+    while (librariesToCheck.isNotEmpty) {
+      var library = librariesToCheck.removeLast();
+      if (_libraryOptInStatus.containsKey(library.source)) continue;
+      _libraryOptInStatus[library.source] = library.isNonNullableByDefault;
+      librariesToCheck.addAll(library.importedLibraries);
+    }
+  }
+
   static Location _computeLocation(
       LineInfo lineInfo, SourceEdit edit, Source source) {
     final locationInfo = lineInfo.getLocation(edit.offset);
diff --git a/pkg/nnbd_migration/test/api_test.dart b/pkg/nnbd_migration/test/api_test.dart
index bfd7469..1e86235 100644
--- a/pkg/nnbd_migration/test/api_test.dart
+++ b/pkg/nnbd_migration/test/api_test.dart
@@ -69,6 +69,7 @@
         }
       }
     }
+    expect(migration.unmigratedDependencies, isEmpty);
     _betweenStages();
     for (var path in input.keys) {
       if (!(session.getFile(path)).isPart) {
diff --git a/pkg/nnbd_migration/test/front_end/nnbd_migration_test_base.dart b/pkg/nnbd_migration/test/front_end/nnbd_migration_test_base.dart
index cace2ce..6f87f89 100644
--- a/pkg/nnbd_migration/test/front_end/nnbd_migration_test_base.dart
+++ b/pkg/nnbd_migration/test/front_end/nnbd_migration_test_base.dart
@@ -219,6 +219,7 @@
     }
 
     await _forEachPath(migration.prepareInput);
+    expect(migration.unmigratedDependencies, isEmpty);
     await _forEachPath(migration.processInput);
     await _forEachPath(migration.finalizeInput);
     migration.finish();
diff --git a/pkg/nnbd_migration/test/instrumentation_test.dart b/pkg/nnbd_migration/test/instrumentation_test.dart
index d06a022..6e8cd37 100644
--- a/pkg/nnbd_migration/test/instrumentation_test.dart
+++ b/pkg/nnbd_migration/test/instrumentation_test.dart
@@ -151,6 +151,7 @@
     source = result.unit.declaredElement.source;
     findNode = FindNode(content, result.unit);
     migration.prepareInput(result);
+    expect(migration.unmigratedDependencies, isEmpty);
     migration.processInput(result);
     migration.finalizeInput(result);
     migration.finish();
diff --git a/pkg/nnbd_migration/test/migration_cli_test.dart b/pkg/nnbd_migration/test/migration_cli_test.dart
index 25bcef9..c581b6b 100644
--- a/pkg/nnbd_migration/test/migration_cli_test.dart
+++ b/pkg/nnbd_migration/test/migration_cli_test.dart
@@ -99,7 +99,6 @@
             defaultSdkPathOverride:
                 _test.resourceProvider.convertPath(mock_sdk.sdkRoot),
             resourceProvider: _test.resourceProvider,
-            processManager: _test.processManager,
             environmentVariables: _test.environmentVariables);
 
   _MigrationCliRunner decodeCommandLineArgs(ArgResults argResults,
@@ -203,8 +202,6 @@
 
   set logger(TestLogger logger);
 
-  _MockProcessManager get processManager;
-
   MemoryResourceProvider get resourceProvider;
 }
 
@@ -305,36 +302,6 @@
     }
   }
 
-  void assertPubOutdatedFailure(
-      {int pubOutdatedExitCode = 0,
-      String pubOutdatedStdout = '',
-      String pubOutdatedStderr = ''}) {
-    processManager._mockResult = ProcessResult(123 /* pid */,
-        pubOutdatedExitCode, pubOutdatedStdout, pubOutdatedStderr);
-    logger = TestLogger(true);
-    var projectContents = simpleProject(sourceText: 'int x;');
-    var projectDir = createProjectDir(projectContents);
-    var success = DependencyChecker(
-            projectDir, resourceProvider.pathContext, logger, processManager)
-        .check();
-    expect(success, isFalse);
-  }
-
-  void assertPubOutdatedSuccess(
-      {int pubOutdatedExitCode = 0,
-      String pubOutdatedStdout = '',
-      String pubOutdatedStderr = ''}) {
-    processManager._mockResult = ProcessResult(123 /* pid */,
-        pubOutdatedExitCode, pubOutdatedStdout, pubOutdatedStderr);
-    logger = TestLogger(true);
-    var projectContents = simpleProject(sourceText: 'int x;');
-    var projectDir = createProjectDir(projectContents);
-    var success = DependencyChecker(
-            projectDir, resourceProvider.pathContext, logger, processManager)
-        .check();
-    expect(success, isTrue);
-  }
-
   Future<String> assertRunFailure(List<String> args,
       {MigrationCli cli,
       bool withUsage = false,
@@ -407,6 +374,9 @@
         http.post(url, headers: headers, body: body, encoding: encoding));
   }
 
+  String packagePath(String path) =>
+      resourceProvider.convertPath('/.pub-cache/$path');
+
   Future<void> runWithPreviewServer(_MigrationCli cli, List<String> args,
       Future<void> Function(String) callback) async {
     String url;
@@ -575,17 +545,17 @@
     expect(_getHelpText(verbose: true), contains(flagName));
   }
 
-  test_flag_skip_pub_outdated_default() {
-    expect(assertParseArgsSuccess([]).skipPubOutdated, isFalse);
+  test_flag_skip_import_check_default() {
+    expect(assertParseArgsSuccess([]).skipImportCheck, isFalse);
   }
 
-  test_flag_skip_pub_outdated_disable() async {
-    // "--no-skip-pub-outdated" is not an option.
-    await assertParseArgsFailure(['--no-skip-pub-outdated']);
+  test_flag_skip_import_check_disable() async {
+    // "--no-skip-import-check" is not an option.
+    await assertParseArgsFailure(['--no-skip-import_check']);
   }
 
-  test_flag_skip_pub_outdated_enable() {
-    expect(assertParseArgsSuccess(['--skip-pub-outdated']).skipPubOutdated,
+  test_flag_skip_import_check_enable() {
+    expect(assertParseArgsSuccess(['--skip-import-check']).skipImportCheck,
         isTrue);
   }
 
@@ -843,8 +813,12 @@
         .join(projectDir, 'lib', 'analyze_but_do_not_migrate.dart');
     overridePathsToProcess = {testPath, analyzeButDoNotMigratePath};
     overrideShouldBeMigrated = (path) => path == testPath;
-    var cliRunner = _createCli().decodeCommandLineArgs(
-        _parseArgs(['--no-web-preview', '--apply-changes', projectDir]));
+    var cliRunner = _createCli().decodeCommandLineArgs(_parseArgs([
+      '--no-web-preview',
+      '--apply-changes',
+      '--skip-import-check',
+      projectDir
+    ]));
     await cliRunner.run();
     assertNormalExit(cliRunner);
     // Check that a summary was printed
@@ -1312,47 +1286,54 @@
     });
   }
 
-  test_lifecycle_skip_pub_outdated_disable() async {
-    var projectContents = simpleProject(sourceText: '''
+  test_lifecycle_skip_import_check_disable() async {
+    var projectContents = simpleProject(
+        sourceText: '''
+import 'package:foo/foo.dart';
+import 'package:foo/bar.dart';
+
 int f() => null;
-''');
+''',
+        packageConfigText: _getPackageConfigText(
+            migrated: false, packagesMigrated: {'foo': false}));
     var projectDir = createProjectDir(projectContents);
-    processManager._mockResult = ProcessResult(
-        123 /* pid */,
-        0 /* exitCode */,
-        '''
-{ "packages":
-  [
-    { "package": "abc", "current": { "version": "1.0.0", "nullSafety": false } }
-  ]
-}
-''' /* stdout */,
-        '' /* stderr */);
-    var output = await assertRunFailure([projectDir], expectedExitCode: 1);
-    expect(output,
-        contains('Warning: not all current dependencies have migrated'));
+    resourceProvider.newFile(packagePath('foo/lib/foo.dart'), '');
+    resourceProvider.newFile(packagePath('foo/lib/bar.dart'), '');
+    await assertRunFailure([projectDir], expectedExitCode: 1);
+    var output = logger.stdoutBuffer.toString();
+    expect(output, contains('Error: package has unmigrated dependencies'));
+    // Output should contain an indented, sorted list of all unmigrated
+    // dependencies.
+    expect(
+        output, contains('\n  package:foo/bar.dart\n  package:foo/foo.dart'));
+    // Output should mention that the user can rerun with `--skip-import-check`.
+    expect(output, contains('`--${CommandLineOptions.skipImportCheckFlag}`'));
   }
 
-  test_lifecycle_skip_pub_outdated_enable() async {
-    var projectContents = simpleProject(sourceText: '''
+  test_lifecycle_skip_import_check_enable() async {
+    var projectContents = simpleProject(
+        sourceText: '''
+import 'package:foo/foo.dart';
+import 'package:foo/bar.dart';
+
 int f() => null;
-''');
+''',
+        packageConfigText: _getPackageConfigText(
+            migrated: false, packagesMigrated: {'foo': false}));
     var projectDir = createProjectDir(projectContents);
-    processManager._mockResult = ProcessResult(
-        123 /* pid */,
-        0 /* exitCode */,
-        '''
-{ "packages":
-  [
-    { "package": "abc", "current": { "version": "1.0.0", "nullSafety": false } }
-  ]
-}
-''' /* stdout */,
-        '' /* stderr */);
+    resourceProvider.newFile(packagePath('foo/lib/foo.dart'), '');
+    resourceProvider.newFile(packagePath('foo/lib/bar.dart'), '');
     var cli = _createCli();
-    await runWithPreviewServer(cli, ['--skip-pub-outdated', projectDir],
+    await runWithPreviewServer(cli, ['--skip-import-check', projectDir],
         (url) async {
       await assertPreviewServerResponsive(url);
+      var output = logger.stdoutBuffer.toString();
+      expect(output, contains('Warning: package has unmigrated dependencies'));
+      // Output should not mention the particular unmigrated dependencies.
+      expect(output, isNot(contains('package:foo')));
+      // Output should mention that the user can rerun without
+      // `--skip-import-check`.
+      expect(output, contains('`--${CommandLineOptions.skipImportCheckFlag}`'));
     });
   }
 
@@ -1642,134 +1623,6 @@
     }
   }
 
-  test_pub_outdated_has_malformed_json() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '{ "packages": }');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_no_packages() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '{}');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_no_pre_null_safety_packages() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc",
-      "current": { "version": "1.0.0", "nullSafety": true }
-    },
-    {
-      "package": "def",
-      "current": { "version": "2.0.0", "nullSafety": true }
-    }
-  ]
-}
-''');
-  }
-
-  test_pub_outdated_has_one_pre_null_safety_package() {
-    assertPubOutdatedFailure(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc",
-      "current": { "version": "1.0.0", "nullSafety": false }
-    },
-    {
-      "package": "def",
-      "current": { "version": "2.0.0", "nullSafety": true }
-    }
-  ]
-}
-''');
-    var stderrText = logger.stderrBuffer.toString();
-    expect(stderrText, contains('Warning:'));
-    expect(stderrText, contains('abc'));
-    expect(stderrText, contains('1.0.0'));
-  }
-
-  test_pub_outdated_has_package_with_missing_current() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc"
-    }
-  ]
-}
-''');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_package_with_missing_name() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "current": {
-        "version": "1.0.0",
-        "nullSafety": false
-      }
-    }
-  ]
-}
-''');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_package_with_missing_nullSafety() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc",
-      "current": {
-        "version": "1.0.0"
-      }
-    }
-  ]
-}
-''');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_package_with_missing_version() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc",
-      "current": {
-        "nullSafety": false
-      }
-    }
-  ]
-}
-''');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
-  test_pub_outdated_has_package_with_null_current() {
-    assertPubOutdatedSuccess(pubOutdatedStdout: '''
-{
-  "packages": [
-    {
-      "package": "abc",
-      "current": null
-    }
-  ]
-}
-''');
-    expect(logger.stderrBuffer.toString(), isEmpty);
-  }
-
-  test_pub_outdated_has_stderr() {
-    assertPubOutdatedSuccess(pubOutdatedStderr: 'anything');
-    expect(logger.stderrBuffer.toString(), startsWith('Warning:'));
-  }
-
   test_pubspec_does_not_exist() async {
     var projectContents = simpleProject()..remove('pubspec.yaml');
     var projectDir = createProjectDir(projectContents);
@@ -1982,23 +1835,34 @@
     return helpText;
   }
 
+  String _getPackageConfigText(
+      {@required bool migrated,
+      Map<String, bool> packagesMigrated = const {}}) {
+    Object makePackageEntry(String name, bool migrated, {String rootUri}) {
+      rootUri ??=
+          resourceProvider.pathContext.toUri(packagePath(name)).toString();
+      return {
+        'name': name,
+        'rootUri': rootUri,
+        'packageUri': 'lib/',
+        'languageVersion': migrated ? '2.12' : '2.6'
+      };
+    }
+
+    var json = {
+      'configVersion': 2,
+      'packages': [
+        makePackageEntry('test', migrated, rootUri: '../'),
+        for (var entry in packagesMigrated.entries)
+          makePackageEntry(entry.key, entry.value)
+      ]
+    };
+    return JsonEncoder.withIndent('  ').convert(json) + '\n';
+  }
+
   ArgResults _parseArgs(List<String> args) {
     return MigrationCli.createParser().parse(args);
   }
-
-  static String _getPackageConfigText({@required bool migrated}) => '''
-{
-  "configVersion": 2,
-  "packages": [
-    {
-      "name": "test",
-      "rootUri": "../",
-      "packageUri": "lib/",
-      "languageVersion": "${migrated ? '2.12' : '2.6'}"
-    }
-  ]
-}
-''';
 }
 
 @reflectiveTest
@@ -2007,16 +1871,12 @@
   @override
   final resourceProvider;
 
-  @override
-  final processManager;
-
   _MigrationCliTestPosix()
       : resourceProvider = MemoryResourceProvider(
             context: path.style == path.Style.posix
                 ? null
                 : path.Context(
-                    style: path.Style.posix, current: '/working_dir')),
-        processManager = _MockProcessManager();
+                    style: path.Style.posix, current: '/working_dir'));
 }
 
 @reflectiveTest
@@ -2025,30 +1885,10 @@
   @override
   final resourceProvider;
 
-  @override
-  final processManager;
-
   _MigrationCliTestWindows()
       : resourceProvider = MemoryResourceProvider(
             context: path.style == path.Style.windows
                 ? null
                 : path.Context(
-                    style: path.Style.windows, current: 'C:\\working_dir')),
-        processManager = _MockProcessManager();
-}
-
-class _MockProcessManager implements ProcessManager {
-  ProcessResult _mockResult;
-
-  dynamic noSuchMethod(Invocation invocation) {}
-
-  ProcessResult runSync(String executable, List<String> arguments,
-          {String workingDirectory}) =>
-      _mockResult ??
-      ProcessResult(
-        123 /* pid */,
-        0 /* exitCode */,
-        jsonEncode({'packages': []}) /* stdout */,
-        '' /* stderr */,
-      );
+                    style: path.Style.windows, current: 'C:\\working_dir'));
 }