diff --git a/src/main/java/org/openrewrite/kotlin/KotlinParser.java b/src/main/java/org/openrewrite/kotlin/KotlinParser.java index 4e12f5142..0bae3fb48 100644 --- a/src/main/java/org/openrewrite/kotlin/KotlinParser.java +++ b/src/main/java/org/openrewrite/kotlin/KotlinParser.java @@ -286,6 +286,18 @@ public Builder classpathFromResources(ExecutionContext ctx, String... classpath) return this; } + /** + * This is an internal API which is subject to removal or change. + */ + public Builder addClasspathEntry(Path classpath) { + if (this.classpath.isEmpty()) { + this.classpath = Collections.singletonList(classpath); + } else { + this.classpath.add(classpath); + } + return this; + } + public Builder typeCache(JavaTypeCache typeCache) { this.typeCache = typeCache; return this; diff --git a/src/main/java/org/openrewrite/kotlin/KotlinTemplate.java b/src/main/java/org/openrewrite/kotlin/KotlinTemplate.java new file mode 100644 index 000000000..6972cc96c --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/KotlinTemplate.java @@ -0,0 +1,123 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin; + +import org.openrewrite.Cursor; +import org.openrewrite.internal.StringUtils; +import org.openrewrite.java.JavaTemplate; +import org.openrewrite.java.internal.template.Substitutions; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaCoordinates; +import org.openrewrite.kotlin.internal.template.KotlinSubstitutions; +import org.openrewrite.kotlin.internal.template.KotlinTemplateParser; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; + +public class KotlinTemplate extends JavaTemplate { + private KotlinTemplate(boolean contextSensitive, KotlinParser.Builder parser, String code, Set imports, Consumer onAfterVariableSubstitution, Consumer onBeforeParseTemplate) { + super( + code, + onAfterVariableSubstitution, + new KotlinTemplateParser( + contextSensitive, + augmentClasspath(parser), + onAfterVariableSubstitution, + onBeforeParseTemplate, + imports + ) + ); + } + + private static KotlinParser.Builder augmentClasspath(KotlinParser.Builder parserBuilder) { + return parserBuilder.addClasspathEntry(getTemplateClasspathDir()); + } + + @Override + protected Substitutions substitutions(Object[] parameters) { + return new KotlinSubstitutions(getCode(), parameters); + } + + public static J2 apply(String template, Cursor scope, JavaCoordinates coordinates, Object... parameters) { + return builder(template).build().apply(scope, coordinates, parameters); + } + + public static Builder builder(String code) { + return new Builder(code); + } + + public static boolean matches(String template, Cursor cursor) { + return builder(template).build().matches(cursor); + } + + @SuppressWarnings("unused") + public static class Builder extends JavaTemplate.Builder { + + private final String code; + private final Set imports = new HashSet<>(); + + private KotlinParser.Builder parser = KotlinParser.builder(); + + private Consumer onAfterVariableSubstitution = s -> { + }; + private Consumer onBeforeParseTemplate = s -> { + }; + + Builder(String code) { + super(code); + this.code = code; + } + + public Builder imports(String... fullyQualifiedTypeNames) { + for (String typeName : fullyQualifiedTypeNames) { + validateImport(typeName); + this.imports.add("import " + typeName + "\n"); + } + return this; + } + + private void validateImport(String typeName) { + if (StringUtils.isBlank(typeName)) { + throw new IllegalArgumentException("Imports must not be blank"); + } else if (typeName.startsWith("import ")) { + throw new IllegalArgumentException("Imports are expressed as fully-qualified names and should not include an \"import \" prefix"); + } else if (typeName.endsWith(";") || typeName.endsWith("\n")) { + throw new IllegalArgumentException("Imports are expressed as fully-qualified names and should not include a suffixed terminator"); + } + } + + Builder parser(KotlinParser.Builder parser) { + this.parser = parser; + return this; + } + + public Builder doAfterVariableSubstitution(Consumer afterVariableSubstitution) { + this.onAfterVariableSubstitution = afterVariableSubstitution; + return this; + } + + public Builder doBeforeParseTemplate(Consumer beforeParseTemplate) { + this.onBeforeParseTemplate = beforeParseTemplate; + return this; + } + + public KotlinTemplate build() { + return new KotlinTemplate(false, parser, code, imports, + onAfterVariableSubstitution, onBeforeParseTemplate); + } + } +} diff --git a/src/main/java/org/openrewrite/kotlin/cleanup/ReplaceCharToIntWithCode.java b/src/main/java/org/openrewrite/kotlin/cleanup/ReplaceCharToIntWithCode.java index e1d14814e..7e1fd2b5d 100644 --- a/src/main/java/org/openrewrite/kotlin/cleanup/ReplaceCharToIntWithCode.java +++ b/src/main/java/org/openrewrite/kotlin/cleanup/ReplaceCharToIntWithCode.java @@ -20,20 +20,16 @@ import org.openrewrite.ExecutionContext; import org.openrewrite.Recipe; import org.openrewrite.TreeVisitor; -import org.openrewrite.internal.lang.Nullable; import org.openrewrite.java.MethodMatcher; import org.openrewrite.java.tree.J; -import org.openrewrite.kotlin.KotlinParser; +import org.openrewrite.kotlin.KotlinTemplate; import org.openrewrite.kotlin.KotlinVisitor; -import org.openrewrite.kotlin.tree.K; @Value @EqualsAndHashCode(callSuper = false) public class ReplaceCharToIntWithCode extends Recipe { private static final MethodMatcher CHAR_TO_INT_METHOD_MATCHER = new MethodMatcher("kotlin.Char toInt()"); - @Nullable - private static J.FieldAccess charCodeTemplate; @Override public String getDisplayName() { @@ -51,29 +47,16 @@ public String getDescription() { public TreeVisitor getVisitor() { return new KotlinVisitor() { @Override - public J visitMethodInvocation(J.MethodInvocation method, - ExecutionContext ctx) { + public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) { if (CHAR_TO_INT_METHOD_MATCHER.matches(method) && method.getSelect() != null) { - J.FieldAccess codeTemplate = getCharCodeTemplate(); - return codeTemplate.withTarget(method.getSelect()).withPrefix(method.getPrefix()); + return KotlinTemplate.builder("#{any(Char)}.code") + .build() + .apply(getCursor(), method.getCoordinates().replace(), method.getSelect()) + .withPrefix(method.getPrefix()); } return super.visitMethodInvocation(method, ctx); } }; } - @SuppressWarnings("all") - private static J.FieldAccess getCharCodeTemplate() { - if (charCodeTemplate == null) { - K.CompilationUnit kcu = KotlinParser.builder().build() - .parse("fun method(c : Char) {c.code}") - .map(K.CompilationUnit.class::cast) - .findFirst() - .get(); - - charCodeTemplate = (J.FieldAccess) ((J.MethodDeclaration) kcu.getStatements().get(0)).getBody().getStatements().get(0); - } - - return charCodeTemplate; - } } diff --git a/src/main/java/org/openrewrite/kotlin/internal/template/KotlinBlockStatementTemplateGenerator.java b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinBlockStatementTemplateGenerator.java new file mode 100644 index 000000000..c2751243b --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinBlockStatementTemplateGenerator.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin.internal.template; + +import org.openrewrite.Cursor; +import org.openrewrite.java.internal.template.BlockStatementTemplateGenerator; +import org.openrewrite.java.tree.Expression; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaCoordinates; + +import java.util.Set; + +public class KotlinBlockStatementTemplateGenerator extends BlockStatementTemplateGenerator { + public KotlinBlockStatementTemplateGenerator(Set imports, boolean contextSensitive) { + super(imports, contextSensitive); + } + + @Override + protected void contextFreeTemplate(Cursor cursor, J j, StringBuilder before, StringBuilder after) { + if (!(j instanceof Expression)) { + throw new IllegalArgumentException( + "Kotlin templating is currently only implemented for context-free expressions and not for `" + j.getClass() + "` instances."); + } + + before.insert(0, "class Template {\n"); + before.append("var o : Object = "); + after.append(";"); + after.append("\n}"); + + before.insert(0, TEMPLATE_INTERNAL_IMPORTS); + for (String anImport : imports) { + before.insert(0, anImport); + } + } +} diff --git a/src/main/java/org/openrewrite/kotlin/internal/template/KotlinSubstitutions.java b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinSubstitutions.java new file mode 100644 index 000000000..4dd4bd8b1 --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinSubstitutions.java @@ -0,0 +1,58 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin.internal.template; + +import org.openrewrite.java.internal.template.Substitutions; +import org.openrewrite.java.tree.JavaType; + +public class KotlinSubstitutions extends Substitutions { + public KotlinSubstitutions(String code, Object[] parameters) { + super(code, parameters); + } + + @Override + protected String newObjectParameter(String fqn, int index) { + return "__P__./*__p" + index + "__*/p<" + fqn + ">()"; + } + + @Override + protected String newPrimitiveParameter(String fqn, int index) { + return newObjectParameter(fqn, index); + } + + @Override + protected String newArrayParameter(JavaType elemType, int dimensions, int index) { + // generate literal of the form: `arrayOf(arrayOf())` + StringBuilder builder = new StringBuilder("/*__p" + index + "__*/"); + for (int i = 0; i < dimensions; i++) { + builder.append("arrayOf"); + if (i < dimensions - 1) { + builder.append('('); + } + } + builder.append('<'); + if (elemType instanceof JavaType.Primitive) { + builder.append(((JavaType.Primitive) elemType).getKeyword()); + } else if (elemType instanceof JavaType.FullyQualified) { + builder.append(((JavaType.FullyQualified) elemType).getFullyQualifiedName().replace("$", ".")); + } + builder.append(">("); + for (int i = 0; i < dimensions; i++) { + builder.append(')'); + } + return builder.toString(); + } +} diff --git a/src/main/java/org/openrewrite/kotlin/internal/template/KotlinTemplateParser.java b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinTemplateParser.java new file mode 100644 index 000000000..763667e2c --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/internal/template/KotlinTemplateParser.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin.internal.template; + +import org.openrewrite.java.internal.template.AnnotationTemplateGenerator; +import org.openrewrite.java.internal.template.JavaTemplateParser; +import org.openrewrite.kotlin.KotlinParser; + +import java.util.Set; +import java.util.function.Consumer; + +public class KotlinTemplateParser extends JavaTemplateParser { + public KotlinTemplateParser(boolean contextSensitive, KotlinParser.Builder parser, Consumer onAfterVariableSubstitution, Consumer onBeforeParseTemplate, Set imports) { + super( + parser, + onAfterVariableSubstitution, + onBeforeParseTemplate, + imports, + contextSensitive, + new KotlinBlockStatementTemplateGenerator(imports, contextSensitive), + new AnnotationTemplateGenerator(imports) + ); + } +} diff --git a/src/main/java/org/openrewrite/kotlin/internal/template/package-info.java b/src/main/java/org/openrewrite/kotlin/internal/template/package-info.java new file mode 100644 index 000000000..aad0d0987 --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/internal/template/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +@NonNullFields +package org.openrewrite.kotlin.internal.template; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/src/main/java/org/openrewrite/kotlin/marker/package-info.java b/src/main/java/org/openrewrite/kotlin/marker/package-info.java new file mode 100644 index 000000000..4dd782244 --- /dev/null +++ b/src/main/java/org/openrewrite/kotlin/marker/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +@NonNullFields +package org.openrewrite.kotlin.marker; + +import org.openrewrite.internal.lang.NonNullApi; +import org.openrewrite.internal.lang.NonNullFields; diff --git a/src/test/java/org/openrewrite/kotlin/KotlinTemplateMatchTest.java b/src/test/java/org/openrewrite/kotlin/KotlinTemplateMatchTest.java new file mode 100644 index 000000000..71f02cba4 --- /dev/null +++ b/src/test/java/org/openrewrite/kotlin/KotlinTemplateMatchTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2023 the original author or authors. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *

+ * https://www.apache.org/licenses/LICENSE-2.0 + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.openrewrite.kotlin; + +import org.junit.jupiter.api.Test; +import org.openrewrite.ExecutionContext; +import org.openrewrite.java.tree.J; +import org.openrewrite.marker.SearchResult; +import org.openrewrite.test.RewriteTest; + +import static org.openrewrite.kotlin.Assertions.kotlin; +import static org.openrewrite.test.RewriteTest.toRecipe; + +public class KotlinTemplateMatchTest implements RewriteTest { + + @Test + void matchBinary() { + rewriteRun( + spec -> spec.recipe(toRecipe(() -> new KotlinVisitor<>() { + @Override + public J visitBinary(J.Binary binary, ExecutionContext ctx) { + return KotlinTemplate.matches("1 == #{any(int)}", getCursor()) ? + SearchResult.found(binary) : super.visitBinary(binary, ctx); + } + })), + kotlin( + """ + class Test { + val b1 = 1 == 2 + val b2 = 1 == 3 + + val b3 = 2 == 1 + val b4 = 2 == 2 + 3 /* no match because type is `kotlin.Int` */ + } + """, + """ + class Test { + val b1 = /*~~>*/1 == 2 + val b2 = /*~~>*/1 == 3 + + val b3 = 2 == 1 + val b4 = 2 == 2 + 3 /* no match because type is `kotlin.Int` */ + } + """ + )); + } +}