diff --git a/main/src/main/scala/sbt/internal/graph/rendering/TreeView.scala b/main/src/main/scala/sbt/internal/graph/rendering/TreeView.scala index d5732f5b7..f3cd7b4f7 100644 --- a/main/src/main/scala/sbt/internal/graph/rendering/TreeView.scala +++ b/main/src/main/scala/sbt/internal/graph/rendering/TreeView.scala @@ -10,14 +10,17 @@ package internal package graph package rendering -import java.io.{ OutputStream, InputStream, FileOutputStream, File } -import java.net.URI - -import graph.{ Module, ModuleGraph } import sbt.io.IO -import scala.annotation.{ nowarn, tailrec } -import scala.util.parsing.json.{ JSONArray, JSONObject } +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.net.URI +import scala.annotation.nowarn +import scala.annotation.tailrec +import scala.util.parsing.json.JSONArray +import scala.util.parsing.json.JSONObject @nowarn object TreeView { def createJson(graph: ModuleGraph): String = { @@ -36,18 +39,27 @@ import scala.util.parsing.json.{ JSONArray, JSONObject } new URI(graphHTML.toURI.toString) } - private def processSubtree(graph: ModuleGraph, module: Module): JSONObject = { - val children = graph.dependencyMap - .getOrElse(module.id, List()) - .map(module => processSubtree(graph, module)) - .toList - moduleAsJson(module, children) + private[rendering] def processSubtree( + graph: ModuleGraph, + module: Module, + parents: Set[GraphModuleId] = Set() + ): JSONObject = { + val cycle = parents.contains(module.id) + val dependencies = if (cycle) List() else graph.dependencyMap.getOrElse(module.id, List()) + val children = + dependencies.map(dependency => processSubtree(graph, dependency, parents + module.id)).toList + moduleAsJson(module, cycle, children) } - private def moduleAsJson(module: Module, children: List[JSONObject]): JSONObject = { + private def moduleAsJson( + module: Module, + isCycle: Boolean, + children: List[JSONObject] + ): JSONObject = { val eviction = module.evictedByVersion.map(version => s" (evicted by $version)").getOrElse("") + val cycle = if (isCycle) " (cycle)" else "" val error = module.error.map(err => s" (errors: $err)").getOrElse("") - val text = module.id.idString + eviction + error + val text = module.id.idString + eviction + error + cycle JSONObject(Map("text" -> text, "children" -> JSONArray(children))) } diff --git a/main/src/test/scala/sbt/internal/graph/rendering/TreeViewTest.scala b/main/src/test/scala/sbt/internal/graph/rendering/TreeViewTest.scala new file mode 100644 index 000000000..544629e16 --- /dev/null +++ b/main/src/test/scala/sbt/internal/graph/rendering/TreeViewTest.scala @@ -0,0 +1,55 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt +package internal +package graph +package rendering + +import org.scalatest.DiagrammedAssertions +import org.scalatest.FunSuite + +import scala.annotation.nowarn +import scala.util.parsing.json.JSONArray +import scala.util.parsing.json.JSONObject + +@nowarn("msg=class JSONObject in package json is deprecated") +class TreeViewTest extends FunSuite with DiagrammedAssertions { + + val modA = GraphModuleId("orgA", "nameA", "1.0") + val modB = GraphModuleId("orgB", "nameB", "2.0") + + val graph = ModuleGraph( + nodes = Seq(Module(modA), Module(modB)), + edges = Seq( + modA -> modA, + modA -> modB, + ) + ) + + test("TreeView should detect cycles and truncate") { + val json = TreeView.processSubtree(graph, Module(modA)) + val (rootText, children) = parseTree(json) + assert(rootText == modA.idString) + + val childrenText = children.map(parseTree).map(_._1) + val expected = List(s"${modA.idString} (cycle)", modB.idString) + assert(childrenText == expected) + } + + @nowarn("cat=unchecked") + def parseTree(json: JSONObject): (String, List[JSONObject]) = { + (json.obj.get("text"), json.obj.get("children")) match { + case (Some(text: String), Some(JSONArray(children: List[JSONObject]))) + if children.forall(_.isInstanceOf[JSONObject]) => + text -> children + case _ => + fail("a string field 'text' and an array of objects in 'children' field were expected!") + } + } + +}