From 6fd86edb32a90922d86a42f9c15b3672a64bdf6b Mon Sep 17 00:00:00 2001
From: Matthew Vaughan <mattvaughan38@gmail.com>
Date: Thu, 7 Apr 2022 15:45:24 -0600
Subject: [PATCH] feat(manager/nuget): Restore all dependent project files to
 generate new lock files (#14312)

Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
---
 .../circular-reference/one/one.csproj         |  16 +++
 .../circular-reference/two/two.csproj         |  16 +++
 .../single-project-file/single.csproj         |  12 ++
 .../__fixtures__/two-no-reference/one.csproj  |  20 ++++
 .../__fixtures__/two-no-reference/two.csproj  |  12 ++
 .../two-one-reference/one/one.csproj          |  12 ++
 .../two-one-reference/one/packages.lock.json  |  13 +++
 .../two-one-reference/two/packages.lock.json  |  19 ++++
 .../two-one-reference/two/two.csproj          |  16 +++
 lib/modules/manager/nuget/artifacts.spec.ts   |   4 +-
 lib/modules/manager/nuget/artifacts.ts        |  86 ++++++++++----
 .../manager/nuget/package-tree.spec.ts        |  82 ++++++++++++++
 lib/modules/manager/nuget/package-tree.ts     | 107 ++++++++++++++++++
 package.json                                  |   1 +
 tsconfig.strict.json                          |   1 +
 yarn.lock                                     |   6 +-
 16 files changed, 398 insertions(+), 25 deletions(-)
 create mode 100644 lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json
 create mode 100644 lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj
 create mode 100644 lib/modules/manager/nuget/package-tree.spec.ts
 create mode 100644 lib/modules/manager/nuget/package-tree.ts

diff --git a/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj b/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj
new file mode 100644
index 0000000000..3bb51c6cdb
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/circular-reference/one/one.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../two/two.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj b/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj
new file mode 100644
index 0000000000..0500818314
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/circular-reference/two/two.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../one/one.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj b/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj
new file mode 100644
index 0000000000..8edcb2d22f
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/single-project-file/single.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj b/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj
new file mode 100644
index 0000000000..836c7a6433
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-no-reference/one.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+    <DisableImplicitNuGetFallbackFolder>true</DisableImplicitNuGetFallbackFolder>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+    <PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
+    <PackageReference Include="Serilog.Exceptions" Version="5.6.0" />
+    <PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
+    <PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
+    <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
+    <PackageReference Include="serilog.sinks.graylog" Version="2.3.0" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.1.3" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj b/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj
new file mode 100644
index 0000000000..8edcb2d22f
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-no-reference/two.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj
new file mode 100644
index 0000000000..8edcb2d22f
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/one.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json
new file mode 100644
index 0000000000..374c404b2d
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/one/packages.lock.json
@@ -0,0 +1,13 @@
+{
+  "version": 1,
+  "dependencies": {
+    ".NETCoreApp,Version=v5.0": {
+      "Serilog": {
+        "type": "Direct",
+        "requested": "[2.10.0, )",
+        "resolved": "2.10.0",
+        "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA=="
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json
new file mode 100644
index 0000000000..426b0a04bf
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/packages.lock.json
@@ -0,0 +1,19 @@
+{
+  "version": 1,
+  "dependencies": {
+    ".NETCoreApp,Version=v5.0": {
+      "Serilog": {
+        "type": "Direct",
+        "requested": "[2.10.0, )",
+        "resolved": "2.10.0",
+        "contentHash": "+QX0hmf37a0/OZLxM3wL7V6/ADvC1XihXN4Kq/p6d8lCPfgkRdiuhbWlMaFjR9Av0dy5F0+MBeDmDdRZN/YwQA=="
+      },
+      "one": {
+        "type": "Project",
+        "dependencies": {
+          "Serilog": "2.10.0"
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj
new file mode 100644
index 0000000000..0500818314
--- /dev/null
+++ b/lib/modules/manager/nuget/__fixtures__/two-one-reference/two/two.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>net5.0</TargetFramework>
+    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Serilog" Version="2.10.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../one/one.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/lib/modules/manager/nuget/artifacts.spec.ts b/lib/modules/manager/nuget/artifacts.spec.ts
index e46ffc0b7a..3d9d356bf7 100644
--- a/lib/modules/manager/nuget/artifacts.spec.ts
+++ b/lib/modules/manager/nuget/artifacts.spec.ts
@@ -1,7 +1,7 @@
 import { exec as _exec } from 'child_process';
 import { join } from 'upath';
 import { envMock, mockExecAll } from '../../../../test/exec-util';
-import { fs, mocked } from '../../../../test/util';
+import { fs, git, mocked } from '../../../../test/util';
 import { GlobalConfig } from '../../../config/global';
 import type { RepoGlobalConfig } from '../../../config/types';
 import * as docker from '../../../util/exec/docker';
@@ -19,6 +19,7 @@ jest.mock('child_process');
 jest.mock('../../../util/exec/env');
 jest.mock('../../../util/fs');
 jest.mock('../../../util/host-rules');
+jest.mock('../../../util/git');
 jest.mock('./util');
 
 const exec: jest.Mock<typeof _exec> = _exec as any;
@@ -48,6 +49,7 @@ describe('modules/manager/nuget/artifacts', () => {
     fs.ensureCacheDir.mockImplementation((dirName: string) =>
       Promise.resolve(`others/${dirName}`)
     );
+    git.getFileList.mockResolvedValueOnce([]);
     getRandomString.mockReturnValue('not-so-random' as any);
     GlobalConfig.set(adminConfig);
     docker.resetPrefetchedImages();
diff --git a/lib/modules/manager/nuget/artifacts.ts b/lib/modules/manager/nuget/artifacts.ts
index 4467624cd5..fc73500dec 100644
--- a/lib/modules/manager/nuget/artifacts.ts
+++ b/lib/modules/manager/nuget/artifacts.ts
@@ -21,6 +21,7 @@ import type {
   UpdateArtifactsConfig,
   UpdateArtifactsResult,
 } from '../types';
+import { getDependentPackageFiles } from './package-tree';
 import {
   getConfiguredRegistries,
   getDefaultRegistries,
@@ -57,6 +58,7 @@ async function addSourceCmds(
 
 async function runDotnetRestore(
   packageFileName: string,
+  dependentPackageFileNames: string[],
   config: UpdateArtifactsConfig
 ): Promise<void> {
   const execOptions: ExecOptions = {
@@ -72,15 +74,33 @@ async function runDotnetRestore(
     nugetConfigFile,
     `<?xml version="1.0" encoding="utf-8"?>\n<configuration>\n</configuration>\n`
   );
+
   const cmds = [
     ...(await addSourceCmds(packageFileName, config, nugetConfigFile)),
-    `dotnet restore ${packageFileName} --force-evaluate --configfile ${nugetConfigFile}`,
+    ...dependentPackageFileNames.map(
+      (f) =>
+        `dotnet restore ${f} --force-evaluate --configfile ${nugetConfigFile}`
+    ),
   ];
-  logger.debug({ cmd: cmds }, 'dotnet command');
   await exec(cmds, execOptions);
   await remove(nugetConfigDir);
 }
 
+async function getLockFileContentMap(
+  lockFileNames: string[]
+): Promise<Record<string, string>> {
+  const lockFileContentMap: Record<string, string> = {};
+
+  for (const lockFileName of lockFileNames) {
+    lockFileContentMap[lockFileName] = await readLocalFile(
+      lockFileName,
+      'utf8'
+    );
+  }
+
+  return lockFileContentMap;
+}
+
 export async function updateArtifacts({
   packageFileName,
   newPackageFileContent,
@@ -101,15 +121,29 @@ export async function updateArtifacts({
     return null;
   }
 
-  const lockFileName = getSiblingFileName(
+  const packageFiles = [
+    ...(await getDependentPackageFiles(packageFileName)),
     packageFileName,
-    'packages.lock.json'
+  ];
+
+  logger.trace(
+    { packageFiles },
+    `Found ${packageFiles.length} dependent package files`
+  );
+
+  const lockFileNames = packageFiles.map((f) =>
+    getSiblingFileName(f, 'packages.lock.json')
   );
-  const existingLockFileContent = await readLocalFile(lockFileName, 'utf8');
-  if (!existingLockFileContent) {
+
+  const existingLockFileContentMap = await getLockFileContentMap(lockFileNames);
+
+  const hasLockFileContent = Object.values(existingLockFileContentMap).some(
+    (val) => !!val
+  );
+  if (!hasLockFileContent) {
     logger.debug(
       { packageFileName },
-      'No lock file found beneath package file.'
+      'No lock file found for package or dependents'
     );
     return null;
   }
@@ -124,23 +158,29 @@ export async function updateArtifacts({
 
     await writeLocalFile(packageFileName, newPackageFileContent);
 
-    await runDotnetRestore(packageFileName, config);
+    await runDotnetRestore(packageFileName, packageFiles, config);
 
-    const newLockFileContent = await readLocalFile(lockFileName, 'utf8');
-    if (existingLockFileContent === newLockFileContent) {
-      logger.debug(`Lock file is unchanged`);
-      return null;
+    const newLockFileContentMap = await getLockFileContentMap(lockFileNames);
+
+    const retArray = [];
+    for (const lockFileName of lockFileNames) {
+      if (
+        existingLockFileContentMap[lockFileName] ===
+        newLockFileContentMap[lockFileName]
+      ) {
+        logger.trace(`Lock file ${lockFileName} is unchanged`);
+      } else {
+        retArray.push({
+          file: {
+            type: 'addition',
+            path: lockFileName,
+            contents: newLockFileContentMap[lockFileName],
+          },
+        });
+      }
     }
-    logger.debug('Returning updated lock file');
-    return [
-      {
-        file: {
-          type: 'addition',
-          path: lockFileName,
-          contents: await readLocalFile(lockFileName),
-        },
-      },
-    ];
+
+    return retArray.length > 0 ? retArray : null;
   } catch (err) {
     // istanbul ignore if
     if (err.message === TEMPORARY_ERROR) {
@@ -150,7 +190,7 @@ export async function updateArtifacts({
     return [
       {
         artifactError: {
-          lockFile: lockFileName,
+          lockFile: lockFileNames.join(', '),
           stderr: err.message,
         },
       },
diff --git a/lib/modules/manager/nuget/package-tree.spec.ts b/lib/modules/manager/nuget/package-tree.spec.ts
new file mode 100644
index 0000000000..2e494b59d7
--- /dev/null
+++ b/lib/modules/manager/nuget/package-tree.spec.ts
@@ -0,0 +1,82 @@
+import { fs as memfs } from 'memfs';
+import upath from 'upath';
+import { Fixtures } from '../../../../test/fixtures';
+import { git } from '../../../../test/util';
+import { GlobalConfig } from '../../../config/global';
+import type { RepoGlobalConfig } from '../../../config/types';
+import { getDependentPackageFiles } from './package-tree';
+
+jest.mock('fs', () => memfs);
+jest.mock('fs-extra', () => Fixtures.fsExtra());
+jest.mock('../../../util/git');
+
+const adminConfig: RepoGlobalConfig = {
+  localDir: upath.resolve('/tmp/repo'),
+};
+
+describe('modules/manager/nuget/package-tree', () => {
+  describe('getDependentPackageFiles()', () => {
+    beforeEach(() => {
+      GlobalConfig.set(adminConfig);
+      Fixtures.reset();
+    });
+
+    afterEach(() => {
+      GlobalConfig.reset();
+      Fixtures.reset();
+    });
+
+    it('returns empty list for single project', async () => {
+      git.getFileList.mockResolvedValue(['single.csproj']);
+      Fixtures.mock({
+        '/tmp/repo/single.csproj': Fixtures.get(
+          'single-project-file/single.csproj'
+        ),
+      });
+
+      expect(await getDependentPackageFiles('single.csproj')).toBeEmpty();
+    });
+
+    it('returns empty list for two projects with no references', async () => {
+      git.getFileList.mockResolvedValue(['one.csproj', 'two.csproj']);
+      Fixtures.mock({
+        '/tmp/repo/one.csproj': Fixtures.get('two-no-reference/one.csproj'),
+        '/tmp/repo/two.csproj': Fixtures.get('two-no-reference/two.csproj'),
+      });
+
+      expect(await getDependentPackageFiles('one.csproj')).toBeEmpty();
+    });
+
+    it('returns project for two projects with one reference', async () => {
+      git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']);
+      Fixtures.mock({
+        '/tmp/repo/one/one.csproj': Fixtures.get(
+          'two-one-reference/one/one.csproj'
+        ),
+        '/tmp/repo/two/two.csproj': Fixtures.get(
+          'two-one-reference/two/two.csproj'
+        ),
+      });
+
+      expect(await getDependentPackageFiles('one/one.csproj')).toEqual([
+        'two/two.csproj',
+      ]);
+    });
+
+    it('throws error on circular reference', async () => {
+      git.getFileList.mockResolvedValue(['one/one.csproj', 'two/two.csproj']);
+      Fixtures.mock({
+        '/tmp/repo/one/one.csproj': Fixtures.get(
+          'circular-reference/one/one.csproj'
+        ),
+        '/tmp/repo/two/two.csproj': Fixtures.get(
+          'circular-reference/two/two.csproj'
+        ),
+      });
+
+      await expect(getDependentPackageFiles('one/one.csproj')).rejects.toThrow(
+        'Circular reference detected in NuGet package files'
+      );
+    });
+  });
+});
diff --git a/lib/modules/manager/nuget/package-tree.ts b/lib/modules/manager/nuget/package-tree.ts
new file mode 100644
index 0000000000..6d697d11e4
--- /dev/null
+++ b/lib/modules/manager/nuget/package-tree.ts
@@ -0,0 +1,107 @@
+import Graph from 'graph-data-structure';
+import minimatch from 'minimatch';
+import upath from 'upath';
+import xmldoc from 'xmldoc';
+import { logger } from '../../../logger';
+import { readLocalFile } from '../../../util/fs';
+import { getFileList } from '../../../util/git';
+
+/**
+ * Get all package files at any level of ancestry that depend on packageFileName
+ */
+export async function getDependentPackageFiles(
+  packageFileName: string
+): Promise<string[]> {
+  const packageFiles = await getAllPackageFiles();
+  const graph: ReturnType<typeof Graph> = Graph();
+
+  for (const f of packageFiles) {
+    graph.addNode(f);
+  }
+
+  for (const f of packageFiles) {
+    const packageFileContent = (await readLocalFile(f, 'utf8')).toString();
+
+    const doc = new xmldoc.XmlDocument(packageFileContent);
+    const projectReferenceAttributes = (
+      doc
+        .childrenNamed('ItemGroup')
+        .map((ig) => ig.childrenNamed('ProjectReference')) ?? []
+    )
+      .flat()
+      .map((pf) => pf.attr['Include']);
+
+    const projectReferences = projectReferenceAttributes.map((a) =>
+      upath.normalize(a)
+    );
+    const normalizedRelativeProjectReferences = projectReferences.map((r) =>
+      reframeRelativePathToRootOfRepo(f, r)
+    );
+
+    for (const ref of normalizedRelativeProjectReferences) {
+      graph.addEdge(ref, f);
+    }
+
+    if (graph.hasCycle()) {
+      throw new Error('Circular reference detected in NuGet package files');
+    }
+  }
+
+  return recursivelyGetDependentPackageFiles(packageFileName, graph);
+}
+
+/**
+ * Traverse graph and find dependent package files at any level of ancestry
+ */
+function recursivelyGetDependentPackageFiles(
+  packageFileName: string,
+  graph: ReturnType<typeof Graph>
+): string[] {
+  const dependents: string[] = graph.adjacent(packageFileName);
+
+  if (dependents.length === 0) {
+    return [];
+  }
+
+  return dependents.concat(
+    dependents.map((d) => recursivelyGetDependentPackageFiles(d, graph)).flat()
+  );
+}
+
+/**
+ * Take the path relative from a project file, and make it relative from the root of the repo
+ */
+function reframeRelativePathToRootOfRepo(
+  dependentProjectRelativePath: string,
+  projectReference: string
+): string {
+  const virtualRepoRoot = '/';
+  const absoluteDependentProjectPath = upath.resolve(
+    virtualRepoRoot,
+    dependentProjectRelativePath
+  );
+  const absoluteProjectReferencePath = upath.resolve(
+    upath.dirname(absoluteDependentProjectPath),
+    projectReference
+  );
+  const relativeProjectReferencePath = upath.relative(
+    virtualRepoRoot,
+    absoluteProjectReferencePath
+  );
+
+  return relativeProjectReferencePath;
+}
+
+/**
+ * Get a list of package files in localDir
+ */
+async function getAllPackageFiles(): Promise<string[]> {
+  const allFiles = await getFileList();
+  const filteredPackageFiles = allFiles.filter(
+    minimatch.filter('*.{cs,vb,fs}proj', { matchBase: true, nocase: true })
+  );
+
+  logger.trace({ filteredPackageFiles }, 'Found package files');
+
+  return filteredPackageFiles;
+}
diff --git a/package.json b/package.json
index 323540b7f0..e63c10a6d2 100644
--- a/package.json
+++ b/package.json
@@ -168,6 +168,7 @@
     "global-agent": "3.0.0",
     "good-enough-parser": "1.1.7",
     "got": "11.8.3",
+    "graph-data-structure": "2.0.0",
     "handlebars": "4.7.7",
     "hasha": "5.2.2",
     "ignore": "5.2.0",
diff --git a/tsconfig.strict.json b/tsconfig.strict.json
index 5d902abee5..0270c8bab8 100644
--- a/tsconfig.strict.json
+++ b/tsconfig.strict.json
@@ -241,6 +241,7 @@
     "lib/modules/manager/nuget/extract.ts",
     "lib/modules/manager/nuget/extract/global-manifest.ts",
     "lib/modules/manager/nuget/index.ts",
+    "lib/modules/manager/nuget/package-tree.ts",
     "lib/modules/manager/nuget/util.ts",
     "lib/modules/manager/nvm/extract.ts",
     "lib/modules/manager/nvm/index.ts",
diff --git a/yarn.lock b/yarn.lock
index 2a664f1ea2..7a542e5433 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5066,6 +5066,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4,
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96"
   integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==
 
+graph-data-structure@2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/graph-data-structure/-/graph-data-structure-2.0.0.tgz#a95762a067f81f0fe4ea77525253b946a941fa89"
+  integrity sha512-ravWDe9LaV0u27ZDme/8w5xHyyTqIWQsetpzDvBNJsGy4nZQB5IVw/u+7ngdEMZWsHYim+PAHatV4cGQX1XKpQ==
+
 grapheme-splitter@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
@@ -5168,7 +5173,6 @@ hosted-git-info@^5.0.0:
   integrity sha512-rRnjWu0Bxj+nIfUOkz0695C0H6tRrN5iYIzYejb0tDEefe2AekHu/U5Kn9pEie5vsJqpNQU02az7TGSH3qpz4Q==
   dependencies:
     lru-cache "^7.5.1"
-
 html-encoding-sniffer@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3"
-- 
GitLab