`outdated` support for workspaces (#4251)

diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 23cfeb7..7ce16fe 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -127,16 +127,24 @@
           'The json report always includes transitive dependencies.');
     }
 
-    final rootPubspec = includeDependencyOverrides
-        ? entrypoint.workspaceRoot.pubspec
-        : stripDependencyOverrides(entrypoint.workspaceRoot.pubspec);
+    /// The workspace root with dependency overrides removed if requested.
+    final baseWorkspace = includeDependencyOverrides
+        ? entrypoint.workspaceRoot
+        : entrypoint.workspaceRoot.transformWorkspace(
+            (package) => stripDependencyOverrides(package.pubspec),
+          );
 
-    final upgradablePubspec = includeDevDependencies
-        ? rootPubspec
-        : stripDevDependencies(rootPubspec);
+    /// [baseWorkspace] with dev-dependencies removed if requested.
+    final upgradableWorkspace = includeDevDependencies
+        ? baseWorkspace
+        : baseWorkspace.transformWorkspace(
+            (package) => stripDevDependencies(package.pubspec),
+          );
 
-    final resolvablePubspec = await mode.resolvablePubspec(upgradablePubspec);
-
+    /// [upgradableWorkspace] with upper bounds removed.
+    final resolvableWorkspace = upgradableWorkspace.transformWorkspace(
+      (package) => mode.resolvablePubspec(package.pubspec),
+    );
     late List<PackageId> upgradablePackages;
     late List<PackageId> resolvablePackages;
     late bool hasUpgradableResolution;
