Skip to content

Commit 30a85e1

Browse files
authored
Kotlin: declare caller-scope type variables on synthetic Template class (#7414)
* Kotlin: declare caller-scope type variables on synthetic Template class The Kotlin `BlockStatementTemplateGenerator` ignored the `typeVariables` collected from parameter types, emitting a bare `class Template {`. When a template parameter's type referenced a caller-scope generic (e.g. `Container<T>` from an enclosing `fun <T : Any> ...`), the substitution produced `__P__.p<Container<T>>()` inside a class where `T` was undeclared. Now the Kotlin generator declares those type variables using Kotlin syntax (`T : Bound` for a single bound, `where` clause for multiple). Fixes #7407 * Kotlin: extract type parameter string helper to KotlinTypeUtils * Kotlin: inline type parameter helper into template generator
1 parent 4bef3d1 commit 30a85e1

2 files changed

Lines changed: 87 additions & 6 deletions

File tree

rewrite-kotlin/src/main/java/org/openrewrite/kotlin/internal/template/KotlinBlockStatementTemplateGenerator.java

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
import org.openrewrite.java.tree.J;
2222
import org.openrewrite.java.tree.JavaType;
2323
import org.openrewrite.java.tree.Statement;
24+
import org.openrewrite.java.tree.TypeUtils;
2425
import org.openrewrite.kotlin.tree.K;
2526

2627
import java.util.Collection;
28+
import java.util.List;
2729
import java.util.Set;
2830

2931
public class KotlinBlockStatementTemplateGenerator extends BlockStatementTemplateGenerator {
@@ -33,16 +35,17 @@ public KotlinBlockStatementTemplateGenerator(Set<String> imports, boolean contex
3335

3436
@Override
3537
protected void contextFreeTemplate(Cursor cursor, J j, Collection<JavaType.GenericTypeVariable> typeVariables, StringBuilder before, StringBuilder after) {
38+
String classDeclaration = "class Template" + kotlinTypeParameters(typeVariables);
3639
if (j instanceof Expression && !(j instanceof J.Assignment)) {
37-
before.insert(0, "class Template {\n");
40+
before.insert(0, classDeclaration + " {\n");
3841
before.append("var o : Any = ");
3942
after.append(";\n}");
4043
} else if (j instanceof J.ClassDeclaration || j instanceof K.ClassDeclaration) {
4144
throw new IllegalArgumentException(
4245
"Templating a class declaration requires context from which package declaration and imports may be reached. " +
4346
"Mark this template as context-sensitive by calling KotlinTemplate.Builder#contextSensitive().");
4447
} else if (j instanceof Statement && !(j instanceof J.Import) && !(j instanceof J.Package)) {
45-
before.insert(0, "class Template {\ninit {\n");
48+
before.insert(0, classDeclaration + " {\ninit {\n");
4649
after.append("\n}\n}");
4750
} else {
4851
throw new IllegalArgumentException(
@@ -54,4 +57,44 @@ protected void contextFreeTemplate(Cursor cursor, J j, Collection<JavaType.Gener
5457
before.insert(0, anImport);
5558
}
5659
}
60+
61+
private static String kotlinTypeParameters(Collection<JavaType.GenericTypeVariable> typeVariables) {
62+
if (typeVariables.isEmpty()) {
63+
return "";
64+
}
65+
StringBuilder params = new StringBuilder("<");
66+
StringBuilder where = new StringBuilder();
67+
boolean firstParam = true;
68+
for (JavaType.GenericTypeVariable tv : typeVariables) {
69+
if ("?".equals(tv.getName())) {
70+
continue;
71+
}
72+
if (!firstParam) {
73+
params.append(", ");
74+
}
75+
firstParam = false;
76+
params.append(tv.getName());
77+
List<JavaType> bounds = tv.getBounds();
78+
if (tv.getVariance() == JavaType.GenericTypeVariable.Variance.COVARIANT && !bounds.isEmpty()) {
79+
if (bounds.size() == 1) {
80+
params.append(" : ").append(TypeUtils.toString(bounds.get(0)));
81+
} else {
82+
for (JavaType bound : bounds) {
83+
if (where.length() > 0) {
84+
where.append(", ");
85+
}
86+
where.append(tv.getName()).append(" : ").append(TypeUtils.toString(bound));
87+
}
88+
}
89+
}
90+
}
91+
if (firstParam) {
92+
return "";
93+
}
94+
params.append(">");
95+
if (where.length() > 0) {
96+
params.append(" where ").append(where);
97+
}
98+
return params.toString();
99+
}
57100
}

rewrite-kotlin/src/test/java/org/openrewrite/kotlin/KotlinTemplateTest.java

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
import org.junit.jupiter.api.Test;
2020
import org.openrewrite.DocumentExample;
2121
import org.openrewrite.ExecutionContext;
22-
import org.openrewrite.java.tree.J;
23-
import org.openrewrite.test.RewriteTest;
24-
22+
import org.openrewrite.Issue;
2523
import org.openrewrite.java.JavaTemplate;
24+
import org.openrewrite.java.tree.J;
2625
import org.openrewrite.java.tree.Statement;
26+
import org.openrewrite.test.RewriteTest;
2727
import org.openrewrite.test.TypeValidation;
2828

29+
import static org.assertj.core.api.Assertions.assertThat;
30+
2931
import java.net.URISyntaxException;
3032
import java.nio.file.Path;
3133
import java.util.List;
@@ -170,7 +172,7 @@ fun foo() {
170172
""",
171173
"""
172174
import com.fasterxml.jackson.databind.ObjectMapper
173-
175+
174176
class Test {
175177
fun foo() {
176178
val mapper = ObjectMapper()
@@ -179,4 +181,40 @@ fun foo() {
179181
"""
180182
));
181183
}
184+
185+
@Issue("https://github.com/openrewrite/rewrite/issues/7407")
186+
@Test
187+
void parameterTypeWithCallerScopeTypeVariable() {
188+
StringBuilder capturedTemplate = new StringBuilder();
189+
rewriteRun(
190+
spec -> spec.typeValidationOptions(TypeValidation.builder().methodInvocations(false).build()).recipe(toRecipe(() -> new KotlinVisitor<>() {
191+
@Override
192+
public J visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
193+
if (multiVariable.getVariables().size() == 1 &&
194+
"x".equals(multiVariable.getVariables().getFirst().getSimpleName())) {
195+
J initializer = multiVariable.getVariables().getFirst().getInitializer();
196+
return KotlinTemplate.builder("println(#{any()})")
197+
.doBeforeParseTemplate(capturedTemplate::append)
198+
.build()
199+
.apply(getCursor(), multiVariable.getCoordinates().replace(), initializer);
200+
}
201+
return super.visitVariableDeclarations(multiVariable, ctx);
202+
}
203+
}).withMaxCycles(1)),
204+
kotlin(
205+
"""
206+
class Container<T : Any>(val value: T)
207+
fun <T : Any> test(c: Container<T>) {
208+
val x = c
209+
}
210+
""",
211+
"""
212+
class Container<T : Any>(val value: T)
213+
fun <T : Any> test(c: Container<T>) {
214+
println(c)
215+
}
216+
"""
217+
));
218+
assertThat(capturedTemplate.toString()).contains("class Template<T");
219+
}
182220
}

0 commit comments

Comments
 (0)