From eecccfa485a8f16c15fd04c575b78e217b762b50 Mon Sep 17 00:00:00 2001
From: Kasper Juul Hermansen <contact@kjuulh.io>
Date: Fri, 9 Dec 2022 06:11:32 +0100
Subject: [PATCH] feat: handle workspace.dependencies in cargo (#18831)

---
 docs/usage/rust.md                        |  2 +-
 lib/modules/manager/cargo/extract.spec.ts | 63 +++++++++++++++++++++++
 lib/modules/manager/cargo/extract.ts      | 25 ++++++++-
 lib/modules/manager/cargo/types.ts        |  3 ++
 lib/types/skip-reason.ts                  |  3 +-
 5 files changed, 93 insertions(+), 3 deletions(-)

diff --git a/docs/usage/rust.md b/docs/usage/rust.md
index 1ecfbba694..cf7494e0b4 100644
--- a/docs/usage/rust.md
+++ b/docs/usage/rust.md
@@ -10,7 +10,7 @@ Renovate supports upgrading dependencies in `Cargo.toml` files and their accompa
 ## How it works
 
 1. Renovate searches in each repository for any `Cargo.toml` files
-1. Renovate extracts existing dependencies from `[dependencies]`, `[dev-dependencies]` and `[build-dependencies]`
+1. Renovate extracts existing dependencies from `[dependencies]`, `[dev-dependencies]`, `[build-dependencies]` and `[workspace.dependencies]`
 1. Renovate tries to find and parse a `.cargo/config.toml` file to discover index URLs for private registries
 1. Renovate resolves the dependency's version using the crates.io API or by cloning the index URL
 1. If Renovate finds an update, Renovate will use `cargo update` to update both `Cargo.toml` and `Cargo.lock`
diff --git a/lib/modules/manager/cargo/extract.spec.ts b/lib/modules/manager/cargo/extract.spec.ts
index 38cdf69cdc..1800fc57da 100644
--- a/lib/modules/manager/cargo/extract.spec.ts
+++ b/lib/modules/manager/cargo/extract.spec.ts
@@ -1,3 +1,4 @@
+import { codeBlock } from 'common-tags';
 import { DirectoryResult, dir } from 'tmp-promise';
 import { join } from 'upath';
 import { Fixtures } from '../../../../test/fixtures';
@@ -112,6 +113,68 @@ describe('modules/manager/cargo/extract', () => {
       expect(res?.deps).toHaveLength(3);
     });
 
+    it('extracts workspace dependencies', async () => {
+      const cargoToml = codeBlock`
+[package]
+name = "renovate-test"
+version = "0.1.0"
+authors = ["John Doe <john.doe@example.org>"]
+edition = "2018"
+
+[dependencies]
+git2 = "0.14.0"
+
+[workspace]
+members = ["pcap-sys"]
+
+[workspace.dependencies]
+serde = "1.0.146"
+tokio = { version = "1.21.1" }`;
+      const res = await extractPackageFile(cargoToml, 'Cargo.toml', config);
+      expect(res?.deps).toEqual([
+        {
+          currentValue: '0.14.0',
+          datasource: 'crate',
+          depName: 'git2',
+          depType: 'dependencies',
+          managerData: { nestedVersion: false },
+        },
+        {
+          currentValue: '1.0.146',
+          datasource: 'crate',
+          depName: 'serde',
+          depType: 'workspace.dependencies',
+          managerData: { nestedVersion: false },
+        },
+        {
+          currentValue: '1.21.1',
+          datasource: 'crate',
+          depName: 'tokio',
+          depType: 'workspace.dependencies',
+          managerData: {
+            nestedVersion: true,
+          },
+        },
+      ]);
+    });
+
+    it('skips workspace dependency', async () => {
+      const cargotoml = '[dependencies]\nfoobar = { workspace = true }';
+      const res = await extractPackageFile(cargotoml, 'Cargo.toml', config);
+      expect(res?.deps).toEqual([
+        {
+          currentValue: '',
+          datasource: 'crate',
+          depName: 'foobar',
+          depType: 'dependencies',
+          managerData: {
+            nestedVersion: false,
+          },
+          skipReason: 'inherited-dependency',
+        },
+      ]);
+    });
+
     it('skips unknown registries', async () => {
       const cargotoml =
         '[dependencies]\nfoobar = { version = "0.1.0", registry = "not-listed" }';
diff --git a/lib/modules/manager/cargo/extract.ts b/lib/modules/manager/cargo/extract.ts
index 85d38ca1e0..87a334aa53 100644
--- a/lib/modules/manager/cargo/extract.ts
+++ b/lib/modules/manager/cargo/extract.ts
@@ -15,7 +15,8 @@ function extractFromSection(
   parsedContent: CargoSection,
   section: keyof CargoSection,
   cargoRegistries: CargoRegistries,
-  target?: string
+  target?: string,
+  depTypeOverride?: string
 ): PackageDependency[] {
   const deps: PackageDependency[] = [];
   const sectionContent = parsedContent[section];
@@ -34,6 +35,7 @@ function extractFromSection(
       const path = currentValue.path;
       const git = currentValue.git;
       const registryName = currentValue.registry;
+      const workspace = currentValue.workspace;
 
       packageName = currentValue.package;
 
@@ -60,6 +62,9 @@ function extractFromSection(
       } else if (git) {
         currentValue = '';
         skipReason = 'git-dependency';
+      } else if (workspace) {
+        currentValue = '';
+        skipReason = 'inherited-dependency';
       } else {
         currentValue = '';
         skipReason = 'invalid-dependency-specification';
@@ -84,6 +89,9 @@ function extractFromSection(
     if (packageName) {
       dep.packageName = packageName;
     }
+    if (depTypeOverride) {
+      dep.depType = depTypeOverride;
+    }
     deps.push(dep);
   });
   return deps;
@@ -152,6 +160,7 @@ export async function extractPackageFile(
     [dev-dependencies]
     [build-dependencies]
     [target.*.dependencies]
+    [workspace.dependencies]
   */
   const targetSection = cargoManifest.target;
   // An array of all dependencies in the target section
@@ -184,11 +193,25 @@ export async function extractPackageFile(
       targetDeps = targetDeps.concat(deps);
     });
   }
+
+  const workspaceSection = cargoManifest.workspace;
+  let workspaceDeps: PackageDependency[] = [];
+  if (workspaceSection) {
+    workspaceDeps = extractFromSection(
+      workspaceSection,
+      'dependencies',
+      cargoRegistries,
+      undefined,
+      'workspace.dependencies'
+    );
+  }
+
   const deps = [
     ...extractFromSection(cargoManifest, 'dependencies', cargoRegistries),
     ...extractFromSection(cargoManifest, 'dev-dependencies', cargoRegistries),
     ...extractFromSection(cargoManifest, 'build-dependencies', cargoRegistries),
     ...targetDeps,
+    ...workspaceDeps,
   ];
   if (!deps.length) {
     return null;
diff --git a/lib/modules/manager/cargo/types.ts b/lib/modules/manager/cargo/types.ts
index deb3189907..fa239f3081 100644
--- a/lib/modules/manager/cargo/types.ts
+++ b/lib/modules/manager/cargo/types.ts
@@ -9,6 +9,8 @@ export interface CargoDep {
   registry?: string;
   /** Name of a package to look up */
   package?: string;
+  /** Whether the dependency is inherited from the workspace*/
+  workspace?: boolean;
 }
 
 export type CargoDeps = Record<string, CargoDep | string>;
@@ -21,6 +23,7 @@ export interface CargoSection {
 
 export interface CargoManifest extends CargoSection {
   target?: Record<string, CargoSection>;
+  workspace?: CargoSection;
 }
 
 export interface CargoConfig {
diff --git a/lib/types/skip-reason.ts b/lib/types/skip-reason.ts
index 5f5ded004e..7164f169b0 100644
--- a/lib/types/skip-reason.ts
+++ b/lib/types/skip-reason.ts
@@ -45,4 +45,5 @@ export type SkipReason =
   | 'is-pinned'
   | 'missing-depname'
   | 'recursive-placeholder'
-  | 'github-token-required';
+  | 'github-token-required'
+  | 'inherited-dependency';
-- 
GitLab