Repository: cortinico/ktfmt-gradle (kotlin)

Issue:
ktfmt* tasks are not configuration-cache compatible
## 🐛 Describe the bug
ktfmt* tasks are not configuration-cache compatible because `KtfmtBaseTask.scope` is non-serializable 

## ⚠️ Current behavior
Build fails because of configuration cache issues

## ✅ Expected behavior
Build should succeed

## 💣 Steps to reproduce
Run `./gradlew :example:ktfmtFormat --configuration-cache`

Pull request:
## 🚀 Description
Remove references to Coroutine Scope so the tasks can properly work with Config. Cache from Gradle.

## 📄 Motivation and Context
Fixes #81

## 🧪 How Has This Been Tested?
Tested locally with `./gradlew :example:ktfmtCheck --configuration-cache`

## 📦 Types of changes
- [x] New feature (non-breaking change which adds functionality)

Patch:
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 452cbac2..92cb0515 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -21,10 +21,8 @@
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
-            <option value="$PROJECT_DIR$/buildSrc" />
             <option value="$PROJECT_DIR$/example" />
             <option value="$PROJECT_DIR$/plugin-build" />
-            <option value="$PROJECT_DIR$/plugin-build/buildSrc" />
             <option value="$PROJECT_DIR$/plugin-build/plugin" />
           </set>
         </option>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7aa2ec75..0616e663 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,9 @@ Please add your entries according to this format.
 
 ## Unreleased
 
+- Add support for Gradle Configuration Cache 
+- KtFmt to 0.40
+
 ## Version 0.9.0 *(2022-08-29)*
 
 - KtFmt to 0.39
diff --git a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTask.kt b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTask.kt
index c30dee3f..a3883b98 100644
--- a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTask.kt
+++ b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTask.kt
@@ -8,15 +8,11 @@ import com.ncorti.ktfmt.gradle.FormattingOptionsBean
 import com.ncorti.ktfmt.gradle.KtfmtExtension
 import java.io.File
 import java.io.IOException
-import kotlin.io.path.ExperimentalPathApi
 import kotlin.io.path.Path
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.isActive
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.runBlocking
 import org.gradle.api.UnknownDomainObjectException
 import org.gradle.api.file.FileCollection
