Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Binary file modified gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ abstract class CreateModuleGraphTask : DefaultTask() {
@get:Optional
abstract val nestingEnabled: Property<Boolean>

@get:Input
@get:Option(
option = "includeIsolatedModules",
description = "Whether to render modules with no dependencies or dependants",
)
@get:Optional
abstract val includeIsolatedModules: Property<Boolean>

@get:Input
@get:Option(option = "graphModels", description = "The produced graph models")
internal abstract val graphModels: ListProperty<GraphParseResult>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ open class ModuleGraphExtension @Inject constructor(project: Project) {
*/
val nestingEnabled: Property<Boolean> = objects.property(Boolean::class.java)

/**
* Whether to include modules that have no dependencies and no dependants.
* Disabled by default.
*/
val includeIsolatedModules: Property<Boolean> = objects.property(Boolean::class.java)

/**
* A list of additional graph configs to generate graphs for.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ open class ModuleGraphPlugin : Plugin<Project> {
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())
Expand Down Expand Up @@ -89,6 +90,7 @@ open class ModuleGraphPlugin : Plugin<Project> {
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

Expand All @@ -104,6 +106,7 @@ open class ModuleGraphPlugin : Plugin<Project> {
excludedModulesRegex,
rootModulesRegex,
showFullPath,
includeIsolatedModules,
strictMode,
nestingEnabled,
)
Expand All @@ -126,6 +129,7 @@ open class ModuleGraphPlugin : Plugin<Project> {
this.linkText = linkText
this.setStyleByModuleType = setStyleByModuleType
this.showFullPath = showFullPath
this.includeIsolatedModules = includeIsolatedModules
this.excludedConfigurationsRegex = excludedConfigurationsRegex
this.excludedModulesRegex = excludedModulesRegex
this.rootModulesRegex = rootModulesRegex
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand All @@ -43,7 +60,7 @@ internal object DigraphBuilder {
}
val isFocusedModulesRegexSet = focusedModulesRegex != null
val shouldNotAddToGraph =
sourceFullName == targetFullName || (!sourceMatches && !targetMatches)
(!sourceMatches && !targetMatches)

return when {
shouldNotAddToGraph -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

/**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -146,6 +155,7 @@ data class GraphConfig(
excludedModulesRegex = excludedModulesRegex,
rootModulesRegex = rootModulesRegex,
showFullPath = showFullPath ?: false,
includeIsolatedModules = includeIsolatedModules ?: false,
strictMode = strictMode ?: false,
nestingEnabled = nestingEnabled ?: false,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal class ProjectParserRootModulesTest {
ModuleToDeps.commonData,
ModuleToDeps.coreNetworking,
ModuleToDeps.coreUtil,
ModuleToDeps.isolated,
)

private val projectQuerier = object : ProjectQuerier {
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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<ProjectPath>)
Expand Down Expand Up @@ -181,6 +211,11 @@ private object Project {
),
)

val isolated = ProjectAndDeps(
MockProjectPath.isolated,
emptyList(),
)

val all = listOf(
coreUtil,
coreNetworking,
Expand All @@ -190,6 +225,7 @@ private object Project {
featAUi,
featAData,
app,
isolated,
)

val allPaths = all.map { it.path }
Expand Down Expand Up @@ -223,6 +259,8 @@ private object ModuleToDeps {
createDefaultModuleTarget(MockProjectPath.coreNetworking),
)

val isolated = createDefaultModuleSource(MockProjectPath.isolated) to emptyList<Module>()

private fun createDefaultModuleSource(path: String) = Module(
path = path,
type = Default.moduleType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
|%%{
Expand Down Expand Up @@ -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,
) =
Expand All @@ -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()
Expand Down
Loading