Repository: cortinico/ktfmt-gradle (kotlin)

Issue:
Plugin not compatible with Gradle configuration cache
## 🐛 Describe the bug
<!-- A clear and concise description of what the bug is. -->
`project.extensions.getByType(KtfmtExtension::class.java).toBean()` in [KtfmtBaseTask](https://github.com/cortinico/ktfmt-gradle/blob/main/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/tasks/KtfmtBaseTask.kt#L76) invocation is unsupported with [Gradle configuration caching](https://docs.gradle.org/current/userguide/configuration_cache.html) due to call to `project`



## ⚠️ Current behavior
<!-- A clear and concise description of what you expected to happen. -->

This gets triggered when I register a new task:
```
tasks.register('formatKotlin', KtfmtFormatTask) {
    source = fileTree("../buildSrc") {
        subdirs.each { include("$it/src/**/*.kt") }
    }
}
```

and run my gradle task with Gradle configuration caching enabled. An error gets thrown:
```
1 problem was found storing the configuration cache.
- Task `:buildSrc-tests:formatKotlin` of type `com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask`: invocation of 'Task.project' at execution time is unsupported.
  See https://docs.gradle.org/8.8-rc-1/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution
```


Full stack trace: https://ge.androidx.dev/s/nkeb2gxeap5tg/failure?expanded-stacktrace=WyIwLTEiXQ#1

If you expand the stack trace, you can see it is coming from the line I linked to above

## ✅ Expected behavior
<!-- A clear and concise description of what you expected to happen. -->

The task is cacheable. I can see a TODO over the problematic line of code that should resolve the issue.



## 💣 Steps to reproduce
<!-- How we can reproduce the behavior: -->

Register a new task and run it with Gradle configuration caching enabled

## 📷 Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->

## 📱 Tech info
 - Device: <!-- e.g. Nexus One -->
 - OS: <!-- e.g. 7.1.1 -->
 - Library/App version: <!-- e.g. 1.0.0 --> AndroidX

Pull request:
<!-- Thanks for taking the time to write this Pull Request ❤️ -->

## 🚀 Description
<!-- Describe your changes in detail -->
Provide the formatting options as property

## 📄 Motivation and Context
<!-- Why is this change required? What problem does it solve? -->
<!-- If it fixes an open issue, please link to the issue here. -->
https://github.com/cortinico/ktfmt-gradle/issues/290

## 🧪 How Has This Been Tested?
<!-- Please describe in detail how you tested your changes. -->
<!-- Include details of your testing environment, tests ran to see how -->
<!-- your change affects other areas of the code, etc. -->
Added integration tests.

## 📦 Types of changes
<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## ✅ Checklist
<!-- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c0b491a..4f9b5978 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,9 +5,7 @@ Please add your entries according to this format.
 
 ## Unreleased
 
-- Update formatting of `build.gradle.kts` with ktfmt 0.51
-- Replace deprecated methods in `build.gradle.kts` files
-- Prepare for Gradle 8.9
+- Fix custom KtFmt tasks not compatible with configuration cache (#290)
 
 ## Version 0.19.0 _(2024-07-03)_
 
@@ -173,4 +171,4 @@ Please add your entries according to this format.
 
 ## Version 0.1.0 _(2020-12-22)_
 
-That's the first version of `ktfmt-gradle`
+That's the first version of `ktfmt-gradle`
\ No newline at end of file
diff --git a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtAndroidUtils.kt b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtAndroidUtils.kt
index b625a897..b6ccdbcc 100644
--- a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtAndroidUtils.kt
+++ b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtAndroidUtils.kt
@@ -12,7 +12,6 @@ internal object KtfmtAndroidUtils {
 
     internal fun applyKtfmtToAndroidProject(
         project: Project,
-        ktfmtExtension: KtfmtExtension,
         topLevelFormat: TaskProvider<Task>,
         topLevelCheck: TaskProvider<Task>,
         isKmpProject: Boolean = false
@@ -44,7 +43,6 @@ internal object KtfmtAndroidUtils {
                         project,
                         sourceSetName,
                         project.files(Callable { srcDirs }),
-                        ktfmtExtension,
                         topLevelFormat,
                         topLevelCheck,
                     )
diff --git a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtExtension.kt b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtExtension.kt
index 9ebdb000..c9918f8d 100644
--- a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtExtension.kt
+++ b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtExtension.kt
@@ -77,7 +77,7 @@ abstract class KtfmtExtension {
         continuationIndent.set(4)
     }
 
-    internal fun toBean(): FormattingOptionsBean =
+    internal fun toFormattingOptions(): FormattingOptionsBean =
         FormattingOptionsBean(
             maxWidth = maxWidth.get(),
             blockIndent = blockIndent.get(),
diff --git a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPlugin.kt b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPlugin.kt
index aee1d3a3..71b75e74 100644
--- a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPlugin.kt
+++ b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPlugin.kt
@@ -26,50 +26,49 @@ abstract class KtfmtPlugin : Plugin<Project> {
     private lateinit var topLevelFormat: TaskProvider<Task>
     private lateinit var topLevelCheck: TaskProvider<Task>
 
-    override fun apply(project: Project) =
-        project.run {
-            ktfmtExtension = project.extensions.create(EXTENSION_NAME, KtfmtExtension::class.java)
-
-            // setup to pull in ktfmt separately to run on an isolated classloader
-            val ktFmt =
-                configurations.create("ktfmt").apply {
-                    attributes.apply {
-                        attribute(
-                            Usage.USAGE_ATTRIBUTE,
-                            project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
-                    }
-                    isVisible = false
-                    isCanBeConsumed = false
+    override fun apply(project: Project) {
+        ktfmtExtension = project.extensions.create(EXTENSION_NAME, KtfmtExtension::class.java)
+
+        // setup to pull in ktfmt separately to run on an isolated classloader
+        val ktFmt =
+            project.configurations.create("ktfmt").apply {
+                attributes.apply {
+                    attribute(
+                        Usage.USAGE_ATTRIBUTE,
+                        project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME))
                 }
+                isVisible = false
+                isCanBeConsumed = false
+            }
 
-            val resourceUri =
-                KtfmtPlugin::class.java.getResource("ktfmt-version.txt")
-                    ?: error("Missing ktfmt version")
-            val ktfmtVersion = resources.text.fromUri(resourceUri).asString()
+        val resourceUri =
+            KtfmtPlugin::class.java.getResource("ktfmt-version.txt")
+                ?: error("Missing ktfmt version")
+        val ktfmtVersion = project.resources.text.fromUri(resourceUri).asString()
 
-            project.dependencies.add(ktFmt.name, ktfmtVersion)
+        project.dependencies.add(ktFmt.name, ktfmtVersion)
 
-            project.tasks.withType(KtfmtBaseTask::class.java).configureEach {
-                it.ktfmtClasspath.from(ktFmt)
-            }
+        project.tasks.withType(KtfmtBaseTask::class.java).configureEach {
+            it.ktfmtClasspath.from(ktFmt)
+            it.formattingOptionsBean.set(ktfmtExtension.toFormattingOptions())
+        }
 
-            topLevelFormat = createTopLevelFormatTask(project)
-            topLevelCheck = createTopLevelCheckTask(project)
+        topLevelFormat = createTopLevelFormatTask(project)
+        topLevelCheck = createTopLevelCheckTask(project)
 
-            project.plugins.withId("kotlin") { applyKtfmt(project) }
-            project.plugins.withId("kotlin-android") {
-                if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
-                    project.logger.i("Skipping Android task creation, as KMP is applied")
-                } else {
-                    applyKtfmtToAndroidProject(
-                        project, ktfmtExtension, topLevelFormat, topLevelCheck)
-                }
-            }
-            project.plugins.withId("org.jetbrains.kotlin.js") { applyKtfmt(project) }
-            project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
-                applyKtfmtToMultiplatformProject(project)
+        project.plugins.withId("kotlin") { applyKtfmt(project) }
+        project.plugins.withId("kotlin-android") {
+            if (project.plugins.hasPlugin("org.jetbrains.kotlin.multiplatform")) {
+                project.logger.i("Skipping Android task creation, as KMP is applied")
+            } else {
+                applyKtfmtToAndroidProject(project, topLevelFormat, topLevelCheck)
             }
         }
+        project.plugins.withId("org.jetbrains.kotlin.js") { applyKtfmt(project) }
+        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
+            applyKtfmtToMultiplatformProject(project)
+        }
+    }
 
     private fun applyKtfmt(project: Project) {
         val extension = project.extensions.getByType(KotlinProjectExtension::class.java)
@@ -78,9 +77,9 @@ abstract class KtfmtPlugin : Plugin<Project> {
                 project,
                 it.name,
                 it.kotlin.sourceDirectories,
-                ktfmtExtension,
                 topLevelFormat,
-                topLevelCheck)
+                topLevelCheck,
+            )
         }
     }
 
@@ -97,7 +96,6 @@ abstract class KtfmtPlugin : Plugin<Project> {
                 project,
                 name,
                 it.kotlin.sourceDirectories,
-                ktfmtExtension,
                 topLevelFormat,
                 topLevelCheck,
             )
@@ -106,7 +104,7 @@ abstract class KtfmtPlugin : Plugin<Project> {
         extension.targets.all { kotlinTarget ->
             if (kotlinTarget.platformType == KotlinPlatformType.androidJvm) {
                 applyKtfmtToAndroidProject(
-                    project, ktfmtExtension, topLevelFormat, topLevelCheck, isKmpProject = true)
+                    project, topLevelFormat, topLevelCheck, isKmpProject = true)
             }
         }
     }
diff --git a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPluginUtils.kt b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPluginUtils.kt
index 69ebc578..778e77a7 100644
--- a/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPluginUtils.kt
+++ b/plugin-build/plugin/src/main/java/com/ncorti/ktfmt/gradle/KtfmtPluginUtils.kt
@@ -34,7 +34,6 @@ internal object KtfmtPluginUtils {
         project: Project,
         srcSetName: String,
         srcSetDir: FileCollection,
-        ktfmtExtension: KtfmtExtension,
         topLevelFormat: TaskProvider<Task>,
         topLevelCheck: TaskProvider<Task>
     ) {
@@ -42,8 +41,8 @@ internal object KtfmtPluginUtils {
             return
         }
 
-        val srcCheckTask = createCheckTask(project, ktfmtExtension, srcSetName, srcSetDir)
-        val srcFormatTask = createFormatTask(project, ktfmtExtension, srcSetName, srcSetDir)
+        val srcCheckTask = createCheckTask(project, srcSetName, srcSetDir)
+        val srcFormatTask = createFormatTask(project, srcSetName, srcSetDir)
 
         // When running together with compileKotlin, ktfmt tasks should have precedence as
         // they're editing the source code
@@ -63,7 +62,6 @@ internal object KtfmtPluginUtils {
 
     private fun createCheckTask(
         project: Project,
-        ktfmtExtension: KtfmtExtension,
         name: String,
         srcDir: FileCollection
     ): TaskProvider<KtfmtCheckTask> {
@@ -86,13 +84,11 @@ internal object KtfmtPluginUtils {
             it.setSource(inputDirs)
             it.setIncludes(KtfmtPlugin.defaultIncludes)
             it.setExcludes(KtfmtPlugin.defaultExcludes)
-            it.bean = ktfmtExtension.toBean()
         }
     }
 
     private fun createFormatTask(
         project: Project,
-        ktfmtExtension: KtfmtExtension,
         name: String,
         srcDir: FileCollection
     ): TaskProvider<KtfmtFormatTask> {
@@ -115,7 +111,6 @@ internal object KtfmtPluginUtils {
             it.setSource(inputDirs)
             it.setIncludes(KtfmtPlugin.defaultIncludes)
             it.setExcludes(KtfmtPlugin.defaultExcludes)
-            it.bean = ktfmtExtension.toBean()
         }
     }
 }
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 c6488ec4..3806a747 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
@@ -1,14 +1,12 @@
 package com.ncorti.ktfmt.gradle.tasks
 
 import com.ncorti.ktfmt.gradle.FormattingOptionsBean
-import com.ncorti.ktfmt.gradle.KtfmtExtension
 import com.ncorti.ktfmt.gradle.KtfmtPlugin
 import com.ncorti.ktfmt.gradle.tasks.worker.KtfmtWorkAction
 import com.ncorti.ktfmt.gradle.tasks.worker.Result
 import com.ncorti.ktfmt.gradle.util.d
 import java.io.File
 import java.util.*
-import org.gradle.api.UnknownDomainObjectException
 import org.gradle.api.file.ConfigurableFileCollection
 import org.gradle.api.file.FileCollection
 import org.gradle.api.file.ProjectLayout
@@ -19,7 +17,6 @@ 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
 import org.gradle.api.tasks.SourceTask
@@ -42,7 +39,7 @@ internal constructor(
 
     @get:Classpath @get:InputFiles internal abstract val ktfmtClasspath: ConfigurableFileCollection
 
-    @get:Input @get:Optional internal var bean: FormattingOptionsBean? = null
+    @get:Input internal abstract val formattingOptionsBean: Property<FormattingOptionsBean>
 
     @get:Option(
         option = "include-only",
@@ -67,19 +64,6 @@ internal constructor(
 
     protected abstract fun execute(workQueue: WorkQueue)
 
-    private fun formattingOptions(): FormattingOptionsBean {
-        if (bean == null) {
-            // TODO - set this as property / convention
-            bean =
-                try {
-                    project.extensions.getByType(KtfmtExtension::class.java).toBean()
-                } catch (ignored: UnknownDomainObjectException) {
-                    FormattingOptionsBean()
-                }
-        }
-        return bean!!
-    }
-
     @get:Internal
     internal val defaultExcludesFilter: Spec<File> =
         Spec<File> {
@@ -104,7 +88,7 @@ internal constructor(
             forEach {
                 queue.submit(action) { parameters ->
                     parameters.sourceFile.set(it)
-                    parameters.formattingOptions.set(formattingOptions())
+                    parameters.formattingOptions.set(formattingOptionsBean.get())
                     parameters.includedFiles.set(includedFiles)
                     parameters.projectDir.set(layout.projectDirectory)
                     parameters.workingDir.set(workingDir)


Test patch:
diff --git a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/KtfmtExtensionTest.kt b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/KtfmtExtensionTest.kt
index 7e102aff..f8611296 100644
--- a/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/KtfmtExtensionTest.kt
+++ b/plugin-build/plugin/src/test/java/com/ncorti/ktfmt/gradle/KtfmtExtensionTest.kt
@@ -68,7 +68,7 @@ class KtfmtExtensionTest {
     }
 
     @Test
-    fun `toBean copies fields correctly`() {
+    fun `toFormattingOptions copies fields correctly`() {
         val extension = createExtension()
 
         extension.maxWidth.set(42)
@@ -78,7 +78,7 @@ class KtfmtExtensionTest {
         extension.manageTrailingCommas.set(true)
         extension.debuggingPrintOpsAfterFormatting.set(true)
 
-        val bean = extension.toBean()
+        val bean = extension.toFormattingOptions()
 
         assertThat(bean.maxWidth).isEqualTo(42)
         assertThat(bean.blockIndent).isEqualTo(43)
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 b649582d..3b7b9383 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
@@ -257,6 +257,28 @@ internal class KtfmtCheckTaskIntegrationTest {
         assertThat(result!!.output).contains("Reusing configuration cache.")
     }
 
+    @Test
+    fun `custom formatCheck task should be compatible with configuration cache`() {
+        createTempFile(content = "val answer = 42\n")
+
+        tempDir
+            .resolve("build.gradle.kts")
+            .appendText(
+                """
+                |
+                |tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtCheckTask>("customFormatCheck") {
+                |   source = fileTree("src/main/java")
+                |}
+                """
+                    .trimMargin())
+
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("customFormatCheck", "--configuration-cache")
+            .build()
+    }
+
     private fun createTempFile(
         @Language("kotlin") content: String,
         fileName: String = "TestFile.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 5d770f26..fd165359 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
@@ -258,6 +258,91 @@ internal class KtfmtFormatTaskIntegrationTest {
         assertThat(result.task(":ktfmtFormatMain")?.outcome).isEqualTo(SUCCESS)
     }
 
+    @Test
+    fun `custom format task should be compatible with configuration cache`() {
+        createTempFile(content = "val answer = 42\n")
+
+        tempDir
+            .resolve("build.gradle.kts")
+            .appendText(
+                """
+                |
+                |tasks.register<com.ncorti.ktfmt.gradle.tasks.KtfmtFormatTask>("customFormatTask") {
+                |   source = fileTree("src/main/java")
+                |}
+                """
+                    .trimMargin())
+
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("customFormatTask", "--configuration-cache")
+            .build()
+    }
+
+    @Test
+    fun `should format the files in kotlinLang style with a 4 space indentation`() {
+        val file =
+            createTempFile(
+                content =
+                    """
+                    |fun someFun(){
+                    |println("Hello, World!")
+                    |println("HelloWorld2")
+                    |}
+                    """
+                        .trimMargin())
+
+        appendToBuildGradle("ktfmt { kotlinLangStyle() }")
+
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("ktfmtFormatMain")
+            .forwardOutput()
+            .build()
+
+        val actual = file.readLines()
+        assertThat(actual)
+            .containsExactly(
+                "fun someFun() {",
+                "    println(\"Hello, World!\")",
+                "    println(\"HelloWorld2\")",
+                "}")
+    }
+
+    @Test
+    fun `should format the files in googleStyle style with a 2 space indentation`() {
+        val file =
+            createTempFile(
+                content =
+                    """
+                    |fun someFun(){
+                    |println("Hello, World!")
+                    |println("HelloWorld2")
+                    |}
+                    """
+                        .trimMargin())
+
+        appendToBuildGradle("ktfmt { googleStyle() }")
+
+        GradleRunner.create()
+            .withProjectDir(tempDir)
+            .withPluginClasspath()
+            .withArguments("ktfmtFormatMain")
+            .forwardOutput()
+            .build()
+
+        val actual = file.readLines()
+
+        assertThat(actual)
+            .containsExactly(
+                "fun someFun() {",
+                "  println(\"Hello, World!\")",
+                "  println(\"HelloWorld2\")",
+                "}")
+    }
+
     private fun createTempFile(
         @Language("kotlin") content: String,
         fileName: String = "TestFile.kt",
@@ -267,4 +352,11 @@ internal class KtfmtFormatTaskIntegrationTest {
             createNewFile()
             writeText(content)
         }
+
+    private fun appendToBuildGradle(content: String) {
+        tempDir.resolve("build.gradle.kts").apply {
+            appendText(System.lineSeparator())
+            appendText(content)
+        }
+    }
 }