@@ -24,7 +20,6 @@ import org.gradle.api.provider.Property
 import org.gradle.api.tasks.IgnoreEmptyDirectories
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFiles
-import org.gradle.api.tasks.Internal
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -38,8 +33,6 @@ import org.gradle.api.tasks.options.Option
  */
 abstract class KtfmtBaseTask : SourceTask() {
 
-    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
-
     @get:Input @get:Optional internal var bean: FormattingOptionsBean? = null
 
     @get:Option(
@@ -61,20 +54,16 @@ abstract class KtfmtBaseTask : SourceTask() {
     @TaskAction
     @VisibleForTesting
     internal fun taskAction() {
-
-        runBlocking(scope.coroutineContext) { execute() }
-        scope.cancel()
+        runBlocking(Dispatchers.IO) { execute() }
     }
 
     protected abstract suspend fun execute()
 
-    @OptIn(ExperimentalPathApi::class)
     @VisibleForTesting
     @Suppress("TooGenericExceptionCaught", "SpreadOperator")
     internal fun processFile(input: File): KtfmtResult {
-
-        // The task should try to read from the ktfmt{} extension before falling back to default
-        // values.
+        // The task should try to read from the ktfmt{} extension
+        // before falling back to default values.
         if (bean == null) {
             bean =
                 try {
@@ -115,9 +104,8 @@ abstract class KtfmtBaseTask : SourceTask() {
         }
     }
 
-    internal suspend fun processFileCollection(input: FileCollection): List<KtfmtResult> {
-        return input.map { scope.async { processFile(it) } }.awaitAll()
-    }
-
-    @VisibleForTesting @Internal internal fun isScopeActive() = scope.isActive
+    internal suspend fun processFileCollection(input: FileCollection): List<KtfmtResult> =
+        coroutineScope {
+            input.map { async { processFile(it) } }.awaitAll()
+        }
 }


Test patch:
diff --git a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTaskTest.kt b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTaskTest.kt
index 9444b611..71e33bf0 100644
--- a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTaskTest.kt
+++ b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTaskTest.kt
@@ -42,29 +42,6 @@ internal class KtfmtBaseTaskTest {
         assertThat(underTest.outputs.files.toList()).isEqualTo(underTest.source.files.toList())
     }
 
-    @Test
-    fun `scope is not canceled before running`() {
-        val project = ProjectBuilder.builder().build()
-        project.pluginManager.apply("org.jetbrains.kotlin.jvm")
-        project.pluginManager.apply("com.ncorti.ktfmt.gradle")
-
-        val underTest = project.tasks.getByName("ktfmtFormatMain") as KtfmtBaseTask
-
-        assertThat(underTest.isScopeActive()).isTrue()
-    }
-
-    @Test
-    fun `scope is canceled after running`() {
-        val project = ProjectBuilder.builder().build()
-        project.pluginManager.apply("org.jetbrains.kotlin.jvm")
-        project.pluginManager.apply("com.ncorti.ktfmt.gradle")
-
-        val underTest = project.tasks.getByName("ktfmtFormatMain") as KtfmtBaseTask
-        underTest.taskAction()
-
-        assertThat(underTest.isScopeActive()).isFalse()
-    }
-
     @Test
     fun `processFile returns success for valid file`() {
         val underTest = project.tasks.getByName("ktfmtFormatMain") as KtfmtBaseTask
diff --git a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtCheckTaskIntegrationTest.kt b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtCheckTaskIntegrationTest.kt
index 02209026..386a1b98 100644
--- a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtCheckTaskIntegrationTest.kt
+++ b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtCheckTaskIntegrationTest.kt
@@ -137,6 +137,25 @@ internal class KtfmtCheckTaskIntegrationTest {
         assertThat(result.output).contains("[ktfmt] Valid formatting")
     }
 
+    @Test
+    fun `format task uses configuration cache correctly`() {
+        createTempFile(content = "val answer = 42\n")
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("--configuration-cache", "ktfmtCheckMain")
+            .build()
+
+        val result =
+            GradleRunner.create()
+                .withProjectDir(tempDir)
+                .withPluginClasspath()
+                .withArguments("--configuration-cache", "ktfmtCheckMain")
+                .build()
+
+        assertThat(result.output).contains("Reusing configuration cache.")
+    }
+
     @Test
     fun `check task validates all the file with a failure`() {
         createTempFile(content = "val answer = `\n", fileName = "File1.kt")
diff --git a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtFormatTaskIntegrationTest.kt b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtFormatTaskIntegrationTest.kt
index 18bdc4f5..f0236ad8 100644
--- a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtFormatTaskIntegrationTest.kt
+++ b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/tasks/KtfmtFormatTaskIntegrationTest.kt
@@ -163,6 +163,25 @@ internal class KtfmtFormatTaskIntegrationTest {
         assertThat(result.output).contains("[ktfmt] Successfully reformatted 1 files with Ktfmt")
     }
 
+    @Test
+    fun `format task uses configuration cache correctly`() {
+        createTempFile(content = "val answer = 42\n")
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("--configuration-cache", "ktfmtFormatMain")
+            .build()
+
+        val result =
+            GradleRunner.create()
+                .withProjectDir(tempDir)
+                .withPluginClasspath()
+                .withArguments("--configuration-cache", "ktfmtFormatMain")
+                .build()
+
+        assertThat(result.output).contains("Reusing configuration cache.")
+    }
+
     @Test
     fun `format task reformats all the file even with a failure`() {
         val file1 = createTempFile(content = "val answer = `", fileName = "File1.kt")