Split transitive dev_dependencies from other transitive dependencies (#2419)

diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 0e544dd..56a8208 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -74,28 +74,41 @@
 
     var resolvablePubspec = _stripVersionConstraints(upgradePubspec);
 
-    SolveResult upgradableSolveResult;
-    SolveResult resolvableSolveResult;
+    List<PackageId> upgradablePackages;
+    List<PackageId> resolvablePackages;
 
     await log.warningsOnlyUnlessTerminal(
       () => log.spinner(
         'Resolving',
         () async {
-          upgradableSolveResult = await resolveVersions(
+          upgradablePackages = (await resolveVersions(
             SolveType.UPGRADE,
             cache,
             Package.inMemory(upgradePubspec),
-          );
+          ))
+              .packages;
 
-          resolvableSolveResult = await resolveVersions(
+          resolvablePackages = (await resolveVersions(
             SolveType.UPGRADE,
             cache,
             Package.inMemory(resolvablePubspec),
-          );
+          ))
+              .packages;
         },
       ),
     );
 
+    final currentPackages = entrypoint.lockFile.packages.values;
+
+    /// The set of all dependencies (direct and transitive) that are in the
+    /// closure of the non-dev dependencies from the root in at least one of
+    /// the current, upgradable and resolvable resolutions.
+    final nonDevDependencies = <String>{
+      ...await nonDevDependencyClosure(entrypoint.root, currentPackages),
+      ...await nonDevDependencyClosure(entrypoint.root, upgradablePackages),
+      ...await nonDevDependencyClosure(entrypoint.root, resolvablePackages)
+    };
+
     Future<_PackageDetails> analyzeDependency(PackageRef packageRef) async {
       final name = packageRef.name;
       final current = (entrypoint.lockFile?.packages ?? {})[name]?.version;
@@ -104,10 +117,10 @@
           .map((id) => id.version)
           .toList()
             ..sort(argResults['pre-releases'] ? null : Version.prioritize);
-      final upgradable = upgradableSolveResult.packages
+      final upgradable = upgradablePackages
           .firstWhere((id) => id.name == name, orElse: () => null)
           ?.version;
-      final resolvable = resolvableSolveResult.packages
+      final resolvable = resolvablePackages
           .firstWhere((id) => id.name == name, orElse: () => null)
           ?.version;
       final latest = available.last;
@@ -118,7 +131,7 @@
           await _describeVersion(name, source, description, upgradable),
           await _describeVersion(name, source, description, resolvable),
           await _describeVersion(name, source, description, latest),
-          _kind(name, entrypoint));
+          _kind(name, entrypoint, nonDevDependencies));
     }
 
     final rows = <_PackageDetails>[];
@@ -135,9 +148,9 @@
       ...immediateDependencies.map((d) => d.name)
     };
     for (final id in [
-      if (includeDevDependencies) ...entrypoint.lockFile.packages.values,
-      ...upgradableSolveResult.packages,
-      ...resolvableSolveResult.packages
+      ...currentPackages,
+      ...upgradablePackages,
+      ...resolvablePackages
     ]) {
       final name = id.name;
       if (!visited.add(name)) continue;
@@ -181,6 +194,24 @@
             .source(source)
             .describe(PackageId(name, source, version, description));
   }
+
+  /// Computes the closure of the graph of dependencies (not including
+  /// dev_dependencies from [root], given the package versions in [resolution].
+  Future<Set<String>> nonDevDependencyClosure(
+      Package root, Iterable<PackageId> resolution) async {
+    final mapping =
+        Map<String, PackageId>.fromIterable(resolution, key: (id) => id.name);
+    final visited = <String>{root.name};
+    final toVisit = [...root.dependencies.keys];
+    while (toVisit.isNotEmpty) {
+      final name = toVisit.removeLast();
+      if (!visited.add(name)) continue;
+      final id = mapping[name];
+      toVisit.addAll(
+          (await cache.source(id.source).describe(id)).dependencies.keys);
+    }
+    return visited;
+  }
 }
 
 Pubspec _stripDevDependencies(Pubspec original) {
@@ -241,6 +272,8 @@
   final devRows = rows.where((row) => row.kind == _DependencyKind.dev);
   final transitiveRows =
       rows.where((row) => row.kind == _DependencyKind.transitive);
+  final devTransitiveRows =
+      rows.where((row) => row.kind == _DependencyKind.devTransitive);
 
   final formattedRows = <List<_FormattedString>>[
     ['Dependencies', 'Current', 'Upgradable', 'Resolvable', 'Latest']
@@ -261,6 +294,13 @@
           : _format('\ntransitive dependencies', log.bold)
     ],
     ...await Future.wait(transitiveRows.map(marker)),
+    if (includeDevDependencies)
+      [
+        devTransitiveRows.isEmpty
+            ? _raw('\ntransitive dev_dependencies: all up-to-date')
+            : _format('\ntransitive dev_dependencies', log.bold)
+      ],
+    ...await Future.wait(devTransitiveRows.map(marker)),
   ];
 
   final columnWidths = <int, int>{};
@@ -400,13 +440,18 @@
   }
 }
 
