How to include input action in custom task

I have a custom task, in a custom plugin, constructed using the kotlin-dsl plugin.
Here is an example:

@CacheableTask
abstract class FooTask : DefaultTask(), CopySourceSpec {

    @get:InputFiles
    @get:Optional
    @get:SkipWhenEmpty
    @get:IgnoreEmptyDirectories
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val sourceFiles: ConfigurableFileCollection

    @get:Input
    @get:Optional
    abstract val renameFile: Property<Action<String>>

    @get:OutputDirectory
    @get:Optional
    abstract val targetDir: DirectoryProperty

    fun generate() {
        val sourceFileTree = sourceFiles.asFileTree

        val (outputDir, rawFilename) =
                if (targetDir.isPresent) {
                    Pair(targetDir.get(), input.name)
                } else {
                    Pair(project.layout.projectDirectory.dir(input.canonicalFile.parentFile.path),
                        input.canonicalFile.name)
                }
        val outputFileString = if (renameFile.isPresent) {
                var wipFilename = rawFilename.toString()
                renameFile.get().execute(wipFilename)
                wipFilename
            } else {
                rawFilename
            }
            val outputFile = outputDir.file(outputFileString).asFile

            outputFile.bufferedWriter().use { appendable ->
                appendable.append(result)
            }
     }

   // I do not think the following are relevant to the problem but I have included them for completeness.

    override fun from(vararg sourcePaths: Any?): ProtobufGenerateTask {
        this.sourceFiles.from(sourcePaths)
        return this
    }
    override fun from(sourcePath: Any, closure: Closure<*>): ProtobufGenerateTask {
        this.sourceFiles.from(sourcePath, closure)
        return this
    }
    override fun from(sourcePath: Any, configureAction: Action<in CopySpec>): ProtobufGenerateTask {
        this.sourceFiles.from(sourcePath, configureAction)
        return this
    }
    open fun  into(destPath: Directory?): ProtobufGenerateTask? {
        this.targetDir.set(destPath)
        return this
    }

    open fun  into(destPath: Provider<Directory>?): ProtobufGenerateTask? {
        if (destPath != null) {
            this.targetDir.set(destPath.get())
        }
        return this
    }
}

This task is then exercised with something in the build.gradle.kts that looks like this…


    fun renameSuffix(str: String, suffix: String) : String {
        return str
            .removeSuffix(".foo")
            .replace("\\.", "_")
            .plus(suffix)
    }

 tasks.register<FooTask>("generateFoo") {
            group = "gen-foo"
            from(layout.projectDirectory.dir("fooDir").asFileTree.matching {
                include("**/*.foo")
            })
            into(layout.buildDirectory.dir("fooDir"))
            renameFile.set { renameSuffix(this, "$1")  }
        }

The property of interest here is renameFile.set().
When I try to run generateFoo task…

./gradlew :generateFoo --debug --stacktrace
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] FAILURE: Build failed with an exception.
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
2022-10-06T14:55:55.325-0500 [ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * What went wrong:
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Build_gradle$5$7
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Build_gradle$5$7
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * Try:
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] > Run with --scan to get full insights.
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] 
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] * Exception is:
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] java.lang.NoClassDefFoundError: Build_gradle$5$7
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.logging.text.TreeFormatter.appendOuter(TreeFormatter.java:139)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.logging.text.TreeFormatter.appendType(TreeFormatter.java:117)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.snapshot.impl.AbstractValueProcessor.newValueSerializationException(AbstractValueProcessor.java:182)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.snapshot.impl.AbstractValueProcessor.javaSerialization(AbstractValueProcessor.java:174)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.snapshot.impl.AbstractValueProcessor.processValue(AbstractValueProcessor.java:153)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.snapshot.impl.DefaultValueSnapshotter.snapshot(DefaultValueSnapshotter.java:47)
...
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter] Caused by: java.lang.ClassNotFoundException: Build_gradle$5$7
[ERROR] [org.gradle.internal.buildevents.BuildExceptionReporter]   ... 72 more

This was working before I added the abstract val renameFile: Property<Action>.
What is the correct way to pass an Action as input to a custom task?

The following works as I wanted.

import org.gradle.api.Transformer
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.SourceTask
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.util.*

@CacheableTask
abstract class FooTask : SourceTask() {

    private val copyActions = LinkedList<Transformer<File, File>>()

    fun rename(renamer: Transformer<File, File>): FooTask {
        copyActions.add(renamer)
        return this
    }

    @TaskAction
    fun generate() {
        val sourceFileTree = this.source
        sourceFileTree.visit {
            val fileCopyDetails = this@visit
            copyActions.forEach { action ->
                val result = action.transform(fileCopyDetails.file)
                logger.quiet(" ${fileCopyDetails.path} -> $result")
            }
        }
    }
}

The usage in ‘build.gradle.kts’.

tasks {

    register<FooTask>("foo") {
        group = "foo"
        source(layout.projectDirectory.dir("foo").asFileTree.matching {
            include("**/*.foo")
        })
        rename { file ->
            file.parentFile.resolve( file.name.replace(Regex("(.*)\\.foo"), "$1.json")  )
        }
    }
}