Skip to content

Commit 07bfdf1

Browse files
LukeNeedhamLukeiurysza
authored
Add support for root modules in config (#44)
* Add support for root modules in config * Improve docs * Fix linter issues * Fix tests - the test had a circular dependency that gradle didn't like. Fix our ProjectParser to handle circular dependencies. Remove the simple parsing with no root modules - it now goes through the same recursive parsing, which is now optimised to avoid doing duplicate work * Revert test change - the circular dependency worked previously, error is elsewhere * rework GradleProject abstraction to ProjectQuerier - the old setup could not handle circular dependencies, since it tried to create all wrapper instances at once. * Add sample docs * Add circular dependency to ProjectParserRootModulesTest for testing * Fix lint issues, code touch ups --------- Co-authored-by: Luke <luke.needham@persgroep.net> Co-authored-by: Iury Souza <iurysza@gmail.com>
1 parent df6b67f commit 07bfdf1

19 files changed

Lines changed: 525 additions & 80 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@
88
/.idea/workspace.xml
99
/.idea/navEditor.xml
1010
/.idea/assetWizardSettings.xml
11+
/.idea/compiler.xml
12+
/.idea/gradle.xml
13+
/.idea/kotlinc.xml
14+
/.idea/misc.xml
15+
/.idea/vcs.xml
16+
/.idea/.name
1117
build

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ moduleGraphConfig {
8181
// excludedConfigurationsRegex = ".*test.*" // optional
8282
// excludedModulesRegex = ".*moduleName.*" // optional
8383
// focusedModulesRegex = ".*(projectName).*" // optional
84+
// rootModulesRegex = ".*moduleName.*" // optional
8485
// setStyleByPluginType = true // optional
8586
// theme = Theme.NEUTRAL // optional
8687
// Or you can fully customize it by using the BASE theme:
@@ -170,6 +171,7 @@ moduleGraphConfig {
170171
// excludedConfigurationsRegex.set(".*test.*") // optional
171172
// excludedModulesRegex.set(".*moduleName.*") // optional
172173
// focusedModulesRegex.set(".*(projectName).*") // optional
174+
// rootModulesRegex.set(".*moduleName.*") // optional
173175
// theme.set(Theme.NEUTRAL) // optional
174176
// or you can fully customize it by using the BASE theme:
175177
// Theme.BASE(
@@ -236,6 +238,10 @@ Optional settings:
236238
- Regex matching the configurations which should be ignored. e.g. "implementation", "testImplementation".
237239
- **excludedModulesRegex**:
238240
- Regex matching the modules which should be ignored.
241+
- **rootModules**:
242+
- Regex matching the modules that should be used as root modules.
243+
If this value is supplied, the generated graph will only include dependencies (direct and transitive) of root modules.
244+
In other words, the graph will only include modules that can be reached from a root module.
239245

240246
### Show me that graph!
241247

plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/CreateModuleGraphTask.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ abstract class CreateModuleGraphTask : DefaultTask() {
6060
@get:Optional
6161
abstract val excludedModulesRegex: Property<String>
6262

63+
@get:Input
64+
@get:Option(option = "rootModulesRegex", description = "A Regex to match root modules")
65+
@get:Optional
66+
abstract val rootModulesRegex: Property<String>
67+
6368
@get:Input
6469
@get:Option(option = "setStyleByModuleType", description = "Whether to customize the node by the plugin type")
6570
@get:Optional

plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ExclusionStrategy.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.

plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphExtension.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@ open class ModuleGraphExtension @Inject constructor(project: Project) {
6767
*/
6868
val excludedModulesRegex: Property<String> = objects.property(String::class.java)
6969

70+
/**
71+
* A Regex to match modules that should be used as root modules.
72+
* If this value is supplied,
73+
* the generated graph will only include dependencies (direct and transitive) of root modules.
74+
*/
75+
val rootModulesRegex: Property<String> = objects.property(String::class.java)
76+
7077
/**
7178
* Whether to show the full path of the module in the graph.
7279
* Use this if you have modules with the same name in different folders.

plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ModuleGraphPlugin.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package dev.iurysouza.modulegraph.gradle
22

33
import dev.iurysouza.modulegraph.Theme
4+
import dev.iurysouza.modulegraph.gradle.graphparser.ProjectParser
5+
import dev.iurysouza.modulegraph.gradle.graphparser.projectquerier.GradleProjectQuerier
46
import org.gradle.api.Plugin
57
import org.gradle.api.Project
68

@@ -29,15 +31,22 @@ open class ModuleGraphPlugin : Plugin<Project> {
2931
task.excludedConfigurationsRegex.set(extension.excludedConfigurationsRegex)
3032
task.excludedModulesRegex.set(extension.excludedModulesRegex)
3133
task.setStyleByModuleType.set(extension.setStyleByModuleType)
34+
task.rootModulesRegex.set(extension.rootModulesRegex)
3235
task.outputFile.set(project.layout.projectDirectory.file(extension.readmePath))
3336

34-
task.graphModel.set(
35-
project.parseProjectStructure(
36-
excludedConfigurations = extension.excludedConfigurationsRegex.orNull,
37-
excludedModules = extension.excludedModulesRegex.orNull,
38-
theme = extension.theme.getOrElse(Theme.NEUTRAL),
39-
),
37+
val allProjects = project.allprojects
38+
val allProjectPaths = allProjects.map { it.path }
39+
val projectQuerier = GradleProjectQuerier(allProjects)
40+
41+
val projectGraph = ProjectParser.parseProjectGraph(
42+
allProjectPaths = allProjectPaths,
43+
rootModulesRegex = extension.rootModulesRegex.orNull,
44+
excludedConfigurations = extension.excludedConfigurationsRegex.orNull,
45+
excludedModules = extension.excludedModulesRegex.orNull,
46+
theme = extension.theme.getOrElse(Theme.NEUTRAL),
47+
projectQuerier = projectQuerier,
4048
)
49+
task.graphModel.set(projectGraph)
4150
}
4251
}
4352
}

plugin-build/modulegraph/src/main/kotlin/dev/iurysouza/modulegraph/gradle/ProjectParser.kt

Lines changed: 0 additions & 50 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package dev.iurysouza.modulegraph.gradle
2+
3+
internal fun RegexMatcher?.matches(
4+
name: String,
5+
) = this?.pattern
6+
?.toRegex()
7+
?.matches(name) ?: false
8+
9+
internal data class RegexMatcher(val pattern: String)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package dev.iurysouza.modulegraph.gradle.graphparser
2+
3+
import dev.iurysouza.modulegraph.ModuleType
4+
import dev.iurysouza.modulegraph.Theme
5+
import dev.iurysouza.modulegraph.gradle.Module
6+
import dev.iurysouza.modulegraph.gradle.RegexMatcher
7+
import dev.iurysouza.modulegraph.gradle.graphparser.model.GradleProjectConfiguration
8+
import dev.iurysouza.modulegraph.gradle.graphparser.model.ProjectPath
9+
import dev.iurysouza.modulegraph.gradle.graphparser.projectquerier.ProjectQuerier
10+
import dev.iurysouza.modulegraph.gradle.matches
11+
12+
internal object ProjectParser {
13+
internal fun parseProjectGraph(
14+
allProjectPaths: List<ProjectPath>,
15+
rootModulesRegex: String?,
16+
excludedConfigurations: String?,
17+
excludedModules: String?,
18+
theme: Theme,
19+
projectQuerier: ProjectQuerier,
20+
): ProjectGraph {
21+
val configExclusionPattern = excludedConfigurations?.let { RegexMatcher(it) }
22+
val moduleExclusionPattern = excludedModules?.let { RegexMatcher(it) }
23+
val rootModuleInclusionPattern = rootModulesRegex?.let { RegexMatcher(it) }
24+
25+
val customModuleTypes = when (theme) {
26+
is Theme.BASE -> theme.moduleTypes
27+
else -> emptyList()
28+
}
29+
30+
val rootModules = if (rootModuleInclusionPattern == null) {
31+
allProjectPaths
32+
} else {
33+
allProjectPaths.filter { rootModuleInclusionPattern.matches(it) }
34+
}
35+
require(rootModules.isNotEmpty()) {
36+
"The graph cannot be generated as no rootModules were found"
37+
}
38+
return parseFromRoots(
39+
rootModulePaths = rootModules,
40+
customModuleTypes = customModuleTypes,
41+
configExclusionPattern = configExclusionPattern,
42+
moduleExclusionPattern = moduleExclusionPattern,
43+
projectQuerier = projectQuerier,
44+
)
45+
}
46+
47+
/** @return the modules which are direct dependencies */
48+
private fun GradleProjectConfiguration.getDirectDependencies(
49+
configExclusionPattern: RegexMatcher?,
50+
moduleExclusionPattern: RegexMatcher?,
51+
): List<String> {
52+
return if (configExclusionPattern.matches(name)) {
53+
emptyList()
54+
} else {
55+
projectPaths.filterNot { moduleExclusionPattern.matches(it) }
56+
}
57+
}
58+
59+
/**
60+
* To handle root modules we need to parse the dependency tree recursively,
61+
* starting at the root module nodes.
62+
* This ensures we only include module nodes that are reachable from the root.
63+
*
64+
* It can happen that projects have circular dependencies.
65+
* Gradle will refuse to build such a project, but we can still render it in a graph -
66+
* in fact, such a graph can be useful to catch circular dependencies.
67+
* We need to handle these cases to avoid infinite recursion.
68+
*/
69+
private fun parseFromRoots(
70+
rootModulePaths: List<ProjectPath>,
71+
customModuleTypes: List<ModuleType>,
72+
configExclusionPattern: RegexMatcher?,
73+
moduleExclusionPattern: RegexMatcher?,
74+
projectQuerier: ProjectQuerier,
75+
): ProjectGraph {
76+
val projectGraph = hashMapOf<Module, List<Module>>()
77+
val projectPathsParsed = hashSetOf<ProjectPath>()
78+
79+
fun parseModuleDeps(sourceProjectPath: ProjectPath) {
80+
// Don't parse projects more than once -
81+
// it's a waste of time, and it might lead to infinite recursion
82+
if (sourceProjectPath in projectPathsParsed) return
83+
projectPathsParsed.add(sourceProjectPath)
84+
85+
projectQuerier.getConfigurations(sourceProjectPath).forEach { config ->
86+
config.getDirectDependencies(
87+
configExclusionPattern = configExclusionPattern,
88+
moduleExclusionPattern = moduleExclusionPattern,
89+
).forEach { targetProject ->
90+
registerDependency(
91+
sourceProjectPath = sourceProjectPath,
92+
targetProjectPath = targetProject,
93+
projectGraph = projectGraph,
94+
configName = config.name,
95+
customModuleTypes = customModuleTypes,
96+
projectQuerier = projectQuerier,
97+
)
98+
parseModuleDeps(targetProject)
99+
}
100+
}
101+
}
102+
103+
rootModulePaths.forEach { rootModule ->
104+
parseModuleDeps(rootModule)
105+
}
106+
return projectGraph
107+
}
108+
109+
/** Registers the dependency from [sourceProjectPath] to [targetProjectPath] in [projectGraph] */
110+
private fun registerDependency(
111+
sourceProjectPath: ProjectPath,
112+
targetProjectPath: ProjectPath,
113+
projectGraph: MutableMap<Module, List<Module>>,
114+
configName: String,
115+
customModuleTypes: List<ModuleType>,
116+
projectQuerier: ProjectQuerier,
117+
) {
118+
val sourceModule = Module(
119+
path = sourceProjectPath,
120+
type = projectQuerier.getProjectType(sourceProjectPath, customModuleTypes),
121+
)
122+
projectGraph[sourceModule] = projectGraph.getOrDefault(sourceModule, emptyList())
123+
.plus(
124+
Module(
125+
path = targetProjectPath,
126+
configName = configName,
127+
type = projectQuerier.getProjectType(targetProjectPath, customModuleTypes),
128+
),
129+
)
130+
}
131+
}
132+
133+
private typealias ProjectGraph = Map<Module, List<Module>>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dev.iurysouza.modulegraph.gradle.graphparser.model
2+
3+
internal data class GradleProjectConfiguration(
4+
val name: String,
5+
/** The paths of all the projects which are dependencies in this configuration */
6+
val projectPaths: List<ProjectPath>,
7+
)

0 commit comments

Comments
 (0)