rewrite icon indicating copy to clipboard operation
rewrite copied to clipboard

Kotlin Recipe Declarative String Arguments Instantiate as Empty String

Open ryan-hudson opened this issue 7 months ago • 1 comments

As of OpenRewrite 8.50.0, String options passed declaratively into Kotlin recipes are being instantiated as "".

Debugging a bit, it looks like this issue was caused by this change to the Recipe Loader, which is now delegating recipe instantiation to RecipeIntrospectionUtils.constructRecipe, which sets Kotlin String arguments to "" here. That logic appears to have been added purely for validation, and was not intended to instantiate recipe args.

  • See https://github.com/openrewrite/rewrite/issues/3508

What version of OpenRewrite are you using?

8.54.0

How are you running OpenRewrite?

Unit Tests in private repo.

What is the smallest, simplest way to reproduce the problem?

Declarative Yaml

type: specs.openrewrite.org/v1beta/recipe
name: net.sample.kotlin.KotlinReplaceAllRecipeWrapper
displayName: Kotlin Replace All Wrapper
description: Tests a simple Kotlin recipe that replaces all text with the provided option.
recipeList:
  - net.sample.kotlin.KotlinReplaceAllRecipe:
      replacementText: "REPLACEMENT_TEXT"

Imperative Kotlin Recipe

package net.sample.kotlin

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import org.openrewrite.*
import org.openrewrite.text.PlainText
import org.openrewrite.text.PlainTextVisitor


class KotlinReplaceAllRecipe @JsonCreator constructor(
  @Option(
    displayName = "Replacement Text",
    description = "Resulting text after the replacement occurs."
  ) @JsonProperty("replacementText") val replacementText: String
): Recipe() {
  override fun getDisplayName() = "Tests a simple Kotlin Recipe With Options"
  override fun getDescription() = "A test case for recipe authoring in Kotlin."

  override fun getVisitor(): TreeVisitor<*, ExecutionContext> =
    object: PlainTextVisitor<ExecutionContext>() {
      override fun visitText(text: PlainText, p: ExecutionContext): PlainText {
        return super.visitText(text.withText(replacementText), p)
      }
    }
}

Recipe Test

package net.sample.kotlin

import org.junit.jupiter.api.Test
import org.openrewrite.test.RecipeSpec
import org.openrewrite.test.RewriteTest
import org.openrewrite.test.SourceSpecs.text

class KotlinReplaceAllRecipeTest: RewriteTest {
  override fun defaults(spec: RecipeSpec) {
    spec.recipeFromResources("net.sample.kotlin.KotlinReplaceAllRecipeWrapper")
  }

  @Test
  fun `replaces all text with provided replacement argument text`() = rewriteRun(text("Some Text", "REPLACEMENT_TEXT"))
}

Test Output

Expected :"REPLACEMENT_TEXT"
Actual   :""

What did you expect to see?

"REPLACEMENT_TEXT"

What did you see instead?

""

ryan-hudson avatar Jun 02 '25 21:06 ryan-hudson

@knutwannheden any insights here given your work on

  • https://github.com/openrewrite/rewrite/pull/5234 ?

https://github.com/openrewrite/rewrite/blob/d7bb97c1369a8a3bbfdf7de8ad0ec841044c3f1b/rewrite-core/src/main/java/org/openrewrite/internal/RecipeIntrospectionUtils.java#L123-L127

timtebeek avatar Jun 02 '25 21:06 timtebeek