Skip to content

Commit 533ce5b

Browse files
RBusarowkodiakhq[bot]
authored andcommitted
support overshot dependencies
fixes #113
1 parent a621a29 commit 533ce5b

19 files changed

Lines changed: 1349 additions & 775 deletions

File tree

modulecheck-api/src/main/kotlin/modulecheck/api/Config.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
package modulecheck.api
1717

18-
data class ConfigurationName(val value: String) {
18+
data class ConfigurationName(val value: String) : Comparable<ConfigurationName> {
1919
fun toSourceSetName(): SourceSetName = when (this.value) {
2020
// "main" source set configurations omit the "main" from their name,
2121
// creating "implementation" instead of "mainImplementation"
@@ -25,6 +25,19 @@ data class ConfigurationName(val value: String) {
2525
else -> this.value.extractSourceSetName()
2626
}
2727

28+
/**
29+
* Returns the base name of the Configuration without any source set prefix.
30+
*
31+
* For "main" source sets, this function just returns the same string, e.g.:
32+
* ConfigurationName("api").nameWithoutSourceSet() == "api"
33+
* ConfigurationName("implementation").nameWithoutSourceSet() == "implementation"
34+
*
35+
* For other source sets, it returns the base configuration names:
36+
* ConfigurationName("debugApi").nameWithoutSourceSet() == "Api"
37+
* ConfigurationName("testImplementation").nameWithoutSourceSet() == "Implementation"
38+
*/
39+
fun nameWithoutSourceSet() = value.removePrefix(toSourceSetName().value)
40+
2841
/**
2942
* find the "base" configuration name and remove it
3043
*
@@ -70,6 +83,10 @@ data class ConfigurationName(val value: String) {
7083
.toSourceSetName()
7184
}
7285

86+
override fun compareTo(other: ConfigurationName): Int {
87+
return value.compareTo(other.value)
88+
}
89+
7390
companion object {
7491

7592
val compileOnlyApi = ConfigurationName("compileOnlyApi")

modulecheck-api/src/main/kotlin/modulecheck/api/settings/ModuleCheckSettings.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ interface SortSettings {
9393
interface ChecksSettings {
9494
var redundantDependency: Boolean
9595
var unusedDependency: Boolean
96+
var overShotDependency: Boolean
9697
var mustBeApi: Boolean
9798
var inheritedDependency: Boolean
9899
var sortDependencies: Boolean
@@ -106,6 +107,7 @@ interface ChecksSettings {
106107

107108
const val REDUNDANT_DEPENDENCY_DEFAULT = false
108109
const val UNUSED_DEPENDENCY_DEFAULT = true
110+
const val OVERSHOT_DEPENDENCY_DEFAULT = true
109111
const val MUST_BE_API_DEFAULT = true
110112
const val INHERITED_DEPENDENCY_DEFAULT = true
111113
const val SORT_DEPENDENCIES_DEFAULT = false
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright (C) 2021 Rick Busarow
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package modulecheck.core
17+
18+
import modulecheck.api.ConfigurationName
19+
import modulecheck.api.Project2
20+
import modulecheck.api.context.ProjectContext
21+
import modulecheck.core.context.OverShotDependencies
22+
import modulecheck.parsing.*
23+
import modulecheck.parsing.ModuleRef.StringRef
24+
import modulecheck.parsing.ModuleRef.TypeSafeRef
25+
import java.io.File
26+
27+
data class OverShotDependencyFinding(
28+
override val dependentPath: String,
29+
override val buildFile: File,
30+
override val dependencyProject: Project2,
31+
override val dependencyIdentifier: String,
32+
override val configurationName: ConfigurationName
33+
) : DependencyFinding("demoted") {
34+
35+
override fun fix(): Boolean {
36+
37+
val blocks = DependencyBlockParser
38+
.parse(buildFile)
39+
40+
val blockMatchPairOrNull = blocks.firstNotNullOfOrNull { block ->
41+
42+
val match = matchingDeclaration(block) ?: return@firstNotNullOfOrNull null
43+
44+
block to match
45+
}
46+
47+
if (blockMatchPairOrNull != null) {
48+
49+
val (block, match) = blockMatchPairOrNull
50+
51+
val moduleDeclaration = newModuleDeclaration(match)
52+
53+
val newDeclaration = newDeclarationText(match, moduleDeclaration)
54+
55+
val oldDeclarationLine = match.statementWithSurroundingText
56+
.lines()
57+
.first { it.contains(match.declarationText.lines().first()) }
58+
59+
val indent = "(\\s*)".toRegex()
60+
.find(oldDeclarationLine)
61+
?.destructured
62+
?.component1()
63+
?: " "
64+
65+
val newBlock = block.contentString.replace(
66+
match.declarationText,
67+
match.declarationText + "\n$indent" + newDeclaration
68+
)
69+
70+
val fileText = buildFile.readText()
71+
.replace(block.contentString, newBlock)
72+
73+
buildFile.writeText(fileText)
74+
return true
75+
}
76+
77+
return false
78+
}
79+
80+
private fun matchingDeclaration(block: DependenciesBlock) =
81+
(
82+
block.allDeclarations
83+
.filterIsInstance<ModuleDependencyDeclaration>()
84+
.maxByOrNull { declaration -> declaration.configName == configurationName.value }
85+
?: block.allDeclarations
86+
.filterNot { it is ModuleDependencyDeclaration }
87+
.maxByOrNull { declaration -> declaration.configName == configurationName.value }
88+
?: block.allDeclarations
89+
.lastOrNull()
90+
)
91+
92+
private fun newModuleDeclaration(match: DependencyDeclaration) = when (match) {
93+
is ExternalDependencyDeclaration -> dependencyProject.path
94+
is ModuleDependencyDeclaration -> when (match.moduleRef) {
95+
is StringRef -> dependencyProject.path
96+
is TypeSafeRef -> StringRef(dependencyProject.path).toTypeSafe().value
97+
}
98+
is UnknownDependencyDeclaration -> dependencyProject.path
99+
}
100+
101+
private fun newDeclarationText(match: DependencyDeclaration, moduleDeclaration: String): String {
102+
return match.declarationText
103+
.replace(match.configName, configurationName.value)
104+
.let {
105+
when (match) {
106+
is ExternalDependencyDeclaration -> it.replace("""(["']).*(["'])""".toRegex()) { mr ->
107+
val quotes = mr.destructured.component1()
108+
109+
"project($quotes$moduleDeclaration$quotes)"
110+
}
111+
is ModuleDependencyDeclaration -> it.replace(match.moduleRef.value, moduleDeclaration)
112+
is UnknownDependencyDeclaration -> it.replace(
113+
match.argument,
114+
"project(\"$moduleDeclaration\")"
115+
)
116+
}
117+
}
118+
}
119+
120+
override fun toString(): String {
121+
return "OverShotDependency(\n" +
122+
"\tdependentPath='$dependentPath', \n" +
123+
"\tbuildFile=$buildFile, \n" +
124+
"\tdependencyProject=$dependencyProject, \n" +
125+
"\tdependencyIdentifier='$dependencyIdentifier', \n" +
126+
"\tconfigurationName=$configurationName\n" +
127+
")"
128+
}
129+
}
130+
131+
val ProjectContext.overshotDependencies: OverShotDependencies get() = get(OverShotDependencies)
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright (C) 2021 Rick Busarow
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package modulecheck.core.context
17+
18+
import modulecheck.api.ConfigurationName
19+
import modulecheck.api.ConfiguredProjectDependency
20+
import modulecheck.api.Project2
21+
import modulecheck.api.context.ProjectContext
22+
import modulecheck.core.OverShotDependencyFinding
23+
import modulecheck.core.internal.uses
24+
import java.util.concurrent.ConcurrentHashMap
25+
import java.util.concurrent.ConcurrentMap
26+
27+
data class OverShotDependencies(
28+
internal val delegate: ConcurrentMap<ConfigurationName, Set<OverShotDependencyFinding>>
29+
) : ConcurrentMap<ConfigurationName, Set<OverShotDependencyFinding>> by delegate,
30+
ProjectContext.Element {
31+
32+
override val key: ProjectContext.Key<OverShotDependencies>
33+
get() = Key
34+
35+
companion object Key : ProjectContext.Key<OverShotDependencies> {
36+
override operator fun invoke(project: Project2): OverShotDependencies {
37+
38+
val used = project.unusedDependencies
39+
.values
40+
.flatMap { allUnused ->
41+
allUnused
42+
.flatMap { unused ->
43+
44+
val configSuffix = unused.configurationName
45+
.nameWithoutSourceSet()
46+
.takeIf { !it.equals(ConfigurationName.api.value, ignoreCase = true) }
47+
?: ConfigurationName.implementation.value
48+
49+
val all = project.configurations
50+
.values
51+
.asSequence()
52+
.filterNot { it.name.nameWithoutSourceSet().isBlank() }
53+
.sortedByDescending {
54+
it.name.nameWithoutSourceSet()
55+
.equals(configSuffix, ignoreCase = true)
56+
}
57+
.mapNotNull { dependentConfig ->
58+
59+
ConfiguredProjectDependency(
60+
configurationName = dependentConfig.name,
61+
project = unused.dependencyProject
62+
)
63+
.takeIf { project.uses(it) }
64+
}
65+
.distinctBy { it.configurationName.toSourceSetName() }
66+
.groupBy { it.configurationName }
67+
68+
val allConfigs = all.values
69+
.flatMap { cpds ->
70+
cpds.map { project.configurations.getValue(it.configurationName) }
71+
}
72+
.distinct()
73+
74+
val top = allConfigs.filter { cfg ->
75+
cfg.inherited.none { it in allConfigs }
76+
}
77+
78+
top.flatMap { all.getValue(it.name) }
79+
.filter { project.projectDependencies.value[it.configurationName].isNullOrEmpty() }
80+
.toSet()
81+
}
82+
}
83+
84+
val grouped = used.map { cpp ->
85+
86+
OverShotDependencyFinding(
87+
dependentPath = project.path,
88+
buildFile = project.buildFile,
89+
dependencyProject = cpp.project,
90+
dependencyIdentifier = cpp.project.path,
91+
configurationName = cpp.configurationName
92+
)
93+
}
94+
.groupBy { it.configurationName }
95+
.mapValues { it.value.toSet() }
96+
97+
return OverShotDependencies(ConcurrentHashMap(grouped))
98+
}
99+
}
100+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class ModuleCheckRuleFactory : RuleFactory {
3939
{ RedundantRule(it) },
4040
{ SortDependenciesRule(it) },
4141
{ SortPluginsRule(it) },
42+
{ OverShotDependencyRule(it) },
4243
{ UnusedDependencyRule(it) },
4344
{ UnusedKaptRule(it) },
4445
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (C) 2021 Rick Busarow
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
package modulecheck.core.rule
17+
18+
import modulecheck.api.Project2
19+
import modulecheck.api.all
20+
import modulecheck.api.settings.ModuleCheckSettings
21+
import modulecheck.core.OverShotDependencyFinding
22+
import modulecheck.core.overshotDependencies
23+
24+
class OverShotDependencyRule(
25+
override val settings: ModuleCheckSettings
26+
) : ModuleCheckRule<OverShotDependencyFinding>() {
27+
28+
override val id = "OverShotDependency"
29+
override val description = "Finds project dependencies which aren't used by the declaring" +
30+
" configuration, but are used by a dependent configuration."
31+
32+
override fun check(project: Project2): List<OverShotDependencyFinding> {
33+
return project.overshotDependencies
34+
.all()
35+
.filterNot { it.dependencyProject.path in settings.ignoreUnusedFinding }
36+
.sortedByDescending { it.configurationName }
37+
}
38+
}

modulecheck-core/src/test/kotlin/modulecheck/core/rule/RulesRegistrationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import modulecheck.api.settings.ChecksSettings.Companion.DISABLE_ANDROID_RESOURC
2424
import modulecheck.api.settings.ChecksSettings.Companion.DISABLE_VIEW_BINDING_DEFAULT
2525
import modulecheck.api.settings.ChecksSettings.Companion.INHERITED_DEPENDENCY_DEFAULT
2626
import modulecheck.api.settings.ChecksSettings.Companion.MUST_BE_API_DEFAULT
27+
import modulecheck.api.settings.ChecksSettings.Companion.OVERSHOT_DEPENDENCY_DEFAULT
2728
import modulecheck.api.settings.ChecksSettings.Companion.REDUNDANT_DEPENDENCY_DEFAULT
2829
import modulecheck.api.settings.ChecksSettings.Companion.SORT_DEPENDENCIES_DEFAULT
2930
import modulecheck.api.settings.ChecksSettings.Companion.SORT_PLUGINS_DEFAULT
@@ -83,6 +84,7 @@ data class TestSettings(
8384
class TestChecksSettings(
8485
override var redundantDependency: Boolean = REDUNDANT_DEPENDENCY_DEFAULT,
8586
override var unusedDependency: Boolean = UNUSED_DEPENDENCY_DEFAULT,
87+
override var overShotDependency: Boolean = OVERSHOT_DEPENDENCY_DEFAULT,
8688
override var mustBeApi: Boolean = MUST_BE_API_DEFAULT,
8789
override var inheritedDependency: Boolean = INHERITED_DEPENDENCY_DEFAULT,
8890
override var sortDependencies: Boolean = SORT_DEPENDENCIES_DEFAULT,

modulecheck-internal-testing/src/main/kotlin/modulecheck/testing/BaseTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ abstract class BaseTest : HermitJUnit5() {
4949
testProjectDir.delete()
5050
}
5151

52-
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE")
52+
@Suppress("UNCHECKED_CAST", "NOTHING_TO_INLINE", "MagicNumber")
5353
infix fun <T, U : T> T.shouldBe(expected: U?) {
5454
/*
5555
Any AssertionError generated by this function will have this function at the top of its stacktrace.
@@ -65,6 +65,7 @@ abstract class BaseTest : HermitJUnit5() {
6565
assertionError.stackTrace = assertionError
6666
.stackTrace
6767
.drop(1)
68+
.take(5)
6869
.toTypedArray()
6970
throw assertionError
7071
}

0 commit comments

Comments
 (0)