@@ -146,11 +154,7 @@
       'Resolving',
       () async {
         final upgradablePackagesResult = await _tryResolve(
-          Package(
-            upgradablePubspec,
-            entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
-          ),
+          upgradableWorkspace,
           cache,
           lockFile: entrypoint.lockFile,
         );
@@ -158,11 +162,7 @@
         upgradablePackages = upgradablePackagesResult ?? [];
 
         final resolvablePackagesResult = await _tryResolve(
-          Package(
-            resolvablePubspec,
-            entrypoint.workspaceRoot.dir,
-            entrypoint.workspaceRoot.workspaceChildren,
-          ),
+          resolvableWorkspace,
           cache,
           lockFile: entrypoint.lockFile,
         );
@@ -206,8 +206,7 @@
       var latestIsOverridden = false;
       PackageId? latest;
       // If not overridden in current resolution we can use this
-      if (!entrypoint.workspaceRoot.pubspec.dependencyOverrides
-          .containsKey(name)) {
+      if (!hasOverride(entrypoint.workspaceRoot, name)) {
         latest ??= await cache.getLatest(
           current?.toRef(),
           version: current?.version,
@@ -216,23 +215,27 @@
       }
       // If present as a dependency or dev_dependency we use this
       latest ??= await cache.getLatest(
-        rootPubspec.dependencies[name]?.toRef(),
+        allDependencies(baseWorkspace)
+            .firstWhereOrNull((r) => r.name == name)
+            ?.toRef(),
         allowPrereleases: prereleases,
       );
       latest ??= await cache.getLatest(
-        rootPubspec.devDependencies[name]?.toRef(),
+        allDevDependencies(baseWorkspace)
+            .firstWhereOrNull((r) => r.name == name)
+            ?.toRef(),
         allowPrereleases: prereleases,
       );
       // If not overridden and present in either upgradable or resolvable we
       // use this reference to find the latest
-      if (!upgradablePubspec.dependencyOverrides.containsKey(name)) {
+      if (!hasOverride(upgradableWorkspace, name)) {
         latest ??= await cache.getLatest(
           upgradable?.toRef(),
           version: upgradable?.version,
           allowPrereleases: prereleases,
         );
       }
-      if (!resolvablePubspec.dependencyOverrides.containsKey(name)) {
+      if (!hasOverride(resolvableWorkspace, name)) {
         latest ??= await cache.getLatest(
           resolvable?.toRef(),
           version: resolvable?.version,
@@ -278,12 +281,12 @@
 
       final upgradableVersionDetails = await _describeVersion(
         upgradable,
-        upgradablePubspec.dependencyOverrides.containsKey(name),
+        hasOverride(upgradableWorkspace, name),
       );
 
       final resolvableVersionDetails = await _describeVersion(
         resolvable,
-        resolvablePubspec.dependencyOverrides.containsKey(name),
+        hasOverride(resolvableWorkspace, name),
       );
 
       final latestVersionDetails = await _describeVersion(
@@ -332,8 +335,9 @@
 
     final rows = <_PackageDetails>[];
 
-    final visited = <String>{
-      entrypoint.workspaceRoot.name,
+    final visited = {
+      ...entrypoint.workspaceRoot.transitiveWorkspace
+          .map((package) => package.name),
     };
     // Add all dependencies from the lockfile.
     for (final id in [
@@ -361,6 +365,7 @@
         includeDevDependencies: includeDevDependencies,
       );
     } else {
+      bool isNotFromSdk(PackageRange range) => range.source is! SdkSource;
       await _outputHuman(
         rows,
         mode,
@@ -368,13 +373,13 @@
         showAll: showAll,
         includeDevDependencies: includeDevDependencies,
         lockFileExists: fileExists(entrypoint.lockFilePath),
-        hasDirectDependencies: rootPubspec.dependencies.values.any(
+        hasDirectDependencies: allDependencies(baseWorkspace).any(
           // Test if it contains non-SDK dependencies
-          (c) => c.source is! SdkSource,
+          isNotFromSdk,
         ),
-        hasDevDependencies: rootPubspec.devDependencies.values.any(
+        hasDevDependencies: allDevDependencies(baseWorkspace).any(
           // Test if it contains non-SDK dependencies
-          (c) => c.source is! SdkSource,
+          isNotFromSdk,
         ),
         showTransitiveDependencies: showTransitiveDependencies,
         hasUpgradableResolution: hasUpgradableResolution,
@@ -420,23 +425,27 @@
   }
 
   /// Computes the closure of the graph of dependencies (not including
-  /// `dev_dependencies` from [root], given the package versions
-  /// in [resolution].
+  /// `dev_dependencies`) from all workspace packages in [workspaceRoot], given
+  /// the package versions in [resolution].
   ///
   /// The [resolution] is allowed to be a partial (or empty) resolution not
-  /// satisfying all the dependencies of [root].
+  /// satisfying all the dependencies of [workspaceRoot].
   Future<Set<String>> _nonDevDependencyClosure(
-    Package root,
+    Package workspaceRoot,
     Iterable<PackageId> resolution,
   ) async {
     final nameToId = {for (final id in resolution) id.name: id};
 
-    final nonDevDependencies = <String>{root.name};
-    final queue = [...root.dependencies.keys];
+    final result = <String>{
+      for (final p in workspaceRoot.transitiveWorkspace) p.name,
+    };
+    final queue = [
+      for (final p in workspaceRoot.transitiveWorkspace) ...p.dependencies.keys,
+    ];
 
     while (queue.isNotEmpty) {
       final name = queue.removeLast();
-      if (!nonDevDependencies.add(name)) {
+      if (!result.add(name)) {
         continue;
       }
 
@@ -448,7 +457,7 @@
       queue.addAll(pubspec.dependencies.keys);
     }
 
-    return nonDevDependencies;
+    return result;
   }
 }
 
@@ -784,7 +793,7 @@
   String get upgradeConstrained;
   String get allSafe;
 
-  Future<Pubspec> resolvablePubspec(Pubspec pubspec);
+  Pubspec resolvablePubspec(Pubspec pubspec);
 }
 
 class _OutdatedMode implements _Mode {
@@ -873,8 +882,8 @@
   }
 
   @override
-  Future<Pubspec> resolvablePubspec(Pubspec? pubspec) async {
-    return stripVersionBounds(pubspec!);
+  Pubspec resolvablePubspec(Pubspec pubspec) {
+    return stripVersionBounds(pubspec);
   }
 }
 
@@ -972,9 +981,9 @@
   Entrypoint entrypoint,
   Set<String> nonDevTransitive,
 ) {
-  if (entrypoint.workspaceRoot.dependencies.containsKey(name)) {
+  if (hasDependency(entrypoint.workspaceRoot, name)) {
     return _DependencyKind.direct;
-  } else if (entrypoint.workspaceRoot.devDependencies.containsKey(name)) {
+  } else if (hasDevDependency(entrypoint.workspaceRoot, name)) {
     return _DependencyKind.dev;
   } else {
     if (nonDevTransitive.contains(name)) {
@@ -1093,3 +1102,29 @@
 
   static String _noFormat(String x) => x;
 }
+
+/// Whether the package [name] is overridden anywhere in the workspace rooted at
+/// [workspaceRoot].
+bool hasOverride(Package workspaceRoot, String name) {
+  return workspaceRoot.allOverridesInWorkspace.containsKey(name);
+}
+
+/// Whether the package [name] is depended on directly anywhere in the workspace
+/// rooted at [workspaceRoot].
+bool hasDependency(Package workspaceRoot, String name) {
+  return workspaceRoot.transitiveWorkspace
+      .any((p) => p.dependencies.containsKey(name));
+}
+
+/// Whether the package [name] is dev-depended on directly anywhere in the workspace
+/// rooted at [workspaceRoot].
+bool hasDevDependency(Package workspaceRoot, String name) {
+  return workspaceRoot.transitiveWorkspace
+      .any((p) => p.devDependencies.containsKey(name));
+}
+
+Iterable<PackageRange> allDependencies(Package workspaceRoot) =>
+    workspaceRoot.transitiveWorkspace.expand((p) => p.dependencies.values);
+
+Iterable<PackageRange> allDevDependencies(Package workspaceRoot) =>
+    workspaceRoot.transitiveWorkspace.expand((p) => p.devDependencies.values);
diff --git a/test/outdated/outdated_test.dart b/test/outdated/outdated_test.dart
index 084c8dc..ff9ed28 100644
--- a/test/outdated/outdated_test.dart
+++ b/test/outdated/outdated_test.dart
@@ -778,6 +778,56 @@
     await ctx.runOutdatedTests();
   });
 
+  testWithGolden('reports dependencies from all of workspace', (ctx) async {
+    final server = await servePackages();
+    server.serve('myapp', '1.2.4');
+    server.serve('dep', '0.9.0', deps: {'myapp': '^1.2.3'});
+    server.serve('dep', '0.8.0', deps: {'myapp': '^1.2.3'});
+    server.serve('dep', '1.0.0');
+    server.serve('dep_a', '0.9.0');
+    server.serve('dep_a', '1.0.0');
+    server.serve('dev_dep_a', '0.9.0');
+    server.serve('dev_dep_a', '1.0.0');
+
+    await d.dir(appPath, [
+      d.libPubspec(
+        'myapp',
+        '1.2.3',
+        deps: {'dep': '^0.9.0'},
+        extras: {
+          'workspace': ['pkgs/a'],
+        },
+        sdk: '^3.5.0',
+      ),
+      d.dir('pkgs', [
+        d.dir('a', [
+          d.libPubspec(
+            'a',
+            '1.1.1',
+            deps: {'myapp': '^1.0.0', 'dep_a': '^0.9.0'},
+            devDeps: {'dev_dep_a': '^0.9.0'},
+            extras: {
+              'dependency_overrides': {'dep': '0.8.0'},
+            },
+            resolutionWorkspace: true,
+          ),
+        ]),
+      ]),
+    ]).create();
+
+    await pubGet(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+
+    server.serve('dep', '0.9.5');
+    server.serve('dep_a', '0.9.5');
+    server.serve('dev_dep_a', '0.9.5');
+
+    await ctx.runOutdatedTests(
+      environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'},
+    );
+  });
+
   testWithGolden('Handles SDK dependencies', (ctx) async {
     await servePackages()
       ..serve(
diff --git a/test/testdata/goldens/outdated/outdated_test/reports dependencies from all of workspace.txt b/test/testdata/goldens/outdated/outdated_test/reports dependencies from all of workspace.txt
new file mode 100644
index 0000000..c4e53af
--- /dev/null
+++ b/test/testdata/goldens/outdated/outdated_test/reports dependencies from all of workspace.txt
@@ -0,0 +1,246 @@
+# GENERATED BY: test/outdated/outdated_test.dart
+
+## Section 0
+$ pub outdated --json
+{
+  "packages": [
+    {
+      "package": "dep",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "isCurrentRetracted": false,
+      "isCurrentAffectedByAdvisory": false,
+      "current": {
+        "version": "0.8.0"
+      },
+      "upgradable": {
+        "version": "0.8.0",
+        "overridden": true
+      },
+      "resolvable": {
+        "version": "0.8.0",
+        "overridden": true
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    },
+    {
+      "package": "dep_a",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "isCurrentRetracted": false,
+      "isCurrentAffectedByAdvisory": false,
+      "current": {
+        "version": "0.9.0"
+      },
+      "upgradable": {
+        "version": "0.9.5"
+      },
+      "resolvable": {
+        "version": "1.0.0"
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    },
+    {
+      "package": "dev_dep_a",
+      "kind": "dev",
+      "isDiscontinued": false,
+      "isCurrentRetracted": false,
+      "isCurrentAffectedByAdvisory": false,
+      "current": {
+        "version": "0.9.0"
+      },
+      "upgradable": {
+        "version": "0.9.5"
+      },
+      "resolvable": {
+        "version": "1.0.0"
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    }
+  ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 1
+$ pub outdated --no-color
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable           Resolvable           Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.8.0 (overridden)  *0.8.0 (overridden)  1.0.0   
+dep_a         *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+dev_dependencies:
+dev_dep_a     *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+2 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `dart pub upgrade`.
+
+2  dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 2
+$ pub outdated --no-color --no-transitive
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable           Resolvable           Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.8.0 (overridden)  *0.8.0 (overridden)  1.0.0   
+dep_a         *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+dev_dependencies:
+dev_dep_a     *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+2 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `dart pub upgrade`.
+
+2  dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 3
+$ pub outdated --no-color --up-to-date
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable           Resolvable           Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.8.0 (overridden)  *0.8.0 (overridden)  1.0.0   
+dep_a         *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+dev_dependencies:
+dev_dep_a     *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+2 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `dart pub upgrade`.
+
+2  dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 4
+$ pub outdated --no-color --prereleases
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable           Resolvable           Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.8.0 (overridden)  *0.8.0 (overridden)  1.0.0   
+dep_a         *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+dev_dependencies:
+dev_dep_a     *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+2 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `dart pub upgrade`.
+
+2  dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 5
+$ pub outdated --no-color --no-dev-dependencies
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable           Resolvable           Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.8.0 (overridden)  *0.8.0 (overridden)  1.0.0   
+dep_a         *0.9.0   *0.9.5               1.0.0                1.0.0   
+
+1 upgradable dependency is locked (in pubspec.lock) to an older version.
+To update it, use `dart pub upgrade`.
+
+1 dependency is constrained to a version that is older than a resolvable version.
+To update it, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 6
+$ pub outdated --no-color --no-dependency-overrides
+Showing outdated packages.
+[*] indicates versions that are not the latest available.
+
+Package Name  Current  Upgradable  Resolvable  Latest  
+
+direct dependencies:
+dep           *0.8.0   *0.9.5      1.0.0       1.0.0   
+dep_a         *0.9.0   *0.9.5      1.0.0       1.0.0   
+
+dev_dependencies:
+dev_dep_a     *0.9.0   *0.9.5      1.0.0       1.0.0   
+
+3 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `dart pub upgrade`.
+
+3  dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml, or run `dart pub upgrade --major-versions`.
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section 7
+$ pub outdated --json --no-dev-dependencies
+{
+  "packages": [
+    {
+      "package": "dep",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "isCurrentRetracted": false,
+      "isCurrentAffectedByAdvisory": false,
+      "current": {
+        "version": "0.8.0"
+      },
+      "upgradable": {
+        "version": "0.8.0",
+        "overridden": true
+      },
+      "resolvable": {
+        "version": "0.8.0",
+        "overridden": true
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    },
+    {
+      "package": "dep_a",
+      "kind": "direct",
+      "isDiscontinued": false,
+      "isCurrentRetracted": false,
+      "isCurrentAffectedByAdvisory": false,
+      "current": {
+        "version": "0.9.0"
+      },
+      "upgradable": {
+        "version": "0.9.5"
+      },
+      "resolvable": {
+        "version": "1.0.0"
+      },
+      "latest": {
+        "version": "1.0.0"
+      }
+    }
+  ]
+}
+