@@ -22,8 +22,12 @@ import modulecheck.api.settings.ChecksSettings
22
22
import modulecheck.core.InheritedDependencyFinding
23
23
import modulecheck.core.context.mustBeApiIn
24
24
import modulecheck.core.internal.uses
25
+ import modulecheck.parsing.gradle.ConfigurationName
25
26
import modulecheck.parsing.gradle.SourceSetName
27
+ import modulecheck.parsing.gradle.inheritingConfigurations
28
+ import modulecheck.project.ConfiguredProjectDependency
26
29
import modulecheck.project.McProject
30
+ import modulecheck.project.TransitiveProjectDependency
27
31
import modulecheck.utils.mapAsync
28
32
29
33
class InheritedDependencyRule : ModuleCheckRule <InheritedDependencyFinding > {
@@ -34,47 +38,91 @@ class InheritedDependencyRule : ModuleCheckRule<InheritedDependencyFinding> {
34
38
35
39
override suspend fun check (project : McProject ): List <InheritedDependencyFinding > {
36
40
37
- val mainDirectDependencies = project.projectDependencies.main()
38
- .map { it.project to it.isTestFixture }
39
- .toSet()
41
+ // For each source set, the set of all module paths and whether they're test fixtures
42
+ val dependencyPathCache = mutableMapOf<SourceSetName , Set <Pair <String , Boolean >>>()
43
+
44
+ // Returns true if the dependency is already declared in this exact configuration, **or** if
45
+ // it's declared in an upstream configuration.
46
+ //
47
+ // For example, this function will return true for a `testImplementation` configured dependency
48
+ // which is already declared in the main source set (such as with `api` or `implementation`).
49
+ fun ConfiguredProjectDependency.alreadyInClasspath (): Boolean {
50
+ fun dependencyPathsForSourceSet (sourceSetName : SourceSetName ): Set <Pair <String , Boolean >> {
51
+ return dependencyPathCache.getOrPut(sourceSetName) {
52
+ project.projectDependencies[sourceSetName]
53
+ .map { it.project.path to it.isTestFixture }
54
+ .toSet()
55
+ }
56
+ }
57
+
58
+ return configurationName.toSourceSetName()
59
+ // Check the receiver's configuration first, but if the dependency isn't used there, also
60
+ // check the upstream configurations.
61
+ .inheritedSourceSetNames(project, includeSelf = true )
62
+ .any { sourceSet ->
63
+ dependencyPathsForSourceSet(sourceSet)
64
+ .contains(this .project.path to isTestFixture)
65
+ }
66
+ }
67
+
68
+ // Returns the list of all transitive dependencies where the contributed dependency is used,
69
+ // filtering out any configuration which would be redundant. For instance, if a dependency is
70
+ // used in `main`, the function will stop there instead of returning a list of `main`, `debug`,
71
+ // `test`, etc.
72
+ suspend fun List<TransitiveProjectDependency>.allUsed (): List <TransitiveProjectDependency > {
73
+ return fold(listOf ()) { acc, transitiveProjectDependency ->
74
+
75
+ val contributedSourceSet = transitiveProjectDependency.contributed
76
+ .configurationName
77
+ .toSourceSetName()
78
+
79
+ val alreadyUsedUpstream = acc.any {
80
+ val usedSourceSet = it.contributed.configurationName.toSourceSetName()
81
+ contributedSourceSet.inheritsFrom(usedSourceSet, project)
82
+ }
83
+
84
+ when {
85
+ alreadyUsedUpstream -> acc
86
+ project.uses(transitiveProjectDependency.contributed) -> {
87
+ acc + transitiveProjectDependency
88
+ }
89
+ else -> acc
90
+ }
91
+ }
92
+ }
40
93
41
94
val used = project.classpathDependencies().all()
42
- .filterNot { mainDirectDependencies.contains(it.contributed.project to it.contributed.isTestFixture) }
43
95
.distinctBy { it.contributed.project.path to it.contributed.isTestFixture }
44
- .filter { project.uses(it) }
96
+ .flatMap { transitive ->
45
97
46
- val dependencyPathCache = mutableMapOf<SourceSetName , Set <Pair <String , Boolean >>>()
47
- fun pathsForSourceSet (sourceSetName : SourceSetName ): Set <Pair <String , Boolean >> {
48
- return dependencyPathCache.getOrPut(sourceSetName) {
49
- project.projectDependencies[sourceSetName]
50
- .map { it.project.path to it.isTestFixture }
51
- .toSet()
98
+ // If a transitive dependency is used in the same configuration as its source, then that's
99
+ // the configuration which should be used and we're done. However, that transitive
100
+ // dependency is also providing the dependency to other source sets which depend upon it.
101
+ // So, check the inheriting dependencies as well.
102
+ transitive.withContributedConfiguration(transitive.source.configurationName)
103
+ .withInheritingVariants(project)
104
+ .filterNot { it.contributed.alreadyInClasspath() }
105
+ .toList()
106
+ .sortedByInheritance(project)
107
+ .allUsed()
52
108
}
53
- }
54
109
55
110
return used.asSequence()
56
- .filterNot {
57
- pathsForSourceSet(it.source.configurationName.toSourceSetName())
58
- .contains((it.contributed.project.path to it.contributed.isTestFixture))
59
- }
60
111
.distinct()
61
112
// Projects shouldn't inherit themselves. This false-positive can happen if a test
62
113
// fixture/utilities module depends upon a module, and that module uses the test module in
63
114
// testImplementation.
64
115
.filterNot { transitive -> transitive.contributed.project == project }
65
- .mapAsync { transitive ->
66
-
67
- val source = transitive.source
68
- val inherited = transitive.contributed
116
+ .mapAsync { (source, inherited) ->
69
117
70
118
val mustBeApi = source.configurationName
71
119
.toSourceSetName() == SourceSetName .MAIN && inherited.project
72
120
.mustBeApiIn(project, inherited.isTestFixture)
73
121
74
122
val newConfig = if (mustBeApi) {
75
- source .configurationName.apiVariant()
123
+ inherited .configurationName.apiVariant()
76
124
} else {
77
- source .configurationName
125
+ inherited .configurationName.implementationVariant()
78
126
}
79
127
80
128
InheritedDependencyFinding (
@@ -93,6 +141,46 @@ class InheritedDependencyRule : ModuleCheckRule<InheritedDependencyFinding> {
93
141
.flatten()
94
142
}
95
143
144
+ private fun TransitiveProjectDependency.withContributedConfiguration (
145
+ configurationName : ConfigurationName
146
+ ): TransitiveProjectDependency {
147
+ val newContributed = contributed.copy(configurationName = configurationName)
148
+ return copy(contributed = newContributed)
149
+ }
150
+
151
+ // Returns a sequence starting with the receiver's configuration, then all **downstream**
152
+ // configurations. This is useful because when we're checking to see if a transitive dependency
153
+ // is used in `main` (for instance), we should also check whether it's used in the source sets
154
+ // which inherit from `main` (like `debug`, `release`, `androidTest`, `test`, etc.).
155
+ private fun TransitiveProjectDependency.withInheritingVariants (
156
+ project : McProject
157
+ ): Sequence <TransitiveProjectDependency > {
158
+ return sequenceOf(this ) + project.inheritingConfigurations(source.configurationName)
159
+ .asSequence()
160
+ .map { config -> config.name }
161
+ .filter { name -> name.isImplementation() }
162
+ .map { configName -> withContributedConfiguration(configName) }
163
+ }
164
+
165
+ private fun List<TransitiveProjectDependency>.sortedByInheritance (
166
+ project : McProject
167
+ ): List <TransitiveProjectDependency > {
168
+ val sorted = toMutableList()
169
+
170
+ sorted.sortWith { o1, o2 ->
171
+ val o1SourceSet = o1.contributed.configurationName.toSourceSetName()
172
+ val o2SourceSet = o2.contributed.configurationName.toSourceSetName()
173
+
174
+ val inherits = o1SourceSet.inheritsFrom(o2SourceSet, project)
175
+ when {
176
+ inherits -> - 1
177
+ o1SourceSet == o2SourceSet -> 0
178
+ else -> 1
179
+ }
180
+ }
181
+ return sorted
182
+ }
183
+
96
184
override fun shouldApply (checksSettings : ChecksSettings ): Boolean {
97
185
return checksSettings.inheritedDependency
98
186
}
0 commit comments