-_DependencyKind _kind(String name, Entrypoint entrypoint) {
+_DependencyKind _kind(
+    String name, Entrypoint entrypoint, Set<String> nonDevTransitive) {
   if (entrypoint.root.dependencies.containsKey(name)) {
     return _DependencyKind.direct;
   } else if (entrypoint.root.devDependencies.containsKey(name)) {
     return _DependencyKind.dev;
   } else {
-    return _DependencyKind.transitive;
+    if (nonDevTransitive.contains(name)) {
+      return _DependencyKind.transitive;
+    } else {
+      return _DependencyKind.devTransitive;
+    }
   }
 }
 
@@ -417,8 +462,11 @@
   /// Direct dev dependencies.
   dev,
 
-  /// Transitive dependencies.
-  transitive
+  /// Transitive dependencies of direct dependencies.
+  transitive,
+
+  /// Transitive dependencies needed only by dev_dependencies.
+  devTransitive,
 }
 
 _FormattedString _format(String value, Function(String) format, {prefix = ''}) {
diff --git a/test/outdated/goldens/circular_dependencies.txt b/test/outdated/goldens/circular_dependencies.txt
index 24034f2..582817b 100644
--- a/test/outdated/goldens/circular_dependencies.txt
+++ b/test/outdated/goldens/circular_dependencies.txt
@@ -20,6 +20,8 @@
 dev_dependencies: all up-to-date
 
 transitive dependencies: all up-to-date
+
+transitive dev_dependencies: all up-to-date
 1 upgradable dependency is locked (in pubspec.lock) to an older version.
 To update it, use `pub upgrade`.
 
@@ -31,6 +33,8 @@
 dev_dependencies: all up-to-date
 
 transitive dependencies: all up-to-date
+
+transitive dev_dependencies: all up-to-date
 1 upgradable dependency is locked (in pubspec.lock) to an older version.
 To update it, use `pub upgrade`.
 
@@ -42,6 +46,8 @@
 dev_dependencies: all up-to-date
 
 transitive dependencies: all up-to-date
+
+transitive dev_dependencies: all up-to-date
 1 upgradable dependency is locked (in pubspec.lock) to an older version.
 To update it, use `pub upgrade`.
 
@@ -53,6 +59,8 @@
 dev_dependencies: all up-to-date
 
 transitive dependencies: all up-to-date
+
+transitive dev_dependencies: all up-to-date
 1 upgradable dependency is locked (in pubspec.lock) to an older version.
 To update it, use `pub upgrade`.
 
diff --git a/test/outdated/goldens/mutually_incompatible.txt b/test/outdated/goldens/mutually_incompatible.txt
index 5aa1fcf..9bee742 100644
--- a/test/outdated/goldens/mutually_incompatible.txt
+++ b/test/outdated/goldens/mutually_incompatible.txt
@@ -29,6 +29,8 @@
 
 transitive dependencies: all up-to-date
 
+transitive dev_dependencies: all up-to-date
+
 Dependencies are all on the latest resolvable versions.
 Newer versions, while available, are not mutually compatible.
 
@@ -42,6 +44,8 @@
 
 transitive dependencies: all up-to-date
 
+transitive dev_dependencies: all up-to-date
+
 Dependencies are all on the latest resolvable versions.
 Newer versions, while available, are not mutually compatible.
 
@@ -55,6 +59,8 @@
 
 transitive dependencies: all up-to-date
 
+transitive dev_dependencies: all up-to-date
+
 Dependencies are all on the latest resolvable versions.
 Newer versions, while available, are not mutually compatible.
 
@@ -68,6 +74,8 @@
 
 transitive dependencies: all up-to-date
 
+transitive dev_dependencies: all up-to-date
+
 Dependencies are all on the latest resolvable versions.
 Newer versions, while available, are not mutually compatible.
 
diff --git a/test/outdated/goldens/newer_versions.txt b/test/outdated/goldens/newer_versions.txt
index cfc1dec..ea2f04d 100644
--- a/test/outdated/goldens/newer_versions.txt
+++ b/test/outdated/goldens/newer_versions.txt
@@ -51,6 +51,8 @@
 transitive dependencies
 transitive    *1.2.3   *1.3.0      *1.3.0      2.0.0   
 transitive2   -        -           1.0.0       1.0.0   
+
+transitive dev_dependencies
 transitive3   -        -           1.0.0       1.0.0   
 
 3 upgradable dependencies are locked (in pubspec.lock) to older versions.
@@ -70,6 +72,8 @@
 transitive dependencies
 transitive    1.2.3    1.3.0       1.3.0       2.0.0   
 transitive2   -        -           1.0.0       1.0.0   
+
+transitive dev_dependencies
 transitive3   -        -           1.0.0       1.0.0   
 
 3 upgradable dependencies are locked (in pubspec.lock) to older versions.
@@ -91,6 +95,8 @@
 transitive dependencies
 transitive     *1.2.3   *1.3.0      *1.3.0      2.0.0   
 transitive2    -        -           1.0.0       1.0.0   
+
+transitive dev_dependencies
 transitive3    -        -           1.0.0       1.0.0   
 
 3 upgradable dependencies are locked (in pubspec.lock) to older versions.
@@ -110,6 +116,8 @@
 transitive dependencies
 transitive    *1.2.3   *1.3.0      *1.3.0      2.0.0        
 transitive2   -        -           1.0.0       1.0.0        
+
+transitive dev_dependencies
 transitive3   -        -           1.0.0       1.0.0        
 
 3 upgradable dependencies are locked (in pubspec.lock) to older versions.