diff --git a/CHANGELOG.md b/CHANGELOG.md index 34a1daf..bb0f8c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ Version 0.9.0 *(In development)* -------------------------------- +- Add configurable node types to the project diagrams, with custom colors/shapes. [\#163](https://github.com/vanniktech/gradle-dependency-graph-generator-plugin/pull/266) ([jonapoul](https://github.com/jonapoul)) + Version 0.8.0 *(2022-06-21)* ---------------------------- @@ -99,4 +101,4 @@ Version 0.2.0 *(2018-03-07)* Version 0.1.0 *(2018-03-04)* ---------------------------- -- Initial release \ No newline at end of file +- Initial release diff --git a/build.gradle b/build.gradle index d3a77ae..87e212b 100755 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,7 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation "com.android.tools.build:gradle:8.10.0" + testImplementation "org.jetbrains.kotlin.native.cocoapods:org.jetbrains.kotlin.native.cocoapods.gradle.plugin:$kotlinVersion" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" // https://github.com/gradle/gradle/issues/16774#issuecomment-893493869 diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt index 87d798c..44cd5a5 100644 --- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt +++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGenerator.kt @@ -89,6 +89,10 @@ internal class DependencyGraphGenerator( .add(Label.of(dependency.getDisplayName())) .add(Shape.RECTANGLE) + val type = generator.dependencyMapper.invoke(dependency) + type?.color?.let(node::add) + type?.shape?.let(node::add) + val mutated = generator.dependencyNode.invoke(node, dependency) nodes[identifier] = mutated graph.add(mutated) diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt index 65211cd..b303130 100644 --- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt +++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorExtension.kt @@ -84,6 +84,8 @@ open class DependencyGraphGeneratorExtension(project: Project) { @get:Nested var graph: (MutableGraph) -> MutableGraph = { it }, /** Allows you to configure the [Graphviz] instance. */ @get:Nested var graphviz: (Graphviz) -> Graphviz = { it }, + /** Determines the color/shape of the project node on the output diagram */ + @get:Nested var dependencyMapper: DependencyMapper = { null }, ) { /** Gradle task name that is associated with this generator. */ @get:Internal val gradleTaskName = "generateDependencyGraph${ @@ -138,6 +140,8 @@ open class DependencyGraphGeneratorExtension(project: Project) { @get:Nested var graph: (MutableGraph) -> MutableGraph = { it }, /** Allows you to configure the [Graphviz] instance. */ @get:Nested var graphviz: (Graphviz) -> Graphviz = { it }, + /** Determines the color/shape of the project node on the output diagram */ + @get:Nested var projectMapper: ProjectMapper = DefaultProjectMapper, ) { /** Gradle task name that is associated with this generator. */ @get:Internal val gradleTaskName = "generateProjectDependencyGraph${ diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/NodeType.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/NodeType.kt new file mode 100644 index 0000000..e59f0b4 --- /dev/null +++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/NodeType.kt @@ -0,0 +1,76 @@ +package com.vanniktech.dependency.graph.generator + +import guru.nidi.graphviz.attribute.Color +import guru.nidi.graphviz.attribute.Shape +import org.gradle.api.Project +import org.gradle.api.artifacts.ResolvedDependency + +interface NodeType { + val color: Color? + val shape: Shape? +} + +data class BasicNodeType( + override val color: Color?, + override val shape: Shape?, +) : NodeType { + constructor(rgbHex: String, shape: Shape?) : this(Color.rgb(rgbHex).fill(), shape) + constructor(rgbInt: Int, shape: Shape?) : this(Color.rgb(rgbInt).fill(), shape) +} + +typealias ProjectMapper = (Project) -> NodeType? + +typealias DependencyMapper = (ResolvedDependency) -> NodeType? + +/** + * Use this to apply a semi-random fill color onto each node, where the same color is used on every dependency of the + * same group. E.g. "a.b:c" will have the same color as "a.b:d", but different to "e.f:c" + */ +val TintDependencyByGroup: DependencyMapper = { dependency -> + BasicNodeType(dependency.moduleGroup.hashCode(), shape = null) +} + +internal enum class DefaultProjectType( + override val color: Color, + override val shape: Shape? = null, +) : NodeType { + ANDROID(color = Color.rgb("#66BB6A").fill()), // green + JVM(color = Color.rgb("#FF7043").fill()), // orange + IOS(color = Color.rgb("#42A5F5").fill()), // blue + JS(color = Color.rgb("#FFCA28").fill()), // yellow + MULTIPLATFORM(color = Color.rgb("#A280FF").fill()), // lilac + OTHER(color = Color.rgb("#BDBDBD").fill()), // grey + ; +} + +val DefaultProjectMapper: ProjectMapper = { project -> + when { + project.hasAnyPlugin("org.jetbrains.kotlin.multiplatform") -> + DefaultProjectType.MULTIPLATFORM + + project.hasAnyPlugin( + "com.android.library", + "com.android.application", + "com.android.test", + "com.android.feature", + "com.android.instantapp", + ) -> DefaultProjectType.ANDROID + + project.hasAnyPlugin( + "java-library", + "java", + "java-gradle-plugin", + "application", + "org.jetbrains.kotlin.jvm", + ) -> DefaultProjectType.JVM + + project.hasAnyPlugin("org.jetbrains.kotlin.native.cocoapods") -> + DefaultProjectType.IOS + + project.hasAnyPlugin("com.eriwen.gradle.js") -> + DefaultProjectType.JS + + else -> + DefaultProjectType.OTHER + } +} 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 f4b571e..3a0eb0a 100644 --- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt +++ b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGenerator.kt @@ -59,7 +59,14 @@ internal class ProjectDependencyGraphGenerator( node.add(Shape.RECTANGLE) } - node.add(project.target().color) + val type = projectGenerator.projectMapper.invoke(project) + type?.color?.let(node::add) + type?.shape?.let(node::add) + + if (node.attrs().isEmpty) { + project.logger.warn("Node '${node.name()}' has no attributes, it won't be visible on the diagram") + } + graph.add(projectGenerator.projectNode(node, project)) } diff --git a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectTarget.kt b/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectTarget.kt deleted file mode 100644 index 90e2ec4..0000000 --- a/src/main/kotlin/com/vanniktech/dependency/graph/generator/ProjectTarget.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.vanniktech.dependency.graph.generator - -import guru.nidi.graphviz.attribute.Color - -internal enum class ProjectTarget( - val ids: Set, - val color: Color, -) { - ANDROID( - ids = setOf("com.android.library", "com.android.application", "com.android.test", "com.android.feature", "com.android.instantapp"), - color = Color.rgb("#66BB6A").fill(), - ), - JVM( - ids = setOf("java-library", "java", "java-gradle-plugin", "application", "org.jetbrains.kotlin.jvm"), - color = Color.rgb("#FF7043").fill(), - ), - IOS( - ids = setOf("org.jetbrains.kotlin.native.cocoapods"), - color = Color.rgb("#42A5F5").fill(), - ), - JS( - ids = setOf("com.eriwen.gradle.js"), - color = Color.rgb("#FFCA28").fill(), - ), - MULTIPLATFORM( - ids = setOf("org.jetbrains.kotlin.multiplatform"), - color = Color.rgb("#A280FF").fill(), - ), - OTHER( - ids = emptySet(), - color = Color.rgb("#BDBDBD").fill(), - ), - ; -} 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 c330aee..f043235 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,5 @@ package com.vanniktech.dependency.graph.generator -import com.vanniktech.dependency.graph.generator.ProjectTarget.MULTIPLATFORM import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ProjectDependency @@ -20,21 +19,9 @@ internal fun String.toHyphenCase(): String { fun Project.isDependingOnOtherProject() = configurations.any { configuration -> configuration.dependencies.any { it is ProjectDependency } } -fun Project.isCommonsProject() = plugins.hasPlugin("org.jetbrains.kotlin.platform.common") - -internal fun Project.target(): ProjectTarget { - val targets = ProjectTarget.values() - .filter { target -> target.ids.any { plugins.hasPlugin(it) } } - - val withoutMultiplatform = targets.minus(MULTIPLATFORM) - - return when { - targets.contains(MULTIPLATFORM) -> MULTIPLATFORM - else -> withoutMultiplatform.firstOrNull() ?: ProjectTarget.OTHER - } -} - internal fun Configuration.isImplementation() = name.lowercase().endsWith("implementation") internal val Project.buildDirectory: File get() = layout.buildDirectory.asFile.get() + +internal fun Project.hasAnyPlugin(vararg ids: String) = ids.any { pluginManager.hasPlugin(it) } diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorMapperTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorMapperTest.kt new file mode 100644 index 0000000..93dc328 --- /dev/null +++ b/src/test/java/com/vanniktech/dependency/graph/generator/DependencyGraphGeneratorMapperTest.kt @@ -0,0 +1,98 @@ +package com.vanniktech.dependency.graph.generator + +import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.Generator.Companion.ALL +import guru.nidi.graphviz.attribute.Shape +import org.gradle.api.Project +import org.gradle.api.plugins.JavaLibraryPlugin +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class DependencyGraphGeneratorMapperTest { + private lateinit var project: Project + + @Before + fun setUp() { + project = buildTestProject(name = "root") + with(project) { + plugins.apply(JavaLibraryPlugin::class.java) + repositories.run { add(mavenCentral()) } + dependencies.add("api", "org.jetbrains.kotlin:kotlin-stdlib:1.2.30") + dependencies.add("api", "org.jetbrains.kotlin:kotlin-reflect:1.2.30") + dependencies.add("implementation", "io.reactivex.rxjava2:rxjava:2.1.10") + } + } + + @Test + fun tintedByGroupDependencyMapperTest() { + assertEquals( + // language=dot + """ + digraph "G" { + edge ["dir"="forward"] + graph ["dpi"="100"] + "root" ["label"="root","shape"="rectangle"] + "orgjetbrainskotlinkotlinstdlib" ["label"="kotlin-stdlib","shape"="rectangle","fillcolor"="#9b5ba3"] + "orgjetbrainsannotations" ["label"="jetbrains-annotations","shape"="rectangle","fillcolor"="#504d8c"] + "orgjetbrainskotlinkotlinreflect" ["label"="kotlin-reflect","shape"="rectangle","fillcolor"="#9b5ba3"] + "ioreactivexrxjava2rxjava" ["label"="rxjava","shape"="rectangle","fillcolor"="#0afeb3"] + "orgreactivestreamsreactivestreams" ["label"="reactive-streams","shape"="rectangle","fillcolor"="#ea70d0"] + { + edge ["dir"="none"] + graph ["rank"="same"] + "root" + } + "root" -> "orgjetbrainskotlinkotlinstdlib" + "root" -> "orgjetbrainskotlinkotlinreflect" + "root" -> "ioreactivexrxjava2rxjava" + "orgjetbrainskotlinkotlinstdlib" -> "orgjetbrainsannotations" + "orgjetbrainskotlinkotlinreflect" -> "orgjetbrainskotlinkotlinstdlib" + "ioreactivexrxjava2rxjava" -> "orgreactivestreamsreactivestreams" + } + """.trimIndent(), + DependencyGraphGenerator( + project = project, + generator = ALL.copy(dependencyMapper = TintDependencyByGroup), + ).generateGraph().toString(), + ) + } + + @Test + fun customDependencyMapperTest() { + val mapper: DependencyMapper = { dependency -> + println("${dependency.moduleGroup} ${dependency.name} ${dependency.moduleName}") + when { + dependency.moduleName.contains("x") -> BasicNodeType("#ABC123", Shape.EGG) + dependency.moduleName.contains("k") -> BasicNodeType("#DEF789", Shape.POINT) + else -> null + } + } + assertEquals( + // language=dot + """ + digraph "G" { + edge ["dir"="forward"] + graph ["dpi"="100"] + "root" ["label"="root","shape"="rectangle"] + "orgjetbrainskotlinkotlinstdlib" ["label"="kotlin-stdlib","shape"="point","fillcolor"="#DEF789"] + "orgjetbrainsannotations" ["label"="jetbrains-annotations","shape"="rectangle"] + "orgjetbrainskotlinkotlinreflect" ["label"="kotlin-reflect","shape"="point","fillcolor"="#DEF789"] + "ioreactivexrxjava2rxjava" ["label"="rxjava","shape"="egg","fillcolor"="#ABC123"] + "orgreactivestreamsreactivestreams" ["label"="reactive-streams","shape"="rectangle"] + { + edge ["dir"="none"] + graph ["rank"="same"] + "root" + } + "root" -> "orgjetbrainskotlinkotlinstdlib" + "root" -> "orgjetbrainskotlinkotlinreflect" + "root" -> "ioreactivexrxjava2rxjava" + "orgjetbrainskotlinkotlinstdlib" -> "orgjetbrainsannotations" + "orgjetbrainskotlinkotlinreflect" -> "orgjetbrainskotlinkotlinstdlib" + "ioreactivexrxjava2rxjava" -> "orgreactivestreamsreactivestreams" + } + """.trimIndent(), + DependencyGraphGenerator(project, ALL.copy(dependencyMapper = mapper)).generateGraph().toString(), + ) + } +} diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorIncludeProjectTest.kt b/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorIncludeProjectTest.kt index eda4b40..4c180f2 100644 --- a/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorIncludeProjectTest.kt +++ b/src/test/java/com/vanniktech/dependency/graph/generator/ProjectDependencyGraphGeneratorIncludeProjectTest.kt @@ -1,9 +1,9 @@ package com.vanniktech.dependency.graph.generator import com.vanniktech.dependency.graph.generator.DependencyGraphGeneratorExtension.ProjectGenerator.Companion.ALL +import guru.nidi.graphviz.attribute.Color +import guru.nidi.graphviz.attribute.Shape import org.gradle.api.Project -import org.gradle.api.plugins.JavaLibraryPlugin -import org.gradle.testfixtures.ProjectBuilder import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -15,25 +15,20 @@ class ProjectDependencyGraphGeneratorIncludeProjectTest { private lateinit var lib1: Project private lateinit var lib2: Project - @Before fun setUp() { - root = ProjectBuilder.builder().withName("root").build() - root.plugins.apply(JavaLibraryPlugin::class.java) - - app = ProjectBuilder.builder().withParent(root).withName("app").build() - app.plugins.apply(JavaLibraryPlugin::class.java) - - lib1 = ProjectBuilder.builder().withParent(root).withName("lib1").build() - lib1.plugins.apply(JavaLibraryPlugin::class.java) - - lib2 = ProjectBuilder.builder().withParent(root).withName("lib2").build() - lib2.plugins.apply(JavaLibraryPlugin::class.java) + @Before + fun setUp() { + root = buildTestProject("root", parent = null, "java-library") + app = buildTestProject("app", root, "java-library") + lib1 = buildTestProject("lib1", root, "java-library") + lib2 = buildTestProject("lib2", root, "java-library") app.dependencies.add("implementation", lib1) app.dependencies.add("implementation", lib2) lib1.dependencies.add("implementation", lib2) } - @Test fun excludeNonLeafProject() { + @Test + fun excludeNonLeafProject() { assertEquals( // language=dot """ @@ -55,7 +50,8 @@ class ProjectDependencyGraphGeneratorIncludeProjectTest { ) } - @Test fun excludeLeafProject() { + @Test + fun excludeLeafProject() { assertEquals( // language=dot """ @@ -76,4 +72,108 @@ class ProjectDependencyGraphGeneratorIncludeProjectTest { ProjectDependencyGraphGenerator(root, ALL.copy(includeProject = { it != lib2 })).generateGraph().toString(), ) } + + @Test + fun withDifferentProjectTypesUsingDefaultMapper() { + val androidLib = buildTestProject("android", root, "com.android.library") + val iosLib = buildTestProject("ios", root, "org.jetbrains.kotlin.native.cocoapods") + val jvmLib = buildTestProject("jvm", root, "org.jetbrains.kotlin.jvm") + val kmpLib = buildTestProject("kmp", root, "org.jetbrains.kotlin.multiplatform") + val otherLib = buildTestProject("other", root, pluginId = null) + + app.dependencies.add("implementation", androidLib) + app.dependencies.add("implementation", iosLib) + app.dependencies.add("implementation", jvmLib) + app.dependencies.add("implementation", kmpLib) + app.dependencies.add("implementation", otherLib) + + assertEquals( + // language=dot + """ + digraph { + edge ["dir"="forward"] + graph ["dpi"="100","label"="root","labelloc"="t","fontsize"="35"] + node ["style"="filled"] + ":app" ["shape"="rectangle","fillcolor"="#FF7043"] + ":lib1" ["fillcolor"="#FF7043"] + ":lib2" ["fillcolor"="#FF7043"] + ":android" ["fillcolor"="#66BB6A"] + ":ios" ["fillcolor"="#42A5F5"] + ":jvm" ["fillcolor"="#FF7043"] + ":kmp" ["fillcolor"="#A280FF"] + ":other" ["fillcolor"="#BDBDBD"] + { + edge ["dir"="none"] + graph ["rank"="same"] + ":app" + } + ":app" -> ":lib1" ["style"="dotted"] + ":app" -> ":lib2" ["style"="dotted"] + ":app" -> ":android" ["style"="dotted"] + ":app" -> ":ios" ["style"="dotted"] + ":app" -> ":jvm" ["style"="dotted"] + ":app" -> ":kmp" ["style"="dotted"] + ":app" -> ":other" ["style"="dotted"] + ":lib1" -> ":lib2" ["style"="dotted"] + } + """.trimIndent(), + ProjectDependencyGraphGenerator(root, ALL).generateGraph().toString(), + ) + } + + private enum class TestProject(override val color: Color?, override val shape: Shape?) : NodeType { + ABC(color = Color.rgb("#FFFFFF").fill(), shape = Shape.PARALLELOGRAM), + DEF(color = Color.rgb("#ABCDEF").fill(), shape = Shape.THREE_P_OVERHANG), + XYZ(color = null, shape = Shape.ASSEMBLY), + } + + @Test + fun withDifferentProjectTypesUsingCustomMapper() { + val root = buildTestProject("root", parent = null, pluginId = null) + val app = buildTestProject("app", root, "java-library") + + val abcLib = buildTestProject("abc", root, pluginId = null) { + extensions.extraProperties.set("some-property", "abc") + } + + val defLib = buildTestProject("def", root, pluginId = null) + val xyzLib = buildTestProject("xyz", root, pluginId = null) + + app.dependencies.add("implementation", abcLib) + app.dependencies.add("implementation", defLib) + app.dependencies.add("implementation", xyzLib) + + val mapper: ProjectMapper = { proj -> + when { + proj.extensions.extraProperties.has("some-property") -> TestProject.ABC + proj.path.contains("def") -> TestProject.DEF + else -> TestProject.XYZ + } + } + val customGenerator = ALL.copy(projectMapper = mapper) + + assertEquals( + // language=dot + """ + digraph { + edge ["dir"="forward"] + graph ["dpi"="100","label"="root","labelloc"="t","fontsize"="35"] + node ["style"="filled"] + ":app" ["shape"="assembly"] + ":abc" ["fillcolor"="#FFFFFF","shape"="parallelogram"] + ":def" ["fillcolor"="#ABCDEF","shape"="threepoverhang"] + ":xyz" ["shape"="assembly"] + { + edge ["dir"="none"] + graph ["rank"="same"] + ":app" + } + ":app" -> ":abc" ["style"="dotted"] + ":app" -> ":def" ["style"="dotted"] + ":app" -> ":xyz" ["style"="dotted"] + } + """.trimIndent(), + ProjectDependencyGraphGenerator(root, customGenerator).generateGraph().toString(), + ) + } } diff --git a/src/test/java/com/vanniktech/dependency/graph/generator/utils.kt b/src/test/java/com/vanniktech/dependency/graph/generator/utils.kt new file mode 100644 index 0000000..fff726e --- /dev/null +++ b/src/test/java/com/vanniktech/dependency/graph/generator/utils.kt @@ -0,0 +1,19 @@ +package com.vanniktech.dependency.graph.generator + +import org.gradle.api.Project +import org.gradle.api.internal.project.DefaultProject +import org.gradle.testfixtures.ProjectBuilder + +internal fun buildTestProject( + name: String, + parent: Project? = null, + pluginId: String? = null, + config: Project.() -> Unit = {}, +): DefaultProject = ProjectBuilder + .builder() + .withParent(parent) + .withName(name) + .build() + .also { if (pluginId != null) it.plugins.apply(pluginId) } + .also { it.config() } + as DefaultProject