Repository: RBusarow/ModuleCheck (kotlin)

Issue:
Autocorrect doesn't work when declaring a dependency with a configuration name string
In the Kotlin DSL, all configurations have String extension functions for declaring dependencies, such as:

``` Kotlin
dependencies {
  "implementation"(project(":library"))
}
```

These functions are probably most common in Android apps which use custom build flavors, because AGP does not generate normal non-String functions for those configurations.

Autocorrect should match declarations using Strings just like it does the conventional functions.

Pull request:
fixes #77

Patch:
diff --git a/modulecheck-core/src/main/kotlin/modulecheck/core/internal/list.kt b/modulecheck-core/src/main/kotlin/modulecheck/core/internal/list.kt
index 535dfe4749..677adcf2e3 100644
--- a/modulecheck-core/src/main/kotlin/modulecheck/core/internal/list.kt
+++ b/modulecheck-core/src/main/kotlin/modulecheck/core/internal/list.kt
@@ -28,7 +28,8 @@ fun List<String>.positionOf(
   path: String,
   configuration: ConfigurationName
 ): Position? {
-  val reg = """.*${configuration.value}\(project[(]?(?:path =\s*)"$path".*""".toRegex()
+  // val reg = """.*${configuration.value}\(project[(]?(?:path =\s*)"$path".*""".toRegex()
+  val reg = """.*"?${configuration.value}"?\(project[(]?(?:path =\s*)"$path".*""".toRegex()
 
   val row = indexOfFirst { it.trim().matches(reg) }
 
diff --git a/modulecheck-psi/src/main/kotlin/modulecheck/psi/ProjectDependencyDeclarationVisitor.kt b/modulecheck-psi/src/main/kotlin/modulecheck/psi/ProjectDependencyDeclarationVisitor.kt
index c3e9f9e2a6..20c482220e 100644
--- a/modulecheck-psi/src/main/kotlin/modulecheck/psi/ProjectDependencyDeclarationVisitor.kt
+++ b/modulecheck-psi/src/main/kotlin/modulecheck/psi/ProjectDependencyDeclarationVisitor.kt
@@ -32,25 +32,29 @@ class ProjectDependencyDeclarationVisitor(
     var found = false
 
     val configCallExpressionVisitor = callExpressionVisitor { innerExpression ->
-      innerExpression
+
+      val matchingExpression = innerExpression
         .referenceExpression()
         .takeIf { it?.text == configurationName }
-        ?.let {
-          innerExpression                                     // implementation(project(path = ":foo:bar"))
-            .valueArguments                                   // [project(path = ":foo:bar")]
-            .firstOrNull()                                    // project(path = ":foo:bar")
-            ?.getChildOfType<KtCallExpression>()              // project(path = ":foo:bar")
-            ?.valueArguments                                  // [path = ":foo:bar"]
-            ?.firstOrNull()                                   // path = ":foo:bar"
-            ?.getChildOfType<KtStringTemplateExpression>()    // ":foo:bar"
-            ?.getChildOfType<KtLiteralStringTemplateEntry>()  // :foo:bar
-            ?.let { projectNameArg ->
-
-              if (projectNameArg.text == projectPath) {
-                found = true
-              }
+        ?: innerExpression
+          .takeIf { it.text.startsWith("\"$configurationName\"") }
+
+      if (matchingExpression != null) {
+        innerExpression                                     // implementation(project(path = ":foo:bar"))
+          .valueArguments                                   // [project(path = ":foo:bar")]
+          .firstOrNull()                                    // project(path = ":foo:bar")
+          ?.getChildOfType<KtCallExpression>()              // project(path = ":foo:bar")
+          ?.valueArguments                                  // [path = ":foo:bar"]
+          ?.firstOrNull()                                   // path = ":foo:bar"
+          ?.getChildOfType<KtStringTemplateExpression>()    // ":foo:bar"
+          ?.getChildOfType<KtLiteralStringTemplateEntry>()  // :foo:bar
+          ?.let { projectNameArg ->
+
+            if (projectNameArg.text == projectPath) {
+              found = true
             }
-        }
+          }
+      }
     }
 
     configCallExpressionVisitor.visitCallExpression(expression)


Test patch:
diff --git a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt
index 116cc83e60..38e9f8d8fe 100644
--- a/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt
+++ b/modulecheck-plugin/src/test/kotlin/modulecheck/gradle/UnusedDependenciesTest.kt
@@ -16,7 +16,6 @@
 package modulecheck.gradle
 
 import com.squareup.kotlinpoet.*
-import io.kotest.matchers.shouldBe
 import io.kotest.matchers.string.shouldContain
 import modulecheck.specs.*
 import modulecheck.specs.ProjectSrcSpecBuilder.XmlFile
@@ -56,24 +55,24 @@ class UnusedDependenciesTest : BaseTest() {
       .addImport("com.example.lib1", "Lib1Class")
       .build()
 
-    val appProject = ProjectSpec("app") {
-      addBuildSpec(
-        ProjectBuildSpec {
-          addPlugin("kotlin(\"jvm\")")
-          addProjectDependency("api", jvmSub1)
-          addProjectDependency("api", jvmSub2)
-          addProjectDependency("implementation", jvmSub3)
-        }
-      )
-      addSrcSpec(
-        ProjectSrcSpec(Path.of("src/main/kotlin")) {
-          addFileSpec(myApp)
-        }
-      )
-    }
-
     @Test
     fun `with autoCorrect should be removed`() {
+      val appProject = ProjectSpec("app") {
+        addBuildSpec(
+          ProjectBuildSpec {
+            addPlugin("kotlin(\"jvm\")")
+            addProjectDependency("api", jvmSub1)
+            addProjectDependency("api", jvmSub2)
+            addProjectDependency("implementation", jvmSub3)
+          }
+        )
+        addSrcSpec(
+          ProjectSrcSpec(Path.of("src/main/kotlin")) {
+            addFileSpec(myApp)
+          }
+        )
+      }
+
       ProjectSpec("project") {
         applyEach(projects) { project ->
           addSubproject(project)
@@ -107,8 +106,116 @@ class UnusedDependenciesTest : BaseTest() {
         |""".trimMargin()
     }
 
+    @Test
+    fun `with autoCorrect using string extension functions for configuration should be removed`() {
+      val appProject = ProjectSpec("app") {
+        addBuildSpec(
+          ProjectBuildSpec {
+            addProjectDependency("\"api\"", jvmSub1)
+            addProjectDependency("\"api\"", jvmSub2)
+            addProjectDependency("\"implementation\"", jvmSub3)
+            addPlugin("kotlin(\"jvm\")")
+          }
+        )
+        addSrcSpec(
+          ProjectSrcSpec(Path.of("src/main/kotlin")) {
+            addFileSpec(myApp)
+          }
+        )
+      }
+      ProjectSpec("project") {
+        applyEach(projects) { project ->
+          addSubproject(project)
+        }
+        addSubproject(appProject)
+        addSubprojects(jvmSub1, jvmSub2, jvmSub3)
+        addSettingsSpec(projectSettings.build())
+        addBuildSpec(
+          projectBuild
+            .addBlock(
+              """moduleCheck {
+            |  autoCorrect = true
+            |}
+          """.trimMargin()
+            ).build()
+        )
+      }
+        .writeIn(testProjectDir.toPath())
+
+      build("moduleCheckUnusedDependency").shouldSucceed()
+
+      File(testProjectDir, "/app/build.gradle.kts").readText() shouldBe """plugins {
+        |  kotlin("jvm")
+        |}
+        |
+        |dependencies {
+        |  "api"(project(path = ":lib-1"))
+        |  // "api"(project(path = ":lib-2"))  // ModuleCheck finding [unused]
+        |  // "implementation"(project(path = ":lib-3"))  // ModuleCheck finding [unused]
+        |}
+        |""".trimMargin()
+    }
+
     @Test
     fun `without autoCorrect should fail with report`() {
+      val appProject = ProjectSpec("app") {
+        addBuildSpec(
+          ProjectBuildSpec {
+            addPlugin("kotlin(\"jvm\")")
+            addProjectDependency("api", jvmSub1)
+            addProjectDependency("api", jvmSub2)
+            addProjectDependency("implementation", jvmSub3)
+          }
+        )
+        addSrcSpec(
+          ProjectSrcSpec(Path.of("src/main/kotlin")) {
+            addFileSpec(myApp)
+          }
+        )
+      }
+      ProjectSpec("project") {
+        applyEach(projects) { project ->
+          addSubproject(project)
+        }
+        addSubproject(appProject)
+        addSubprojects(jvmSub1, jvmSub2, jvmSub3)
+        addSettingsSpec(projectSettings.build())
+        addBuildSpec(
+          projectBuild
+            .addBlock(
+              """moduleCheck {
+            |  autoCorrect = false
+            |}
+          """.trimMargin()
+            ).build()
+        )
+      }
+        .writeIn(testProjectDir.toPath())
+
+      shouldFailWithMessage("moduleCheckUnusedDependency") {
+        it shouldContain "> ModuleCheck found 2 issues which were not auto-corrected."
+        it shouldContain "app/build.gradle.kts: (7, 3):  unused: :lib-2"
+        it shouldContain "app/build.gradle.kts: (8, 3):  unused: :lib-3"
+      }
+    }
+
+    @Test
+    fun `without autoCorrect using string extension functions for configuration should fail with report`() {
+      val appProject = ProjectSpec("app") {
+        addBuildSpec(
+          ProjectBuildSpec {
+            addPlugin("kotlin(\"jvm\")")
+            addProjectDependency("\"api\"", jvmSub1)
+            addProjectDependency("\"api\"", jvmSub2)
+            addProjectDependency("\"implementation\"", jvmSub3)
+          }
+        )
+        addSrcSpec(
+          ProjectSrcSpec(Path.of("src/main/kotlin")) {
+            addFileSpec(myApp)
+          }
+        )
+      }
       ProjectSpec("project") {
         applyEach(projects) { project ->
           addSubproject(project)