diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index e77cdda..5d13f8e 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -20,5 +20,5 @@ jobs: distribution: 'zulu' java-version: 8 - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v4 diff --git a/README.md b/README.md index d4df2df..fce5117 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,7 @@ Optional settings: - If set, only these modules and their dependencies (direct and transitive) will be included in the graph. - If not set, all modules are considered root modules, which means the graph will include all modules and their dependencies. - This is useful when you want to focus on a specific part of your project's dependency structure. +- **includeIndependentModules**: Whether to include modules with no connections (no dependencies and no dependants) in the graph. Default is `false`. ### Multiple graphs diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c35211..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt index 92c5789..35b71a3 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt @@ -112,6 +112,14 @@ abstract class CreateModuleGraphTask : DefaultTask() { @get:Optional abstract val nestingEnabled: Property + @get:Input + @get:Option( + option = "includeIsolatedModules", + description = "Whether to render modules with no dependencies or dependants", + ) + @get:Optional + abstract val includeIsolatedModules: Property + @get:Input @get:Option(option = "graphModels", description = "The produced graph models") internal abstract val graphModels: ListProperty diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphExtension.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphExtension.kt index bd83ed9..d310178 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphExtension.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphExtension.kt @@ -111,6 +111,12 @@ open class ModuleGraphExtension @Inject constructor(project: Project) { */ val nestingEnabled: Property = objects.property(Boolean::class.java) + /** + * Whether to include modules that have no dependencies and no dependants. + * Disabled by default. + */ + val includeIsolatedModules: Property = objects.property(Boolean::class.java) + /** * A list of additional graph configs to generate graphs for. */ diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt index 189db89..21e0fda 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt @@ -43,6 +43,7 @@ open class ModuleGraphPlugin : Plugin { task.projectDirectory.set(project.layout.projectDirectory) task.strictMode.set(extension.strictMode) task.nestingEnabled.set(extension.nestingEnabled) + task.includeIsolatedModules.set(extension.includeIsolatedModules) val primaryGraphConfig = getPrimaryGraphConfig(task) val additionalGraphConfigs = task.graphConfigs.getOrElse(emptyList()) @@ -89,6 +90,7 @@ open class ModuleGraphPlugin : Plugin { val excludedModulesRegex = task.excludedModulesRegex.orNull val rootModulesRegex = task.rootModulesRegex.orNull val showFullPath = task.showFullPath.orNull + val includeIsolatedModules = task.includeIsolatedModules.orNull val strictMode = task.strictMode.orNull val nestingEnabled = task.nestingEnabled.orNull @@ -104,6 +106,7 @@ open class ModuleGraphPlugin : Plugin { excludedModulesRegex, rootModulesRegex, showFullPath, + includeIsolatedModules, strictMode, nestingEnabled, ) @@ -126,6 +129,7 @@ open class ModuleGraphPlugin : Plugin { this.linkText = linkText this.setStyleByModuleType = setStyleByModuleType this.showFullPath = showFullPath + this.includeIsolatedModules = includeIsolatedModules this.excludedConfigurationsRegex = excludedConfigurationsRegex this.excludedModulesRegex = excludedModulesRegex this.rootModulesRegex = rootModulesRegex diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/ProjectParser.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/ProjectParser.kt index a3790b3..766e956 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/ProjectParser.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/graphparser/ProjectParser.kt @@ -80,7 +80,7 @@ internal object ProjectParser { /** * To handle root modules we need to parse the dependency tree recursively, * starting at the root module nodes. - * This ensures we only include module nodes that are reachable from the root. + * This ensures we include all modules reachable from the root and isolated modules. * * It can happen that projects have circular dependencies. * Gradle will refuse to build such a project, but we can still render it in a graph - @@ -103,6 +103,16 @@ internal object ProjectParser { if (sourceProjectPath in projectPathsParsed) return projectPathsParsed.add(sourceProjectPath) + // Ensure the source module is present in the graph even if it has no dependencies + // Skip the Gradle root project path ":" to avoid rendering a stray colon node + if (moduleExclusionPattern.matches(sourceProjectPath).not() && sourceProjectPath != ":") { + val sourceModule = Module( + path = sourceProjectPath, + type = projectQuerier.getProjectType(sourceProjectPath, customModuleTypes), + ) + projectGraph.putIfAbsent(sourceModule, emptyList()) + } + projectQuerier.getConfigurations(sourceProjectPath).forEach { config -> val deps = config.getDirectDependencies( configExclusionPattern = configExclusionPattern, diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphBuilder.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphBuilder.kt index 35f240e..e5871ec 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphBuilder.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphBuilder.kt @@ -13,14 +13,31 @@ internal object DigraphBuilder { val config = graphResult.config verifySufficientGraph(graphResult, config.strictMode) - return graphModel.flatMap { (source, targetList) -> + val edgeModels = graphModel.flatMap { (source, targetList) -> when (config.linkText) { LinkText.NONE -> targetList.distinctBy { it.path } else -> targetList - }.mapNotNull { target -> - buildModel(config, source, target) + }.filter { target -> target.path != source.path } + .mapNotNull { target -> + buildModel(config, source, target) + } + } + + val isolatedModels = if (config.includeIsolatedModules) { + val targetsSet = graphModel.values.flatten().map { it.path }.toSet() + val isolatedSources = graphModel.keys.filter { source -> + val hasNoOutgoing = graphModel[source].isNullOrEmpty() + val hasNoIncoming = !targetsSet.contains(source.path) + hasNoOutgoing && hasNoIncoming + } + isolatedSources.mapNotNull { source -> + buildModel(config, source, source) } - }.also { result -> + } else { + emptyList() + } + + return (edgeModels + isolatedModels).also { result -> throwIfNothingMatches(result, config.focusedModulesRegex, config.strictMode) } } @@ -43,7 +60,7 @@ internal object DigraphBuilder { } val isFocusedModulesRegexSet = focusedModulesRegex != null val shouldNotAddToGraph = - sourceFullName == targetFullName || (!sourceMatches && !targetMatches) + (!sourceMatches && !targetMatches) return when { shouldNotAddToGraph -> null diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphCodeBuilder.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphCodeBuilder.kt index d4cad51..fb1223e 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphCodeBuilder.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/graph/DigraphCodeBuilder.kt @@ -27,9 +27,13 @@ internal object DigraphCodeBuilder { """.trimMargin(), ) - private fun toMermaid(it: DigraphModel, linkText: LinkText): CharSequence = """ - | ${it.source.fullPath} ${linkText.toLinkString(it.target.config.value)} ${it.target.fullPath} - """.trimMargin() + private fun toMermaid(it: DigraphModel, linkText: LinkText): CharSequence { + return if (it.source.fullPath == it.target.fullPath) { + """ ${it.source.fullPath}["${it.source.name}"]""" + } else { + """ ${it.source.fullPath} ${linkText.toLinkString(it.target.config.value)} ${it.target.fullPath}""" + } + } } private fun LinkText.toLinkString(configName: String?): String = when (this) { diff --git a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/model/GraphConfig.kt b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/model/GraphConfig.kt index ba0e8e4..f51e689 100644 --- a/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/model/GraphConfig.kt +++ b/plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/model/GraphConfig.kt @@ -46,6 +46,12 @@ data class GraphConfig( */ val showFullPath: Boolean, + /** + * Whether to include modules that have no dependencies and no dependants. + * Disabled by default. + */ + val includeIsolatedModules: Boolean, + /* Content regex pattern parameters */ /** @@ -111,6 +117,9 @@ data class GraphConfig( /** @see [GraphConfig.showFullPath] */ var showFullPath: Boolean? = null + /** @see [GraphConfig.includeIsolatedModules] */ + var includeIsolatedModules: Boolean? = null + /** @see [GraphConfig.excludedConfigurationsRegex] */ var excludedConfigurationsRegex: String? = null @@ -146,6 +155,7 @@ data class GraphConfig( excludedModulesRegex = excludedModulesRegex, rootModulesRegex = rootModulesRegex, showFullPath = showFullPath ?: false, + includeIsolatedModules = includeIsolatedModules ?: false, strictMode = strictMode ?: false, nestingEnabled = nestingEnabled ?: false, ) diff --git a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphPluginTest.kt b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphPluginTest.kt index cbce260..b749492 100644 --- a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphPluginTest.kt +++ b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ModuleGraphPluginTest.kt @@ -43,6 +43,7 @@ class ModuleGraphPluginTest { excludedModulesRegex.set("project") focusedModulesRegex.set(".*test.*") rootModulesRegex.set(".*") + includeIsolatedModules.set(true) } val task = project.tasks.getByName("createModuleGraph") as CreateModuleGraphTask @@ -57,5 +58,6 @@ class ModuleGraphPluginTest { assertEquals("implementation", task.excludedConfigurationsRegex.get()) assertEquals("project", task.excludedModulesRegex.get()) assertEquals(".*", task.rootModulesRegex.get()) + assertEquals(true, task.includeIsolatedModules.get()) } } diff --git a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ProjectParserRootModulesTest.kt b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ProjectParserRootModulesTest.kt index 35160a2..95ea4cf 100644 --- a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ProjectParserRootModulesTest.kt +++ b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/ProjectParserRootModulesTest.kt @@ -25,6 +25,7 @@ internal class ProjectParserRootModulesTest { ModuleToDeps.commonData, ModuleToDeps.coreNetworking, ModuleToDeps.coreUtil, + ModuleToDeps.isolated, ) private val projectQuerier = object : ProjectQuerier { @@ -45,7 +46,16 @@ internal class ProjectParserRootModulesTest { @Test fun `correct graph when root module is app`() { - val expectedGraph = entireGraph + val expectedGraph = mapOf( + ModuleToDeps.app, + ModuleToDeps.featAUi, + ModuleToDeps.commonComponent, + ModuleToDeps.coreUi, + ModuleToDeps.featAData, + ModuleToDeps.commonData, + ModuleToDeps.coreNetworking, + ModuleToDeps.coreUtil, + ) val actualGraph = ProjectParser.parseProjectGraph( allProjectPaths = Project.allPaths, config = getConfig(rootModulesRegex = MockProjectPath.app, theme = theme), @@ -122,6 +132,25 @@ internal class ProjectParserRootModulesTest { Assertions.assertEquals(expectedGraph, actualGraph) } + + @Test + fun `correct graph when include isolated modules is true`() { + val expectedGraph = entireGraph + ModuleToDeps.isolated + val actualGraph = ProjectParser.parseProjectGraph( + allProjectPaths = Project.allPaths, + config = getConfig(rootModulesRegex = null, theme = theme, includeIsolatedModules = true), + projectQuerier = projectQuerier, + ) + + Assertions.assertEquals(expectedGraph, actualGraph) + val isolatedModule = Module( + path = MockProjectPath.isolated, + type = Default.moduleType, + configName = null, + ) + Assertions.assertTrue(actualGraph.containsKey(isolatedModule)) + Assertions.assertTrue(actualGraph[isolatedModule]?.isEmpty() == true) + } } private object Default { @@ -138,6 +167,7 @@ private object MockProjectPath { const val coreUtil = ":core:util" const val commonData = ":common:data" const val coreNetworking = ":core:networking" + const val isolated = ":isolated" } private data class ProjectAndDeps(val path: ProjectPath, val deps: List) @@ -181,6 +211,11 @@ private object Project { ), ) + val isolated = ProjectAndDeps( + MockProjectPath.isolated, + emptyList(), + ) + val all = listOf( coreUtil, coreNetworking, @@ -190,6 +225,7 @@ private object Project { featAUi, featAData, app, + isolated, ) val allPaths = all.map { it.path } @@ -223,6 +259,8 @@ private object ModuleToDeps { createDefaultModuleTarget(MockProjectPath.coreNetworking), ) + val isolated = createDefaultModuleSource(MockProjectPath.isolated) to emptyList() + private fun createDefaultModuleSource(path: String) = Module( path = path, type = Default.moduleType, diff --git a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/DigraphCodeBuilderTest.kt b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/DigraphCodeBuilderTest.kt index 5fb16b2..2d667e2 100644 --- a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/DigraphCodeBuilderTest.kt +++ b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/DigraphCodeBuilderTest.kt @@ -78,4 +78,24 @@ class DigraphCodeBuilderTest { """.trimMargin() assertEquals(expectedMermaidCode, mermaidCode.value) } + + @Test + fun `Build digraph with include isolated modules returns correct graph`() { + val graphModel = graphWithIsolatedModules() + val config = getConfig(includeIsolatedModules = true) + val result = GraphParseResult(graphModel, config) + + val mermaidCode = DigraphCodeBuilder.build( + digraphModel = DigraphBuilder.build(result), + linkText = config.linkText, + ) + + val expected = """ + | :app --> :utilities + | :group:child1 --> :group:child2 + | :isolated["isolated"] + | :group:lonely["lonely"] + """.trimMargin() + assertEquals(expected, mermaidCode.value) + } } diff --git a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/Fixtures.kt b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/Fixtures.kt index b05920d..bf470f0 100644 --- a/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/Fixtures.kt +++ b/plugin-build/modulegraph/src/test/java/dev/iurysouza/modulegraph/graph/Fixtures.kt @@ -46,6 +46,17 @@ internal fun aModuleGraph() = mapOf( ), ) +internal fun graphWithIsolatedModules() = linkedMapOf( + Module(path = ":app") to listOf( + Module(path = ":utilities", configName = "implementation"), + ), + Module(path = ":group:child1") to listOf( + Module(path = ":group:child2", configName = "implementation"), + ), + Module(path = ":isolated") to emptyList(), + Module(path = ":group:lonely") to emptyList(), +) + internal val expectedMermaidGraphCode = """ |```mermaid |%%{ @@ -84,6 +95,7 @@ internal fun getConfig( linkText: LinkText? = null, setStyleByModuleType: Boolean? = null, showFullPath: Boolean? = null, + includeIsolatedModules: Boolean? = null, strictMode: Boolean? = null, nestingEnabled: Boolean? = null, ) = @@ -100,6 +112,7 @@ internal fun getConfig( this.linkText = linkText this.setStyleByModuleType = setStyleByModuleType this.showFullPath = showFullPath + this.includeIsolatedModules = includeIsolatedModules this.strictMode = strictMode this.nestingEnabled = nestingEnabled }.build()