From e7323171a29522deceff873ac9f6b3524707e650 Mon Sep 17 00:00:00 2001 From: calm <148254234+calm329@users.noreply.github.com> Date: Sun, 11 Jan 2026 18:27:11 -0800 Subject: [PATCH] fix: Handle relocated dependencies in dependencyTree (#8400) (#8489) --- .../graph/backend/SbtUpdateReport.scala | 17 ++- .../graph/backend/SbtUpdateReportTest.scala | 122 ++++++++++++++++++ .../relocated-dependency/build.sbt | 11 ++ .../relocated-dependency/test | 2 + 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 main/src/test/scala/sbt/internal/graph/backend/SbtUpdateReportTest.scala create mode 100644 sbt-app/src/sbt-test/dependency-graph/relocated-dependency/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-graph/relocated-dependency/test diff --git a/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala b/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala index 3ff0684bd..194f79c39 100644 --- a/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala +++ b/main/src/main/scala/sbt/internal/graph/backend/SbtUpdateReport.scala @@ -50,7 +50,22 @@ object SbtUpdateReport { val (nodes, edges) = report.details.flatMap(moduleEdges).unzip val root = Module(rootInfo) + val allNodes = root +: nodes + val flatEdges = edges.flatten + val existingNodeIds = allNodes.map(_.id).toSet - ModuleGraph(root +: nodes, edges.flatten) + // Handle relocated dependencies where the caller node doesn't exist (#8400) + val fixedEdges = flatEdges.flatMap { case edge @ (from, to) => + if (existingNodeIds.contains(from)) Seq(edge) + else { + val callersOfMissing = flatEdges.collect { + case (caller, target) if target == from => caller + } + if (callersOfMissing.isEmpty) Seq(Edge(root.id, to)) + else callersOfMissing.map(caller => Edge(caller, to)) + } + } + + ModuleGraph(allNodes, fixedEdges.distinct) } } diff --git a/main/src/test/scala/sbt/internal/graph/backend/SbtUpdateReportTest.scala b/main/src/test/scala/sbt/internal/graph/backend/SbtUpdateReportTest.scala new file mode 100644 index 000000000..7b32c176a --- /dev/null +++ b/main/src/test/scala/sbt/internal/graph/backend/SbtUpdateReportTest.scala @@ -0,0 +1,122 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.graph.backend + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import sbt.internal.graph.GraphModuleId +import sbt.librarymanagement.* + +class SbtUpdateReportTest extends AnyFlatSpec with Matchers { + + def caller(org: String, name: String, version: String): Caller = + Caller( + ModuleID(org, name, version), + Vector.empty, + Map.empty, + isForceDependency = false, + isChangingDependency = false, + isTransitiveDependency = false, + isDirectlyForceDependency = false + ) + + def moduleReport( + org: String, + name: String, + version: String, + callers: Vector[Caller] = Vector.empty + ): ModuleReport = + ModuleReport( + ModuleID(org, name, version), + artifacts = Vector.empty, + missingArtifacts = Vector.empty + ).withCallers(callers) + + // #8400 + "fromConfigurationReport" should "handle relocated direct dependencies" in { + val root = ModuleID("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val relocatedReport = moduleReport( + "at.yawk.lz4", + "lz4-java", + "1.8.1", + callers = Vector(caller("org.lz4", "lz4-java", "1.8.1")) + ) + val orgArtReport = + OrganizationArtifactReport("at.yawk.lz4", "lz4-java", Vector(relocatedReport)) + val configReport = ConfigurationReport( + ConfigRef("compile"), + modules = Vector(relocatedReport), + details = Vector(orgArtReport) + ) + + val graph = SbtUpdateReport.fromConfigurationReport(configReport, root) + + graph.nodes.size shouldBe 2 + val rootId = GraphModuleId("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val relocatedId = GraphModuleId("at.yawk.lz4", "lz4-java", "1.8.1") + graph.edges should contain((rootId, relocatedId)) + graph.dependencyMap(rootId).map(_.id) should contain(relocatedId) + } + + it should "handle normal dependencies without relocation" in { + val root = ModuleID("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val normalReport = moduleReport( + "org.example", + "example-lib", + "1.0.0", + callers = Vector(caller("test", "test-project_2.12", "0.1.0-SNAPSHOT")) + ) + val orgArtReport = + OrganizationArtifactReport("org.example", "example-lib", Vector(normalReport)) + val configReport = ConfigurationReport( + ConfigRef("compile"), + modules = Vector(normalReport), + details = Vector(orgArtReport) + ) + + val graph = SbtUpdateReport.fromConfigurationReport(configReport, root) + + graph.nodes.size shouldBe 2 + val rootId = GraphModuleId("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val depId = GraphModuleId("org.example", "example-lib", "1.0.0") + graph.edges should contain((rootId, depId)) + } + + it should "handle transitive relocated dependencies" in { + val root = ModuleID("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val depA = moduleReport( + "org.example", + "dep-a", + "1.0.0", + callers = Vector(caller("test", "test-project_2.12", "0.1.0-SNAPSHOT")) + ) + val relocatedB = moduleReport( + "new.group", + "dep-b", + "2.0.0", + callers = Vector(caller("old.group", "dep-b", "2.0.0")) + ) + val orgArtReportA = OrganizationArtifactReport("org.example", "dep-a", Vector(depA)) + val orgArtReportB = OrganizationArtifactReport("new.group", "dep-b", Vector(relocatedB)) + val configReport = ConfigurationReport( + ConfigRef("compile"), + modules = Vector(depA, relocatedB), + details = Vector(orgArtReportA, orgArtReportB) + ) + + val graph = SbtUpdateReport.fromConfigurationReport(configReport, root) + + graph.nodes.size shouldBe 3 + val rootId = GraphModuleId("test", "test-project_2.12", "0.1.0-SNAPSHOT") + val depAId = GraphModuleId("org.example", "dep-a", "1.0.0") + val relocatedBId = GraphModuleId("new.group", "dep-b", "2.0.0") + graph.edges should contain((rootId, depAId)) + graph.edges should contain((rootId, relocatedBId)) + } +} diff --git a/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/build.sbt b/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/build.sbt new file mode 100644 index 000000000..f0192ea3e --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/build.sbt @@ -0,0 +1,11 @@ +// #8400 +scalaVersion := "2.12.21" +libraryDependencies += "org.lz4" % "lz4-java" % "1.8.1" + +TaskKey[Unit]("check") := { + val content = IO.read(new File("target/tree.txt")) + assert( + content.contains("at.yawk.lz4:lz4-java:1.8.1"), + s"Expected relocated dependency in tree:\n$content" + ) +} diff --git a/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/test b/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/test new file mode 100644 index 000000000..c06133c1f --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-graph/relocated-dependency/test @@ -0,0 +1,2 @@ +> dependencyTree --out target/tree.txt +> check