diff --git a/modulecheck-api/src/main/kotlin/modulecheck/api/finding/dependencies.kt b/modulecheck-api/src/main/kotlin/modulecheck/api/finding/dependencies.kt index e221d13a68..d98608a27d 100644 --- a/modulecheck-api/src/main/kotlin/modulecheck/api/finding/dependencies.kt +++ b/modulecheck-api/src/main/kotlin/modulecheck/api/finding/dependencies.kt @@ -15,20 +15,104 @@ package modulecheck.api.finding +import kotlinx.coroutines.runBlocking import modulecheck.parsing.gradle.Declaration import modulecheck.parsing.gradle.DependencyDeclaration +import modulecheck.parsing.gradle.ModuleDependencyDeclaration +import modulecheck.parsing.gradle.ProjectPath import modulecheck.project.ConfiguredDependency import modulecheck.project.ConfiguredProjectDependency import modulecheck.project.McProject +import modulecheck.utils.isGreaterThan +import modulecheck.utils.remove +import modulecheck.utils.replaceDestructured +import modulecheck.utils.sortedWith import org.jetbrains.kotlin.util.prefixIfNot +import org.jetbrains.kotlin.util.suffixIfNot fun McProject.addDependency( cpd: ConfiguredProjectDependency, newDeclaration: DependencyDeclaration, - markerDeclaration: DependencyDeclaration + existingDeclaration: DependencyDeclaration? = null +) { + + if (existingDeclaration != null) { + prependStatement(newDeclaration = newDeclaration, existingDeclaration = existingDeclaration) + } else { + addStatement(newDeclaration = newDeclaration) + } + + projectDependencies.add(cpd) +} + +/** + * Finds the existing project dependency declaration (if there are any) which is the closest match + * to the desired new dependency. + * + * @param matchPathFirst If true, matching project paths will be prioritized over matching + * configurations. If false, configuration matches will take priority over a matching project path. + * @return the closest matching declaration, or null if there are no declarations at all. + */ +suspend fun McProject.closestDeclarationOrNull( + newDependency: ConfiguredProjectDependency, + matchPathFirst: Boolean +): DependencyDeclaration? { + + return buildFileParser.dependenciesBlocks() + .firstNotNullOfOrNull { dependenciesBlock -> + + val allModuleDeclarations = dependenciesBlock.settings + + allModuleDeclarations + .sortedWith( + { + if (matchPathFirst) it.configName == newDependency.configurationName + else it.configName != newDependency.configurationName + }, + { it !is ModuleDependencyDeclaration }, + { (it as? ModuleDependencyDeclaration)?.projectPath?.value ?: "" } + ) + .let { sorted -> + + sorted.firstOrNull { it.projectPathOrNull() == newDependency.path } + ?: sorted.firstOrNull { + it.projectPathOrNull()?.isGreaterThan(newDependency.path) ?: false + } + ?: sorted.lastOrNull() + } + ?.let { declaration -> + + val sameProject = declaration.projectPathOrNull() == newDependency.path + + if (sameProject) { + declaration + } else { + + val precedingWhitespace = "^\\s*".toRegex() + .find(declaration.statementWithSurroundingText)?.value ?: "" + + (declaration as? ModuleDependencyDeclaration)?.copy( + statementWithSurroundingText = declaration.declarationText + .prefixIfNot(precedingWhitespace) + // strip out any config block + .remove(""" *\{[\s\S]*}""".toRegex()), + suppressed = emptyList() + ) + } + } + } +} + +private fun DependencyDeclaration.projectPathOrNull(): ProjectPath? { + return (this as? ModuleDependencyDeclaration)?.projectPath +} + +private fun McProject.prependStatement( + newDeclaration: DependencyDeclaration, + existingDeclaration: DependencyDeclaration ) = synchronized(buildFile) { - val oldStatement = markerDeclaration.statementWithSurroundingText + val oldStatement = existingDeclaration.statementWithSurroundingText val newStatement = newDeclaration.statementWithSurroundingText // the `prefixIfNot("\n")` here is important. @@ -40,8 +124,39 @@ fun McProject.addDependency( val buildFileText = buildFile.readText() buildFile.writeText(buildFileText.replace(oldStatement, combinedStatement)) +} - projectDependencies.add(cpd) +private fun McProject.addStatement( + newDeclaration: DependencyDeclaration +) = synchronized(buildFile) { + + val newStatement = newDeclaration.statementWithSurroundingText + + val buildFileText = buildFile.readText() + + runBlocking { + val oldBlockOrNull = buildFileParser.dependenciesBlocks().lastOrNull() + + if (oldBlockOrNull != null) { + + val newBlock = oldBlockOrNull.fullText + .replaceDestructured("""([\s\S]*)}(\s*)""".toRegex()) { group1, group2 -> + + val prefix = group1.trim(' ') + .suffixIfNot("\n") + + "$prefix$newStatement}$group2" + } + + buildFile.writeText(buildFileText.replace(oldBlockOrNull.fullText, newBlock)) + } else { + + val newBlock = "\n\ndependencies {\n${newStatement.suffixIfNot("\n")}}" + val newText = buildFileText + newBlock + + buildFile.writeText(newText) + } + } } fun McProject.removeDependencyWithComment( diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt index 78367b0ec0..7e5210ab4f 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/InheritedDependencyFinding.kt @@ -18,10 +18,12 @@ package modulecheck.core import modulecheck.api.finding.AddsDependency import modulecheck.api.finding.Finding.Position import modulecheck.api.finding.addDependency +import modulecheck.api.finding.closestDeclarationOrNull import modulecheck.core.internal.positionIn import modulecheck.core.internal.statementOrNullIn import modulecheck.parsing.gradle.Declaration import modulecheck.parsing.gradle.ModuleDependencyDeclaration +import modulecheck.parsing.gradle.createProjectDependencyDeclaration import modulecheck.project.ConfiguredProjectDependency import modulecheck.project.McProject import modulecheck.utils.LazyDeferred @@ -60,15 +62,24 @@ data class InheritedDependencyFinding( override suspend fun fix(): Boolean { - val oldDeclaration = declarationOrNull.await() as? ModuleDependencyDeclaration ?: return false + val token = dependentProject + .closestDeclarationOrNull( + newDependency, + matchPathFirst = false + ) as? ModuleDependencyDeclaration - val newDeclaration = oldDeclaration.replace( + val newDeclaration = token?.replace( newConfigName = newDependency.configurationName, newModulePath = newDependency.path, testFixtures = newDependency.isTestFixture ) + ?: dependentProject.createProjectDependencyDeclaration( + configurationName = newDependency.configurationName, + projectPath = newDependency.path, + isTestFixtures = newDependency.isTestFixture + ) - dependentProject.addDependency(newDependency, newDeclaration, oldDeclaration) + dependentProject.addDependency(newDependency, newDeclaration, token) return true } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt index ab91a92c68..b1421e3dd4 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/MustBeApiFinding.kt @@ -19,11 +19,13 @@ import modulecheck.api.finding.AddsDependency import modulecheck.api.finding.ModifiesDependency import modulecheck.api.finding.RemovesDependency import modulecheck.api.finding.addDependency +import modulecheck.api.finding.closestDeclarationOrNull import modulecheck.api.finding.removeDependencyWithDelete import modulecheck.core.internal.statementOrNullIn import modulecheck.parsing.gradle.ConfigurationName import modulecheck.parsing.gradle.Declaration import modulecheck.parsing.gradle.ModuleDependencyDeclaration +import modulecheck.parsing.gradle.createProjectDependencyDeclaration import modulecheck.project.ConfiguredProjectDependency import modulecheck.project.McProject import modulecheck.utils.LazyDeferred @@ -64,15 +66,30 @@ data class MustBeApiFinding( override suspend fun fix(): Boolean { - val oldDeclaration = declarationOrNull.await() as? ModuleDependencyDeclaration ?: return false + val token = dependentProject + .closestDeclarationOrNull( + newDependency, + matchPathFirst = true + ) as? ModuleDependencyDeclaration - val newDeclaration = oldDeclaration.replace( + val oldDeclaration = declarationOrNull.await() + + val newDeclaration = token?.replace( newConfigName = newDependency.configurationName, + newModulePath = newDependency.path, testFixtures = newDependency.isTestFixture ) + ?: dependentProject.createProjectDependencyDeclaration( + configurationName = newDependency.configurationName, + projectPath = newDependency.path, + isTestFixtures = newDependency.isTestFixture + ) + + dependentProject.addDependency(newDependency, newDeclaration, token) - dependentProject.addDependency(newDependency, newDeclaration, oldDeclaration) - dependentProject.removeDependencyWithDelete(oldDeclaration, oldDependency) + if (oldDeclaration != null) { + dependentProject.removeDependencyWithDelete(oldDeclaration, oldDependency) + } return true } diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt index 8a3f09063d..0a195505d7 100644 --- a/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt +++ b/modulecheck-core/src/main/kotlin/modulecheck/core/OverShotDependencyFinding.kt @@ -18,12 +18,13 @@ package modulecheck.core import modulecheck.api.finding.AddsDependency import modulecheck.api.finding.ModifiesDependency import modulecheck.api.finding.RemovesDependency +import modulecheck.api.finding.addDependency +import modulecheck.api.finding.closestDeclarationOrNull import modulecheck.parsing.gradle.ConfigurationName -import modulecheck.parsing.gradle.DependenciesBlock import modulecheck.parsing.gradle.ModuleDependencyDeclaration +import modulecheck.parsing.gradle.createProjectDependencyDeclaration import modulecheck.project.ConfiguredProjectDependency import modulecheck.project.McProject -import org.jetbrains.kotlin.util.prefixIfNot data class OverShotDependencyFinding( override val dependentProject: McProject, @@ -45,59 +46,28 @@ data class OverShotDependencyFinding( override suspend fun fix(): Boolean { - val blocks = dependentProject.buildFileParser - .dependenciesBlocks() + val token = dependentProject + .closestDeclarationOrNull( + newDependency, + matchPathFirst = false + ) as? ModuleDependencyDeclaration - val sourceDeclaration = blocks.firstNotNullOfOrNull { block -> - - block.getOrEmpty(dependencyProject.path, oldDependency.configurationName) - .firstOrNull() - } ?: return false - - val positionBlockDeclarationPair = blocks.firstNotNullOfOrNull { block -> - - val match = matchingDeclaration(block) ?: return@firstNotNullOfOrNull null - - block to match - } ?: return false - - val (block, positionDeclaration) = positionBlockDeclarationPair - - val newDeclaration = sourceDeclaration.replace( - configurationName, testFixtures = newDependency.isTestFixture + val newDeclaration = token?.replace( + newConfigName = newDependency.configurationName, + newModulePath = newDependency.path, + testFixtures = newDependency.isTestFixture ) + ?: dependentProject.createProjectDependencyDeclaration( + configurationName = newDependency.configurationName, + projectPath = newDependency.path, + isTestFixtures = newDependency.isTestFixture + ) - val oldStatement = positionDeclaration.statementWithSurroundingText - val newStatement = oldStatement.plus( - newDeclaration.statementWithSurroundingText - .prefixIfNot("\n") - ) - - val newBlock = block.lambdaContent.replaceFirst( - oldValue = oldStatement, - newValue = newStatement - ) - - val fileText = buildFile.readText() - .replace(block.lambdaContent, newBlock) - - buildFile.writeText(fileText) - - // dependencyProject.removeDependencyWithDelete(oldDependency) - // dependencyProject.addDependency(newDependency) + dependentProject.addDependency(newDependency, newDeclaration, token) return true } - private fun matchingDeclaration(block: DependenciesBlock) = block.settings - .filterIsInstance() - .maxByOrNull { declaration -> declaration.configName == configurationName } - ?: block.settings - .filterNot { it is ModuleDependencyDeclaration } - .maxByOrNull { declaration -> declaration.configName == configurationName } - ?: block.settings - .lastOrNull() - override fun fromStringOrEmpty(): String = "" override fun toString(): String { diff --git a/modulecheck-core/src/test/kotlin/modulecheck/core/InheritedDependenciesTest.kt b/modulecheck-core/src/test/kotlin/modulecheck/core/InheritedDependenciesTest.kt index e9e985abee..56f5e2e05c 100644 --- a/modulecheck-core/src/test/kotlin/modulecheck/core/InheritedDependenciesTest.kt +++ b/modulecheck-core/src/test/kotlin/modulecheck/core/InheritedDependenciesTest.kt @@ -376,6 +376,967 @@ class InheritedDependenciesTest : RunnerTest() { ) } + @Test + fun `inherited as api from invisible dependency with a visible unrelated api project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + class Lib3Class + """.trimIndent() + ) + } + + val lib4 = project(":lib4") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + addDependency(ConfigurationName.api, lib3) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib3")) + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + val clazz = Lib1Class() + val clazz2 = Lib2Class() + val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib4.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + api(project(path = ":lib3")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib4" to listOf( + inheritedDependency( + fixed = true, + configuration = "api", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as api from invisible dependency with a visible unrelated implementation project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + class Lib3Class + """.trimIndent() + ) + } + + val lib4 = project(":lib4") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + addDependency(ConfigurationName.implementation, lib3) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib3")) + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib4.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + implementation(project(path = ":lib3")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib4" to listOf( + inheritedDependency( + fixed = true, + configuration = "api", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with a visible unrelated api project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + class Lib3Class + """.trimIndent() + ) + } + + val lib4 = project(":lib4") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + addDependency(ConfigurationName.api, lib3) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib3")) + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib4.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib1")) + api(project(path = ":lib3")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib4" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with a visible unrelated implementation project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + class Lib3Class + """.trimIndent() + ) + } + + val lib4 = project(":lib4") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + addDependency(ConfigurationName.implementation, lib3) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib3")) + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib4.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib1")) + implementation(project(path = ":lib3")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib4" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with an empty multi-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with an empty single-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with no dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with only external implementation dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + implementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + + @Test + fun `inherited as implementation from invisible dependency with only external api dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + open class Lib2Class : Lib1Class() + """.trimIndent() + ) + } + + val lib3 = project(":lib3") { + // lib2 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + addSource( + "com/modulecheck/lib4/Lib4Class.kt", + """ + package com.modulecheck.lib4 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + import com.modulecheck.lib3.Lib3Class + + private val clazz = Lib1Class() + val clazz2 = Lib2Class() + private val clazz3 = Lib3Class() + """.trimIndent() + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + implementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + inheritedDependency( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + source = ":lib2", + position = null + ) + ) + ) + } + @Test fun `inherited as internalImplementation from internalApi dependency with auto-correct should be fixed`() { @@ -2162,8 +3123,8 @@ class InheritedDependenciesTest : RunnerTest() { } dependencies { - // api(testFixtures(project(path = ":lib1"))) // ModuleCheck finding [unusedDependency] testFixturesApi(testFixtures(project(path = ":lib1"))) + // api(testFixtures(project(path = ":lib1"))) // ModuleCheck finding [unusedDependency] } """ diff --git a/modulecheck-core/src/test/kotlin/modulecheck/core/MustBeApiTest.kt b/modulecheck-core/src/test/kotlin/modulecheck/core/MustBeApiTest.kt index 41d26d2776..e00ec798bf 100644 --- a/modulecheck-core/src/test/kotlin/modulecheck/core/MustBeApiTest.kt +++ b/modulecheck-core/src/test/kotlin/modulecheck/core/MustBeApiTest.kt @@ -836,6 +836,535 @@ class MustBeApiTest : RunnerTest() { ) } + @Test + fun `must be api from invisible dependency with unrelated api dependency declaration`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + class Lib2Class + """ + ) + } + + val lib3 = project(":lib3") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + addDependency(ConfigurationName.api, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib2")) + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + api(project(path = ":lib2")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with unrelated implementation dependency declaration`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + class Lib2Class + """ + ) + } + + val lib3 = project(":lib3") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + addDependency(ConfigurationName.implementation, lib2) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib2")) + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib3.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib1")) + implementation(project(path = ":lib2")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib3" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with unrelated implementation external dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + api(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with unrelated api external dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + api(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with empty multi-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with empty single-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `must be api from invisible dependency with no dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """ + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.implementation, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + + addSource( + "com/modulecheck/lib3/Lib3Class.kt", + """ + package com.modulecheck.lib3 + + import com.modulecheck.lib1.Lib1Class + import com.modulecheck.lib2.Lib2Class + + class Lib3Class : Lib1Class() + private val lib2Class = Lib2Class() + """ + ) + } + + run().isSuccess shouldBe true + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + mustBeApi( + fixed = true, + configuration = "implementation", + dependency = ":lib1", + position = null + ) + ) + ) + } + @Test fun `auto-correct should only replace the configuration invocation text`() { diff --git a/modulecheck-core/src/test/kotlin/modulecheck/core/OverShotDependenciesTest.kt b/modulecheck-core/src/test/kotlin/modulecheck/core/OverShotDependenciesTest.kt index 33e58de5a4..6831548ce5 100644 --- a/modulecheck-core/src/test/kotlin/modulecheck/core/OverShotDependenciesTest.kt +++ b/modulecheck-core/src/test/kotlin/modulecheck/core/OverShotDependenciesTest.kt @@ -215,8 +215,8 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { - // api(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] testImplementation(project(path = ":lib1")) + // api(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] } """ @@ -287,8 +287,8 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { - // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] debugApi(project(path = ":lib1")) + // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] } """ @@ -359,8 +359,8 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { - // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] debugImplementation(project(path = ":lib1")) + // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] } """ @@ -430,8 +430,8 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { - // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] "debugApi"(project(path = ":lib1")) + // implementation(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] } """ @@ -522,9 +522,9 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { + testImplementation(project(path = ":lib1")) // api(project(path = ":lib1")) // ModuleCheck finding [unusedDependency] testImplementation(testFixtures(project(path = ":lib2"))) - testImplementation(project(path = ":lib1")) } """ @@ -617,15 +617,15 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { + // a comment + testImplementation(project(path = ":lib1")) { + because("this is a test") + } // // a comment // api(project(path = ":lib1")) { // because("this is a test") // } // ModuleCheck finding [unusedDependency] testImplementation(project(path = ":lib2")) - // a comment - testImplementation(project(path = ":lib1")) { - because("this is a test") - } } """ @@ -739,9 +739,9 @@ class OverShotDependenciesTest : RunnerTest() { } dependencies { + testImplementation(testFixtures(project(path = ":lib1"))) // api(testFixtures(project(path = ":lib1"))) // ModuleCheck finding [unusedDependency] testImplementation(testFixtures(project(path = ":lib2"))) - testImplementation(testFixtures(project(path = ":lib1"))) } """ @@ -762,4 +762,519 @@ class OverShotDependenciesTest : RunnerTest() { ) ) } + + @Test + fun `overshot as testImplementation from invisible dependency with a visible unrelated api project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api(project(path = ":lib4")) + } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + testImplementation(project(path = ":lib1")) + api(project(path = ":lib4")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as testImplementation from invisible dependency with a visible unrelated implementation project dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation(project(path = ":lib4")) + } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + testImplementation(project(path = ":lib1")) + implementation(project(path = ":lib4")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as testImplementation from invisible dependency with an empty multi-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + private val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + testImplementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as testImplementation from invisible dependency with an empty single-line dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + private val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + testImplementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as testImplementation from invisible dependency with no dependencies block`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + private val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + testImplementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as testImplementation from invisible dependency with only external implementation dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + addSource( + "com/modulecheck/lib2/lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + private val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + testImplementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } + + @Test + fun `overshot as implementation from invisible dependency with only external api dependency`() { + + val lib1 = project(":lib1") { + addSource( + "com/modulecheck/lib1/Lib1Class.kt", + """ + package com.modulecheck.lib1 + + open class Lib1Class + """.trimIndent() + ) + } + + val lib2 = project(":lib2") { + // lib1 is added as a dependency, but it's not in the build file. + // This is intentional, because it mimics the behavior of a convention plugin + // which adds a dependency without any visible declaration in the build file + addDependency(ConfigurationName.api, lib1) + + buildFile { + """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + } + """ + } + addSource( + "com/modulecheck/lib2/Lib2Class.kt", + """ + package com.modulecheck.lib2 + + import com.modulecheck.lib1.Lib1Class + + private val clazz = Lib1Class() + """.trimIndent(), + SourceSetName.TEST + ) + } + + run().isSuccess shouldBe false + + lib2.buildFile shouldHaveText """ + plugins { + kotlin("jvm") + } + + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") + testImplementation(project(":lib1")) + } + """ + + logger.parsedReport() shouldBe listOf( + ":lib2" to listOf( + overshot( + fixed = true, + configuration = "testImplementation", + dependency = ":lib1", + position = null + ), + unusedDependency( + fixed = false, + configuration = "api", + dependency = ":lib1", + position = null + ) + ) + ) + } } diff --git a/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt b/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt index 99fdc2c964..deb5970437 100644 --- a/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt +++ b/modulecheck-core/src/test/kotlin/modulecheck/core/UnusedDependenciesTest.kt @@ -1289,8 +1289,8 @@ class UnusedDependenciesTest : RunnerTest() { } dependencies { - // testImplementation(testFixtures(project(path = ":lib1"))) // ModuleCheck finding [unusedDependency] testImplementation(project(path = ":lib1")) + // testImplementation(testFixtures(project(path = ":lib1"))) // ModuleCheck finding [unusedDependency] } """ diff --git a/modulecheck-project/api/src/main/kotlin/modulecheck/project/BuildFileParser.kt b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/BuildFileParser.kt similarity index 82% rename from modulecheck-project/api/src/main/kotlin/modulecheck/project/BuildFileParser.kt rename to modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/BuildFileParser.kt index 6cfa9ee2ad..81886f781b 100644 --- a/modulecheck-project/api/src/main/kotlin/modulecheck/project/BuildFileParser.kt +++ b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/BuildFileParser.kt @@ -13,20 +13,13 @@ * limitations under the License. */ -package modulecheck.project +package modulecheck.parsing.gradle import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import modulecheck.parsing.gradle.AndroidGradleSettings -import modulecheck.parsing.gradle.AndroidGradleSettingsProvider -import modulecheck.parsing.gradle.DependenciesBlock -import modulecheck.parsing.gradle.DependenciesBlocksProvider -import modulecheck.parsing.gradle.InvokesConfigurationNames -import modulecheck.parsing.gradle.PluginsBlock -import modulecheck.parsing.gradle.PluginsBlockProvider class BuildFileParser @AssistedInject constructor( dependenciesBlocksProviderFactory: DependenciesBlocksProvider.Factory, diff --git a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/InvokesConfigurationNames.kt b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/InvokesConfigurationNames.kt index 037299af4a..a65179473e 100644 --- a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/InvokesConfigurationNames.kt +++ b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/InvokesConfigurationNames.kt @@ -15,8 +15,12 @@ package modulecheck.parsing.gradle +import modulecheck.parsing.gradle.ProjectAccessor.TypeSafeProjectAccessor import modulecheck.parsing.gradle.ProjectPath.StringProjectPath import modulecheck.parsing.gradle.SourceSetName.Companion +import modulecheck.utils.findMinimumIndent +import modulecheck.utils.letIf +import modulecheck.utils.mapToSet import java.io.File interface InvokesConfigurationNames : @@ -35,6 +39,8 @@ interface PluginAware { interface HasBuildFile { val buildFile: File + + val buildFileParser: BuildFileParser } interface HasDependencyDeclarations : HasBuildFile, HasConfigurations { @@ -51,30 +57,6 @@ interface HasConfigurations { val configurations: Configurations } -/** - * Reverse lookup of all the configurations which inherit another configuration. - * - * For instance, every java/kotlin configuration (`implementation`, `testImplementation`, - * etc.) within a project inherits from the common `api` configuration, so - * `someProject.inheritingConfigurations(ConfigurationName.api)` would return all other java/kotlin - * configurations within that project. - */ -fun HasConfigurations.inheritingConfigurations(configurationName: ConfigurationName): Set { - return configurations.values - .asSequence() - .map { it.name.toSourceSetName() } - .flatMap { sourceSet -> - sourceSet.javaConfigurationNames() - .mapNotNull { configName -> configurations[configName] } - } - .filter { inheritingConfig -> - inheritingConfig.upstream - .any { inheritedConfig -> - inheritedConfig.name == configurationName - } - }.toSet() -} - /** * Precompiled configuration names are names which are added by a pre-compiled plugin. These names * can be used as functions in Kotlin scripts. examples: @@ -103,6 +85,62 @@ suspend fun ConfigurationName.isDefinitelyPrecompiledForProject(project: T): project.getConfigurationInvocations().contains(value) } +suspend fun T.createProjectDependencyDeclaration( + configurationName: ConfigurationName, + projectPath: StringProjectPath, + isTestFixtures: Boolean +): ModuleDependencyDeclaration + where T : PluginAware, + T : HasDependencyDeclarations { + + val isKotlin = buildFile.extension == "kts" + + val configInvocation = when { + isKotlin && !configurationName.isDefinitelyPrecompiledForProject(this) -> { + configurationName.wrapInQuotes() + } + else -> configurationName.value + } + + val projectAccessorText = projectAccessors() + .any { it is TypeSafeProjectAccessor } + .let { useTypeSafe -> + if (useTypeSafe) { + "projects.${projectPath.typeSafeValue}" + } else if (isKotlin) { + "project(\"${projectPath.value}\")" + } else { + "project('${projectPath.value}')" + } + } + + val projectAccessor = ProjectAccessor.from(projectAccessorText, projectPath) + + val projectWithTestFixtures = projectAccessorText + .letIf(isTestFixtures) { "testFixtures($it)" } + + val declarationText = if (isKotlin) { + "$configInvocation($projectWithTestFixtures)" + } else "$configInvocation $projectWithTestFixtures" + + val statementWithSurroundingText = buildFileParser.dependenciesBlocks() + .map { it.lambdaContent.findMinimumIndent() } + .minByOrNull { it.length } + .let { min -> + val indent = min ?: " " + "$indent$declarationText\n" + } + + return ModuleDependencyDeclaration( + projectPath, + projectAccessor, + configurationName, + declarationText, + statementWithSurroundingText, + emptyList() + ) { it.value } +} + @Suppress("ComplexMethod") private tailrec fun SourceSetName.isDefinitelyPrecompiledForProject(project: T): Boolean where T : PluginAware, @@ -201,3 +239,10 @@ private suspend fun ConfigurationName.shouldUseQuotes( // used as a normal function invocation return !isDefinitelyPrecompiledForProject(invokesConfigurationNames) } + +private suspend fun HasBuildFile.projectAccessors(): Set { + return buildFileParser.dependenciesBlocks() + .flatMap { it.settings } + .filterIsInstance() + .mapToSet { declaration -> declaration.projectAccessor } +} diff --git a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/ProjectPath.kt b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/ProjectPath.kt index 4abcdcc364..473051b9af 100644 --- a/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/ProjectPath.kt +++ b/modulecheck-parsing/gradle/src/main/kotlin/modulecheck/parsing/gradle/ProjectPath.kt @@ -22,7 +22,7 @@ sealed class ProjectPath : Comparable { abstract val value: String - private val typeSafeValue: String by lazy { + val typeSafeValue: String by lazy { when (this) { is StringProjectPath -> value.typeSafeName() is TypeSafeProjectPath -> value @@ -63,7 +63,7 @@ sealed class ProjectPath : Comparable { class TypeSafeProjectPath(override val value: String) : ProjectPath() companion object { - fun from(rawString: String): ProjectPath = if (rawString.startsWith(':')) { + fun from(rawString: String): ProjectPath = if (rawString.trim().startsWith(':')) { StringProjectPath(rawString) } else { TypeSafeProjectPath(rawString) @@ -75,11 +75,10 @@ internal val projectSplitRegex = "[.\\-_]".toRegex() /** * Takes a conventional Gradle project path (":core:jvm") and returns the type-safe accessor name. - * - * `:core` becomes `core` - * `:core:jvm` becomes `core.jvm` - * `:core-testing` becomes `coreTesting` - * `:base:ui:navigation` becomes `base.ui.navigation` + * - `:core` becomes `core` + * - `:core:jvm` becomes `core.jvm` + * - `:core-testing` becomes `coreTesting` + * - `:base:ui:navigation` becomes `base.ui.navigation` */ internal fun String.typeSafeName(): String = split(projectSplitRegex) .filterNot { it.isBlank() } diff --git a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/GradleProjectProvider.kt b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/GradleProjectProvider.kt index 2fbe80a983..90bd860a90 100644 --- a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/GradleProjectProvider.kt +++ b/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/GradleProjectProvider.kt @@ -28,6 +28,7 @@ import modulecheck.gradle.internal.androidManifests import modulecheck.gradle.internal.sourcesets.AndroidSourceSetsParser import modulecheck.gradle.internal.sourcesets.JvmSourceSetParser import modulecheck.gradle.task.GradleLogger +import modulecheck.parsing.gradle.BuildFileParser import modulecheck.parsing.gradle.ConfigFactory import modulecheck.parsing.gradle.Configurations import modulecheck.parsing.gradle.ProjectPath.StringProjectPath @@ -36,7 +37,6 @@ import modulecheck.parsing.gradle.asConfigurationName import modulecheck.parsing.source.AnvilGradlePlugin import modulecheck.parsing.source.JavaVersion import modulecheck.parsing.wiring.RealJvmFileProvider -import modulecheck.project.BuildFileParser import modulecheck.project.ConfiguredProjectDependency import modulecheck.project.ExternalDependencies import modulecheck.project.ExternalDependency diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt index 6c5a4af4e4..7ba7f01b89 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/AnvilScopesTest.kt @@ -24,7 +24,7 @@ import modulecheck.specs.ProjectBuildSpecBuilder import modulecheck.specs.ProjectSettingsSpecBuilder import modulecheck.specs.ProjectSpec import modulecheck.specs.ProjectSrcSpec -import modulecheck.specs.applyEach +import modulecheck.utils.applyEach import org.junit.jupiter.api.Test import java.nio.file.Path diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt index f172296e5c..c3e9e62f40 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesPluginTest.kt @@ -26,7 +26,7 @@ import modulecheck.specs.ProjectBuildSpecBuilder import modulecheck.specs.ProjectSettingsSpecBuilder import modulecheck.specs.ProjectSpec import modulecheck.specs.ProjectSrcSpec -import modulecheck.specs.applyEach +import modulecheck.utils.applyEach import org.junit.jupiter.api.Test import java.io.File import java.nio.file.Path diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/jvmSubProject.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/jvmSubProject.kt index cf0ec341bd..6266cbf89b 100644 --- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/jvmSubProject.kt +++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/jvmSubProject.kt @@ -21,7 +21,7 @@ import com.squareup.kotlinpoet.TypeSpec import modulecheck.specs.ProjectBuildSpec import modulecheck.specs.ProjectSpec import modulecheck.specs.ProjectSrcSpec -import modulecheck.specs.applyEach +import modulecheck.utils.applyEach import java.nio.file.Path @Suppress("LongParameterList") diff --git a/modulecheck-project/api/src/main/kotlin/modulecheck/project/McProject.kt b/modulecheck-project/api/src/main/kotlin/modulecheck/project/McProject.kt index f798bdcfb5..c1dcae9f6f 100644 --- a/modulecheck-project/api/src/main/kotlin/modulecheck/project/McProject.kt +++ b/modulecheck-project/api/src/main/kotlin/modulecheck/project/McProject.kt @@ -53,8 +53,6 @@ interface McProject : override val hasAGP: Boolean get() = this is AndroidMcProject - val buildFileParser: BuildFileParser - val logger: Logger val jvmFileProviderFactory: JvmFileProvider.Factory diff --git a/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/AndroidMcProjectBuilderScope.kt b/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/AndroidMcProjectBuilderScope.kt index fb490c27f6..ef05936e93 100644 --- a/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/AndroidMcProjectBuilderScope.kt +++ b/modulecheck-project/api/src/testFixtures/kotlin/modulecheck/project/test/AndroidMcProjectBuilderScope.kt @@ -15,6 +15,7 @@ package modulecheck.project.test +import modulecheck.parsing.gradle.BuildFileParser import modulecheck.parsing.gradle.Config import modulecheck.parsing.gradle.ConfigurationName import modulecheck.parsing.gradle.Configurations @@ -33,7 +34,6 @@ import modulecheck.parsing.source.JavaVersion import modulecheck.parsing.wiring.RealAndroidGradleSettingsProvider import modulecheck.parsing.wiring.RealDependenciesBlocksProvider import modulecheck.parsing.wiring.RealPluginsBlockProvider -import modulecheck.project.BuildFileParser import modulecheck.project.ExternalDependencies import modulecheck.project.McProject import modulecheck.project.PrintLogger diff --git a/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealAndroidMcProject.kt b/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealAndroidMcProject.kt index 2313def8e4..c29f6e6c06 100644 --- a/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealAndroidMcProject.kt +++ b/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealAndroidMcProject.kt @@ -16,6 +16,7 @@ package modulecheck.project.impl import modulecheck.api.context.resolvedDeclarationNames +import modulecheck.parsing.gradle.BuildFileParser import modulecheck.parsing.gradle.Configurations import modulecheck.parsing.gradle.ProjectPath.StringProjectPath import modulecheck.parsing.gradle.SourceSetName @@ -24,7 +25,6 @@ import modulecheck.parsing.source.AnvilGradlePlugin import modulecheck.parsing.source.JavaVersion import modulecheck.parsing.source.asDeclarationName import modulecheck.project.AndroidMcProject -import modulecheck.project.BuildFileParser import modulecheck.project.ExternalDependencies import modulecheck.project.JvmFileProvider import modulecheck.project.Logger diff --git a/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealMcProject.kt b/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealMcProject.kt index f9f92102c7..96815d556c 100644 --- a/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealMcProject.kt +++ b/modulecheck-project/impl/src/main/kotlin/modulecheck/project/impl/RealMcProject.kt @@ -16,6 +16,7 @@ package modulecheck.project.impl import modulecheck.api.context.resolvedDeclarationNames +import modulecheck.parsing.gradle.BuildFileParser import modulecheck.parsing.gradle.Configurations import modulecheck.parsing.gradle.ProjectPath.StringProjectPath import modulecheck.parsing.gradle.SourceSetName @@ -23,7 +24,6 @@ import modulecheck.parsing.gradle.SourceSets import modulecheck.parsing.source.AnvilGradlePlugin import modulecheck.parsing.source.JavaVersion import modulecheck.parsing.source.asDeclarationName -import modulecheck.project.BuildFileParser import modulecheck.project.ExternalDependencies import modulecheck.project.JvmFileProvider import modulecheck.project.Logger diff --git a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/internal/apply.kt b/modulecheck-utils/src/main/kotlin/modulecheck/utils/apply.kt similarity index 74% rename from modulecheck-plugin/src/main/kotlin/modulecheck/gradle/internal/apply.kt rename to modulecheck-utils/src/main/kotlin/modulecheck/utils/apply.kt index adfad3f371..9efe24cfd9 100644 --- a/modulecheck-plugin/src/main/kotlin/modulecheck/gradle/internal/apply.kt +++ b/modulecheck-utils/src/main/kotlin/modulecheck/utils/apply.kt @@ -13,9 +13,23 @@ * limitations under the License. */ -package modulecheck.gradle.internal +package modulecheck.utils inline fun T.applyEach(elements: Iterable, block: T.(E) -> Unit): T { elements.forEach { element -> this.block(element) } return this } + +inline fun T.applyIf(predicate: Boolean, body: T.() -> T): T = apply { + if (predicate) { + body() + } +} + +inline fun T.letIf(predicate: Boolean, body: (T) -> T): T = let { + if (predicate) { + body(it) + } else { + it + } +} diff --git a/modulecheck-specs/src/main/kotlin/modulecheck/specs/apply.kt b/modulecheck-utils/src/main/kotlin/modulecheck/utils/comparable.kt similarity index 76% rename from modulecheck-specs/src/main/kotlin/modulecheck/specs/apply.kt rename to modulecheck-utils/src/main/kotlin/modulecheck/utils/comparable.kt index 9669ec1e08..2a4756b93c 100644 --- a/modulecheck-specs/src/main/kotlin/modulecheck/specs/apply.kt +++ b/modulecheck-utils/src/main/kotlin/modulecheck/utils/comparable.kt @@ -13,9 +13,8 @@ * limitations under the License. */ -package modulecheck.specs +package modulecheck.utils -public inline fun T.applyEach(elements: Iterable, block: T.(E) -> Unit): T { - elements.forEach { element -> this.block(element) } - return this +infix fun Comparable.isGreaterThan(other: T): Boolean { + return compareTo(other) > 0 } diff --git a/modulecheck-utils/src/main/kotlin/modulecheck/utils/string.kt b/modulecheck-utils/src/main/kotlin/modulecheck/utils/string.kt index a26f6d949f..291b8a93aa 100644 --- a/modulecheck-utils/src/main/kotlin/modulecheck/utils/string.kt +++ b/modulecheck-utils/src/main/kotlin/modulecheck/utils/string.kt @@ -32,6 +32,7 @@ fun String.findMinimumIndent( if (contains("\t")) return "\t" return lines() + .filter { it.isNotBlank() } .map { it.indentWidth() } .filter { it > 0 } .minOrNull()