Repository: vanniktech/gradle-dependency-graph-generator-plugin
Pull request #134 (2021-09-26 06:33:23)

Support generateProjectDependencyGraph in SubProject
In monorepo, generateProjectDependencyGraph from individual sub project would be more practical. 
eg. `./gradlew app:generateProjectDependencyGraph`

tedliang: #125 will be achieved by this merge request as well. 
vanniktech: @tedliang can you show an before and after image of your repository?
tedliang: <img width="432" alt="Screen Shot 2021-09-26 at 9 53 42 pm" src="https://user-images.githubusercontent.com/1089679/134806774-c886c1ed-3b73-46c0-a310-42ec9607a6f1.png">

vanniktech: @tedliang thanks! Do you mind adding a test for this new behavior? Then we can merge

Patch:
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
index 81d2514..1c5faaf 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt
@@ -21,64 +21,59 @@ internal class ProjectDependencyGraphGenerator(
   private val projectGenerator: DependencyGraphGeneratorExtension.ProjectGenerator
 ) {
   fun generateGraph(): MutableGraph {
-    val graph = mutGraph().setDirected(true)
-    graph.graphAttrs().add(Label.of(project.name).locate(Label.Location.TOP), Font.size(DEFAULT_FONT_SIZE))
-    graph.nodeAttrs().add(Font.name("Times New Roman"), Style.FILLED)
-
-    val rootProjects = project.allprojects.toMutableList()
     val projects = mutableSetOf<Project>()
     val dependencies = mutableListOf<ProjectDependencyContainer>()
+    fun addProject(project: Project) {
+      if (projectGenerator.includeProject(project) && projects.add(project)) {
+        project.configurations
+          .flatMap { configuration ->
+            configuration.dependencies
+              .withType(ProjectDependency::class.java)
+              .map { ProjectDependencyContainer(project, it.dependencyProject, configuration.name.toLowerCase(US).endsWith("implementation")) }
+          }
+          .forEach {
+            dependencies.add(it)
+            addProject(it.to)
+          }
+      }
+    }
+    project.allprojects.filter { it.isDependingOnOtherProject() }.forEach { addProject(it) }
 
-    addProjects(projects, rootProjects, dependencies, graph)
-    rankRootProjects(graph, projects, rootProjects)
+    val graph = mutGraph().setDirected(true)
+    graph.graphAttrs().add(Label.of(project.name).locate(Label.Location.TOP), Font.size(DEFAULT_FONT_SIZE))
+    graph.nodeAttrs().add(Font.name("Times New Roman"), Style.FILLED)
+    projects.forEach { addNode(it, dependencies, graph) }
+    rankRootProjects(graph, projects, dependencies)
     addDependencies(dependencies, graph)
 
     return projectGenerator.graph(graph)
   }
 
-  @Suppress("Detekt.ComplexMethod") private fun addProjects(projects: MutableSet<Project>, rootProjects: MutableList<Project>, dependencies: MutableList<ProjectDependencyContainer>, graph: MutableGraph) {
-    project.allprojects
-        .filter { projectGenerator.includeProject(it) }
-        .flatMap { project -> project.configurations.map { project to it } }
-        .flatMap { (project, configuration) ->
-          configuration.dependencies
-              .withType(ProjectDependency::class.java)
-              .map { it.dependencyProject }
-              .flatMap { projectDependency ->
-                projects.add(project)
-                projects.add(projectDependency)
-
-                rootProjects.remove(projectDependency)
-                dependencies.add(ProjectDependencyContainer(project, projectDependency, configuration.name.toLowerCase(US).endsWith("implementation")))
-                listOf(project, projectDependency)
-              }
-        }
-        .forEach { project ->
-          val node = mutNode(project.path)
+  private fun addNode(project: Project, dependencies: List<ProjectDependencyContainer>, graph: MutableGraph) {
+    val node = mutNode(project.path)
 
-          if (rootProjects.contains(project)) {
-            node.add(Shape.RECTANGLE)
-          } else if (project.isCommonsProject()) {
-            node.add(Style.DASHED, Color.BLACK)
-          }
+    if (dependencies.none { it.to == project }) {
+      node.add(Shape.RECTANGLE)
+    } else if (project.isCommonsProject()) {
+      node.add(Style.DASHED, Color.BLACK)
+    }
 
-          when {
-            project.isJsProject() -> node.add(Color.rgb("#fff176").fill())
-            project.isAndroidProject() -> node.add(Color.rgb("#81c784").fill())
-            project.isKotlinProject() -> node.add(Color.rgb("#ffb74d").fill())
-            project.isJavaProject() -> node.add(Color.rgb("#ff8a65").fill())
-            else -> node.add(Color.rgb("#e0e0e0").fill())
-          }
+    when {
+      project.isJsProject() -> node.add(Color.rgb("#fff176").fill())
+      project.isAndroidProject() -> node.add(Color.rgb("#81c784").fill())
+      project.isKotlinProject() -> node.add(Color.rgb("#ffb74d").fill())
+      project.isJavaProject() -> node.add(Color.rgb("#ff8a65").fill())
+      else -> node.add(Color.rgb("#e0e0e0").fill())
+    }
 
-          graph.add(projectGenerator.projectNode(node, project))
-        }
+    graph.add(projectGenerator.projectNode(node, project))
   }
 
-  @Suppress("Detekt.SpreadOperator") private fun rankRootProjects(graph: MutableGraph, projects: MutableSet<Project>, rootProjects: MutableList<Project>) {
+  @Suppress("Detekt.SpreadOperator") private fun rankRootProjects(graph: MutableGraph, projects: MutableSet<Project>, dependencies: List<ProjectDependencyContainer>) {
     graph.add(graph()
         .graphAttr()
         .with(Rank.SAME)
-        .with(*projects.filter { rootProjects.contains(it) }.map { mutNode(it.path) }.toTypedArray()))
+        .with(*projects.filter { project -> dependencies.none { it.to == project } }.map { mutNode(it.path) }.toTypedArray()))
   }
 
   private fun addDependencies(dependencies: MutableList<ProjectDependencyContainer>, graph: MutableGraph) {
diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
index 77218d1..2da18ef 100644
--- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
+++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/extensions.kt
@@ -1,6 +1,7 @@
 package com.vanniktech.dependency.graph.generator
 
 import org.gradle.api.Project
+import org.gradle.api.artifacts.ProjectDependency
 
 private val whitespaceRegex = Regex("\\s")
 
@@ -22,6 +23,8 @@ internal fun String.toHyphenCase(): String {
 
 internal val Project.dotIdentifier get() = "$group$name".dotIdentifier
 
+fun Project.isDependingOnOtherProject() = configurations.any { configuration -> configuration.dependencies.any { it is ProjectDependency } }
+
 fun Project.isJavaProject() = listOf("java-library", "java", "java-gradle-plugin").any { plugins.hasPlugin(it) }
 
 fun Project.isKotlinProject() = listOf("kotlin", "kotlin-android", "kotlin-platform-jvm").any { plugins.hasPlugin(it) }


Test patch:
diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorPluginTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorPluginTest.kt
index 7375ae3..91c6eb8 100644
--- a/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorPluginTest.kt
+++ b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorPluginTest.kt
@@ -197,7 +197,10 @@ class DependencyGraphGeneratorPluginTest {
 
     val app = testProjectDir.newFolder("app").run { parentFile.name + name }
     testProjectDir.newFile("app/build.gradle").writeText("""
-        |plugins { id "java-library" }
+        |plugins {
+        |  id "java-library"
+        |  id "com.vanniktech.dependency.graph.generator"
+        |}
         |
         |repositories { mavenCentral() }
         |
@@ -213,7 +216,7 @@ class DependencyGraphGeneratorPluginTest {
         .withPluginClasspath()
         .withGradleVersion("5.0")
         .withProjectDir(testProjectDir.root)
-        .withArguments("generateDependencyGraph", "generateProjectDependencyGraph")
+        .withArguments("generateDependencyGraph", "generateProjectDependencyGraph", "app:generateProjectDependencyGraph")
         .build()
 
     result.tasks.filter { it.path.contains("DependencyGraph") }.forEach {
@@ -253,9 +256,9 @@ class DependencyGraphGeneratorPluginTest {
     // We don't want to assert the content of the image, just that it exists.
     assertThat(File(testProjectDir.root, "build/reports/project-dependency-graph/project-dependency-graph.svg")).exists()
 
-    assertThat(File(testProjectDir.root, "build/reports/project-dependency-graph/project-dependency-graph.dot")).hasContent("""
+    fun projectDependencyGraph(label: String) = """
         digraph {
-        graph ["fontsize"="35","label"="${testProjectDir.root.name}","labelloc"="t"]
+        graph ["fontsize"="35","label"="$label","labelloc"="t"]
         node ["fontname"="Times New Roman","style"="filled"]
         ":app" ["fillcolor"="#ff8a65","shape"="rectangle"]
         ":lib1" ["fillcolor"="#ff8a65"]
@@ -269,6 +272,13 @@ class DependencyGraphGeneratorPluginTest {
         ":app" -> ":lib2" ["style"="dotted"]
         ":lib1" -> ":lib"
         ":lib2" -> ":lib"
-        }""".trimIndent())
+        }""".trimIndent()
+
+    assertThat(File(testProjectDir.root, "build/reports/project-dependency-graph/project-dependency-graph.dot")).hasContent(projectDependencyGraph(testProjectDir.root.name))
+
+    // We don't want to assert the content of the image, just that it exists.
+    assertThat(File(testProjectDir.root, "app/build/reports/project-dependency-graph/project-dependency-graph.svg")).exists()
+
+    assertThat(File(testProjectDir.root, "app/build/reports/project-dependency-graph/project-dependency-graph.dot")).hasContent(projectDependencyGraph("app"))
   }
 }