Skip to content

Commit 7e486bf

Browse files
authored
Allow direct Kotlin recipe instantiation in tests (#6578)
1 parent e70fd6c commit 7e486bf

2 files changed

Lines changed: 79 additions & 33 deletions

File tree

rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinFromDeclarativeRecipeTest.kt renamed to rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinRecipeTest.kt

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,49 @@ import org.openrewrite.ExecutionContext
2222
import org.openrewrite.Option
2323
import org.openrewrite.Recipe
2424
import org.openrewrite.TreeVisitor
25-
import org.openrewrite.test.RecipeSpec
2625
import org.openrewrite.test.RewriteTest
2726
import org.openrewrite.test.SourceSpecs.text
2827
import org.openrewrite.text.PlainText
2928
import org.openrewrite.text.PlainTextVisitor
3029

30+
class KotlinRecipeTest : RewriteTest {
3131

32-
class KotlinFromDeclarativeRecipeTest : RewriteTest {
33-
override fun defaults(spec: RecipeSpec) {
34-
spec.recipeFromYaml(
35-
"""
36-
type: specs.openrewrite.org/v1beta/recipe
37-
name: org.openrewrite.kotlin.KotlinReplaceAllRecipeWrapper
38-
displayName: Kotlin Replace All Wrapper
39-
description: Tests a simple Kotlin recipe that replaces all text with the provided option.
40-
recipeList:
41-
- org.openrewrite.kotlin.KotlinReplaceAllRecipeJackson:
42-
replacementText: "REPLACED_WITH_JACKSON"
43-
- org.openrewrite.kotlin.KotlinReplaceAllRecipe:
44-
replacementText: "REPLACED_WITH_KOTLIN"
45-
""", "org.openrewrite.kotlin.KotlinReplaceAllRecipeWrapper"
32+
@Test
33+
fun `replaces all text with provided replacement argument text`() =
34+
rewriteRun(
35+
{ spec ->
36+
spec.cycles(1)
37+
.expectedCyclesThatMakeChanges(1)
38+
.recipeFromYaml(
39+
"""
40+
type: specs.openrewrite.org/v1beta/recipe
41+
name: org.openrewrite.kotlin.KotlinReplaceAllRecipeWrapper
42+
displayName: Kotlin Replace All Wrapper
43+
description: Tests a simple Kotlin recipe that replaces all text with the provided option.
44+
recipeList:
45+
- org.openrewrite.kotlin.KotlinReplaceAllRecipeJackson:
46+
replacementText: "REPLACED_WITH_JACKSON"
47+
- org.openrewrite.kotlin.KotlinReplaceAllRecipe:
48+
replacementText: "REPLACED_WITH_KOTLIN"
49+
""",
50+
"org.openrewrite.kotlin.KotlinReplaceAllRecipeWrapper"
51+
)
52+
},
53+
text("Some Text", "REPLACED_WITH_KOTLIN")
4654
)
47-
.cycles(1)
48-
.expectedCyclesThatMakeChanges(1)
49-
}
5055

56+
/**
57+
* This test directly uses a Kotlin recipe with required options (without the declarative wrapper).
58+
* Before the fix in RewriteTest.java, this test would fail because Jackson's Kotlin module
59+
* enforces non-nullability when trying to instantiate with null arguments via RecipeLoader.
60+
* Now the RecipeLoader null-instantiation validation is skipped for Kotlin recipes with required options.
61+
*/
5162
@Test
52-
fun `replaces all text with provided replacement argument text`() =
53-
rewriteRun(text("Some Text", "REPLACED_WITH_KOTLIN"))
63+
fun `directly uses kotlin recipe with required options`() =
64+
rewriteRun(
65+
{ spec -> spec.recipe(KotlinReplaceAllRecipe("DIRECT_REPLACEMENT")) },
66+
text("Some Text", "DIRECT_REPLACEMENT")
67+
)
5468
}
5569

5670
@Suppress("unused")

rewrite-test/src/main/java/org/openrewrite/test/RewriteTest.java

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import org.openrewrite.tree.ParseError;
3030
import org.openrewrite.tree.ParsingExecutionContextView;
3131

32+
import java.lang.annotation.Annotation;
33+
import java.lang.reflect.Field;
3234
import java.nio.charset.StandardCharsets;
3335
import java.nio.file.Path;
3436
import java.util.*;
@@ -174,19 +176,25 @@ default void rewriteRun(Consumer<RecipeSpec> spec, SourceSpec<?>... sourceSpecs)
174176
assertThat(recipeSerializer.read(recipeSerializer.write(recipe)))
175177
.as("Recipe must be serializable/deserializable")
176178
.isEqualTo(recipe);
177-
assertThatCode(() -> {
178-
Recipe r = new RecipeLoader(recipe.getClass().getClassLoader())
179-
.load(recipe.getClass(), null);
180-
// getRecipeList should not fail with default parameters from RecipeLoader.
181-
r.getRecipeList();
182-
// We add recipes to HashSet in some places, we need to validate that hashCode and equals does not fail.
183-
//noinspection ResultOfMethodCallIgnored
184-
r.hashCode();
185-
//noinspection EqualsWithItself,ResultOfMethodCallIgnored
186-
r.equals(r);
187-
})
188-
.as("Recipe must be able to instantiate via RecipeLoader")
189-
.doesNotThrowAnyException();
179+
// Skip RecipeLoader null-instantiation test for Kotlin recipes with required options,
180+
// as Jackson's Kotlin module enforces non-nullability and will fail when trying to
181+
// instantiate with null arguments. The serialization round-trip test above still
182+
// validates that actual recipe instances work correctly.
183+
if (!RewriteTestUtils.isKotlinRecipeWithRequiredOptions(recipe.getClass())) {
184+
assertThatCode(() -> {
185+
Recipe r = new RecipeLoader(recipe.getClass().getClassLoader())
186+
.load(recipe.getClass(), null);
187+
// getRecipeList should not fail with default parameters from RecipeLoader.
188+
r.getRecipeList();
189+
// We add recipes to HashSet in some places, we need to validate that hashCode and equals does not fail.
190+
//noinspection ResultOfMethodCallIgnored
191+
r.hashCode();
192+
//noinspection EqualsWithItself,ResultOfMethodCallIgnored
193+
r.equals(r);
194+
})
195+
.as("Recipe must be able to instantiate via RecipeLoader")
196+
.doesNotThrowAnyException();
197+
}
190198
validateRecipeNameAndDescription(recipe);
191199
validateRecipeOptions(recipe);
192200
}
@@ -709,6 +717,30 @@ default void validateRecipeOptions(Recipe recipe) {
709717
}
710718

711719
class RewriteTestUtils {
720+
721+
/**
722+
* Checks if a Kotlin recipe class has required options that will fail when
723+
* instantiated with null arguments via Jackson. This is because Jackson's
724+
* Kotlin module enforces non-nullability for Kotlin classes.
725+
* <p>
726+
* This check only applies to Kotlin classes since Java classes can have
727+
* null values for any parameter type.
728+
*/
729+
static boolean isKotlinRecipeWithRequiredOptions(Class<?> recipeClass) {
730+
for (Annotation a : recipeClass.getDeclaredAnnotations()) {
731+
if ("kotlin.Metadata".equals(a.annotationType().getName())) {
732+
// Check for @Option fields with required=true (which is the default)
733+
for (Field field : recipeClass.getDeclaredFields()) {
734+
Option option = field.getAnnotation(Option.class);
735+
if (option != null && option.required()) {
736+
return true;
737+
}
738+
}
739+
}
740+
}
741+
return false;
742+
}
743+
712744
static boolean groupSourceSpecsByParser(List<Parser.Builder> parserBuilders, Map<Parser.Builder, List<SourceSpec<?>>> sourceSpecsByParser, SourceSpec<?> sourceSpec) {
713745
for (Map.Entry<Parser.Builder, List<SourceSpec<?>>> entry : sourceSpecsByParser.entrySet()) {
714746
if (entry.getKey().getSourceFileType().equals(sourceSpec.sourceFileType) && sourceSpec.getParser().getClass().isAssignableFrom(entry.getKey().getClass())) {

0 commit comments

Comments
 (0)