From 63ea25db6befdd521bc1ca444b76152a98ee828a Mon Sep 17 00:00:00 2001 From: "Ryan Brandenburg (He/Him)" Date: Wed, 19 Aug 2026 14:04:40 -0700 Subject: [PATCH] Handle circular dependencies --- .../conda/CondaDependencyResolver.cs | 21 ++++++++-- .../CondaLockComponentDetectorTests.cs | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs index c84637640..c6d6dd9c6 100644 --- a/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs +++ b/src/Microsoft.ComponentDetection.Detectors/conda/CondaDependencyResolver.cs @@ -18,7 +18,10 @@ public static class CondaDependencyResolver /// The full condaLock object. /// The SingleFileComponentRecorder. public static void RecordDependencyGraphFromFile(CondaLock condaLock, ISingleFileComponentRecorder singleFileComponentRecorder) - => GetPackages(condaLock).ForEach(package => RegisterPackageWithDependencies(package, null, condaLock, singleFileComponentRecorder)); + { + HashSet<(string Name, string Version)> visitedPackages = []; + GetPackages(condaLock).ForEach(package => RegisterPackageWithDependencies(package, null, condaLock, singleFileComponentRecorder, visitedPackages)); + } /// /// Updates all registered packages that don't have any ancestors. @@ -60,7 +63,13 @@ public static void UpdateDirectlyReferencedPackages(ISingleFileComponentRecorder /// The id of the parent package. /// The full condaLock object. /// The SingleFileComponentRecorder. - private static void RegisterPackageWithDependencies(CondaPackage package, string parentId, CondaLock condaLock, ISingleFileComponentRecorder singleFileComponentRecorder) + /// The packages whose dependencies have already been registered. + private static void RegisterPackageWithDependencies( + CondaPackage package, + string parentId, + CondaLock condaLock, + ISingleFileComponentRecorder singleFileComponentRecorder, + ISet<(string Name, string Version)> visitedPackages) { if (package == null) { @@ -72,13 +81,19 @@ private static void RegisterPackageWithDependencies(CondaPackage package, string //// Register the package itself. RegisterPackage(component, parentId, false, singleFileComponentRecorder); + if (!visitedPackages.Add((package.Name, package.Version))) + { + return; + } + //// Register all dependencies of the package. package.Dependencies.Keys.ToList().ForEach(dependency => RegisterPackageWithDependencies( condaLock?.Package.FirstOrDefault(condaPackage => condaPackage.Name == dependency && condaPackage.Platform == package.Platform), component.Id, condaLock, - singleFileComponentRecorder)); + singleFileComponentRecorder, + visitedPackages)); } /// diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs index 4902b205a..3b1e61cc9 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/CondaLockComponentDetectorTests.cs @@ -104,6 +104,46 @@ public async Task CondaComponentDetector_TestCondaLockFileAsync() detectedComponents.Should().HaveCount(4); } + [TestMethod] + public async Task CondaComponentDetector_CircularDependenciesAsync() + { + var condaLockContent = +@"version: 1 +package: +- name: alpha + version: 1.0.0 + manager: conda + platform: linux-64 + dependencies: + beta: '>=1.0.0' + category: main + optional: false +- name: beta + version: 1.0.0 + manager: conda + platform: linux-64 + dependencies: + alpha: '>=1.0.0' + category: main + optional: false +"; + + var (scanResult, componentRecorder) = await this.detectorTestUtility + .WithFile("conda-lock.yml", condaLockContent) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + var alpha = detectedComponents.Single(component => component.Component is CondaComponent condaComponent && condaComponent.Name == "alpha"); + var beta = detectedComponents.Single(component => component.Component is CondaComponent condaComponent && condaComponent.Name == "beta"); + var dependencyGraph = componentRecorder.GetDependencyGraphsByLocation().Values.Single(); + + detectedComponents.Should().HaveCount(2); + dependencyGraph.GetDependenciesForComponent(alpha.Component.Id).Should().ContainSingle(beta.Component.Id); + dependencyGraph.GetDependenciesForComponent(beta.Component.Id).Should().ContainSingle(alpha.Component.Id); + } + private void AssertCondaLockComponentNameAndVersion(IEnumerable detectedComponents, string name, string version) { detectedComponents.SingleOrDefault(c =>