Skip to content

Commit

Permalink
Add KotlinTemplate (#219)
Browse files Browse the repository at this point in the history
* Add `KotlinTemplate`

The `KotlinTemplate` class in this PR is an extension of `JavaTemplate` and replaces many of its internals so that `JavaTemplate` can be used to parse and template Kotlin code.

As a demo have a look at `ReplaceCharToIntWithCode`.

* Remove optional semicolon from template code

* Copy some code from `JavaTemplate.Builder`

* Implement `KotlinSubstitutions` and `KotlinBlockStatementTemplateGenerator`

* Update to use `getTemplateClasspathDir()`

* Simplify `KotlinSubstitutions.newArrayParameter()`

* Add `TEMPLATE_INTERNAL_IMPORTS` imports

* Polish

* Remove no longer required `JavaTemplate` constructor argument

* Align with latest upstream changes

* Make `addClasspathEntry()` public

---------

Co-authored-by: Kun Li <[email protected]>
  • Loading branch information
knutwannheden and kunli2 authored Mar 5, 2024
1 parent 1b0c623 commit 2bdf2c6
Show file tree
Hide file tree
Showing 9 changed files with 386 additions and 23 deletions.
12 changes: 12 additions & 0 deletions src/main/java/org/openrewrite/kotlin/KotlinParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
123 changes: 123 additions & 0 deletions src/main/java/org/openrewrite/kotlin/KotlinTemplate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> imports, Consumer<String> onAfterVariableSubstitution, Consumer<String> 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 extends J> 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<String> imports = new HashSet<>();

private KotlinParser.Builder parser = KotlinParser.builder();

private Consumer<String> onAfterVariableSubstitution = s -> {
};
private Consumer<String> 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<String> afterVariableSubstitution) {
this.onAfterVariableSubstitution = afterVariableSubstitution;
return this;
}

public Builder doBeforeParseTemplate(Consumer<String> beforeParseTemplate) {
this.onBeforeParseTemplate = beforeParseTemplate;
return this;
}

public KotlinTemplate build() {
return new KotlinTemplate(false, parser, code, imports,
onAfterVariableSubstitution, onBeforeParseTemplate);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -51,29 +47,16 @@ public String getDescription() {
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new KotlinVisitor<ExecutionContext>() {
@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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String?>())`
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> onAfterVariableSubstitution, Consumer<String> onBeforeParseTemplate, Set<String> imports) {
super(
parser,
onAfterVariableSubstitution,
onBeforeParseTemplate,
imports,
contextSensitive,
new KotlinBlockStatementTemplateGenerator(imports, contextSensitive),
new AnnotationTemplateGenerator(imports)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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;
21 changes: 21 additions & 0 deletions src/main/java/org/openrewrite/kotlin/marker/package-info.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2023 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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;
Loading

0 comments on commit 2bdf2c6

Please sign in to comment.