Skip to content

Commit 0a50aa1

Browse files
RBusarowkodiakhq[bot]
authored andcommitted
fix auto-correct when using a non-standard config name
fixes #360
1 parent d408e8f commit 0a50aa1

File tree

31 files changed

+1214
-262
lines changed

31 files changed

+1214
-262
lines changed

modulecheck-api/src/main/kotlin/modulecheck/api/context/Declarations.kt

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import modulecheck.utils.LazySet.DataSource.Priority.HIGH
2929
import modulecheck.utils.SafeCache
3030
import modulecheck.utils.dataSource
3131
import modulecheck.utils.lazySet
32-
import modulecheck.utils.mapToSet
3332

3433
data class Declarations(
3534
private val delegate: SafeCache<SourceSetName, LazySet<DeclarationName>>,
@@ -42,13 +41,10 @@ data class Declarations(
4241
suspend fun get(sourceSetName: SourceSetName): LazySet<DeclarationName> {
4342
return delegate.getOrPut(sourceSetName) {
4443

45-
val inheritedSourceSetsNames = sourceSetName.javaConfigurationNames()
46-
.flatMapTo(mutableSetOf(sourceSetName)) { configName ->
47-
project.configurations[configName]
48-
?.inherited
49-
?.mapToSet { inherited -> inherited.name.toSourceSetName() }
50-
.orEmpty()
51-
}
44+
val inheritedSourceSetsNames = sourceSetName.inheritedSourceSetNames(
45+
project,
46+
includeSelf = true
47+
)
5248

5349
val rNameOrNull = (project as? AndroidMcProject)?.androidRFqNameOrNull
5450

modulecheck-core/src/main/kotlin/modulecheck/core/rule/InheritedDependencyRule.kt

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ import modulecheck.api.settings.ChecksSettings
2222
import modulecheck.core.InheritedDependencyFinding
2323
import modulecheck.core.context.mustBeApiIn
2424
import modulecheck.core.internal.uses
25+
import modulecheck.parsing.gradle.ConfigurationName
2526
import modulecheck.parsing.gradle.SourceSetName
27+
import modulecheck.parsing.gradle.inheritingConfigurations
28+
import modulecheck.project.ConfiguredProjectDependency
2629
import modulecheck.project.McProject
30+
import modulecheck.project.TransitiveProjectDependency
2731
import modulecheck.utils.mapAsync
2832

2933
class InheritedDependencyRule : ModuleCheckRule<InheritedDependencyFinding> {
@@ -34,47 +38,91 @@ class InheritedDependencyRule : ModuleCheckRule<InheritedDependencyFinding> {
3438

3539
override suspend fun check(project: McProject): List<InheritedDependencyFinding> {
3640

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+
}
4093

4194
val used = project.classpathDependencies().all()
42-
.filterNot { mainDirectDependencies.contains(it.contributed.project to it.contributed.isTestFixture) }
4395
.distinctBy { it.contributed.project.path to it.contributed.isTestFixture }
44-
.filter { project.uses(it) }
96+
.flatMap { transitive ->
4597

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()
52108
}
53-
}
54109

55110
return used.asSequence()
56-
.filterNot {
57-
pathsForSourceSet(it.source.configurationName.toSourceSetName())
58-
.contains((it.contributed.project.path to it.contributed.isTestFixture))
59-
}
60111
.distinct()
61112
// Projects shouldn't inherit themselves. This false-positive can happen if a test
62113
// fixture/utilities module depends upon a module, and that module uses the test module in
63114
// testImplementation.
64115
.filterNot { transitive -> transitive.contributed.project == project }
65-
.mapAsync { transitive ->
66-
67-
val source = transitive.source
68-
val inherited = transitive.contributed
116+
.mapAsync { (source, inherited) ->
69117

70118
val mustBeApi = source.configurationName
71119
.toSourceSetName() == SourceSetName.MAIN && inherited.project
72120
.mustBeApiIn(project, inherited.isTestFixture)
73121

74122
val newConfig = if (mustBeApi) {
75-
source.configurationName.apiVariant()
123+
inherited.configurationName.apiVariant()
76124
} else {
77-
source.configurationName
125+
inherited.configurationName.implementationVariant()
78126
}
79127

80128
InheritedDependencyFinding(
@@ -93,6 +141,46 @@ class InheritedDependencyRule : ModuleCheckRule<InheritedDependencyFinding> {
93141
.flatten()
94142
}
95143

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+
96184
override fun shouldApply(checksSettings: ChecksSettings): Boolean {
97185
return checksSettings.inheritedDependency
98186
}

0 commit comments

Comments
 (0)