Skip to content

Commit

Permalink
Add Java idiom for concatenating strings (#220)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstepanov authored Jan 9, 2025
1 parent 0162733 commit 0fbe0d5
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.micronaut.sourcegen.model.EnumDef;
import io.micronaut.sourcegen.model.ExpressionDef;
import io.micronaut.sourcegen.model.FieldDef;
import io.micronaut.sourcegen.model.JavaIdioms;
import io.micronaut.sourcegen.model.MethodDef;
import io.micronaut.sourcegen.model.ObjectDef;
import io.micronaut.sourcegen.model.StatementDef;
Expand All @@ -32,6 +33,118 @@

class ByteCodeWriterTest {

@Test
void concatStrings() {
ClassDef def = ClassDef.builder("example.Example")
.addModifiers(Modifier.PUBLIC)
.addMethod(MethodDef.builder("myMethod")
.addParameters(String.class)
.addParameters(Object.class)
.addParameters(String[].class)
.build((aThis, methodParameters) -> JavaIdioms.concatStrings(methodParameters).returning()))
.build();

StringWriter bytecodeWriter = new StringWriter();
byte[] bytes = generateFile(def, bytecodeWriter);

String bytecode = bytecodeWriter.toString();
Assertions.assertEquals("""
// class version 61.0 (61)
// access flags 0x1
// signature Ljava/lang/Object;
// declaration: example/Example
public class example/Example {
// access flags 0x1
public <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
// access flags 0x0
myMethod(Ljava/lang/String;Ljava/lang/Object;[Ljava/lang/String;)Ljava/lang/String;
L0
NEW java/lang/StringBuilder
DUP
ALOAD 1
INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
ALOAD 2
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/Object;)Ljava/lang/StringBuilder;
ALOAD 3
INVOKESTATIC java/util/Arrays.toString ([Ljava/lang/String;)Ljava/lang/String;
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
ARETURN
L1
LOCALVARIABLE arg1 Ljava/lang/String; L0 L1 1
LOCALVARIABLE arg2 Ljava/lang/Object; L0 L1 2
LOCALVARIABLE arg3 [Ljava/lang/String; L0 L1 3
}
""", bytecode);

Assertions.assertEquals("""
package example;
import java.util.Arrays;
public class Example {
String myMethod(String arg1, Object arg2, String[] arg3) {
return arg1 + arg2 + Arrays.toString(arg3);
}
}
""", decompileToJava(bytes));
}

@Test
void instanceOf() {
ClassDef def = ClassDef.builder("example.Example")
.addModifiers(Modifier.PUBLIC)
.addMethod(MethodDef.builder("myMethod")
.addParameters(Object.class)
.build((aThis, methodParameters) -> methodParameters.get(0).instanceOf(TypeDef.STRING).returning()))
.build();

StringWriter bytecodeWriter = new StringWriter();
byte[] bytes = generateFile(def, bytecodeWriter);

String bytecode = bytecodeWriter.toString();
Assertions.assertEquals("""
// class version 61.0 (61)
// access flags 0x1
// signature Ljava/lang/Object;
// declaration: example/Example
public class example/Example {
// access flags 0x1
public <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
// access flags 0x0
myMethod(Ljava/lang/Object;)Z
L0
ALOAD 1
INSTANCEOF java/lang/String
IRETURN
L1
LOCALVARIABLE arg1 Ljava/lang/Object; L0 L1 1
}
""", bytecode);

Assertions.assertEquals("""
package example;
public class Example {
boolean myMethod(Object arg1) {
return arg1 instanceof String;
}
}
""", decompileToJava(bytes));
}

@Test
void arrayElement() {
ClassDef def = ClassDef.builder("example.Example")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@
import io.micronaut.sourcegen.model.ClassDef;
import io.micronaut.sourcegen.model.ClassTypeDef;
import io.micronaut.sourcegen.model.ExpressionDef;
import io.micronaut.sourcegen.model.JavaIdioms;
import io.micronaut.sourcegen.model.MethodDef;
import io.micronaut.sourcegen.model.StatementDef;
import io.micronaut.sourcegen.model.TypeDef;
import io.micronaut.sourcegen.model.VariableDef;

import javax.lang.model.element.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
Expand All @@ -59,16 +59,6 @@ public final class ObjectAnnotationVisitor implements TypeElementVisitor<Object,

private static final ExpressionDef HASH_MULTIPLIER = ExpressionDef.primitiveConstant(31);

private static final MethodDef APPEND_STRING = MethodDef.builder("append")
.returns(StringBuilder.class)
.addParameter(String.class)
.build();

private static final MethodDef APPEND_OBJECT = MethodDef.builder("append")
.returns(StringBuilder.class)
.addParameter(Object.class)
.build();

private final Set<String> processed = new HashSet<>();

@Override
Expand Down Expand Up @@ -148,34 +138,21 @@ private static void createToStringMethod(ClassDef.ClassDefBuilder classDefBuilde
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(TypeDef.STRING)
.addParameter("instance", selfType)
.build((self, parameterDef) ->
ClassTypeDef.of(StringBuilder.class).instantiate(
ExpressionDef.constant(selfType.getSimpleName() + "["))
.newLocal("strBuilder", variableDef -> {
ExpressionDef exp = variableDef;
for (int i = 0; i < properties.size(); i++) {
var beanProperty = properties.get(i);
if (beanProperty.isWriteOnly()) {
continue;
}
ExpressionDef propertyValue = parameterDef.get(0).getPropertyValue(beanProperty);

exp = exp.invoke(APPEND_STRING, ExpressionDef.constant(beanProperty.getName() + "="));
TypeDef propertyType = TypeDef.of(beanProperty.getType());
if (propertyType.isArray()) {
exp = exp.invoke(APPEND_STRING,
ClassTypeDef.of(Arrays.class).invokeStatic("toString", TypeDef.STRING, propertyValue)
);
} else if (propertyType.isPrimitive() || propertyType.equals(TypeDef.STRING)) {
exp = exp.invoke("append", variableDef.type(), propertyValue);
} else {
exp = exp.invoke(APPEND_OBJECT, propertyValue);
}
exp = exp.invoke(APPEND_STRING,
ExpressionDef.constant((i == properties.size() - 1) ? "]" : ", "));
.build((self, parameterDef) -> {
List<ExpressionDef> expressions = new ArrayList<>();
expressions.add(ExpressionDef.constant(selfType.getSimpleName() + "["));
for (int i = 0; i < properties.size(); i++) {
var beanProperty = properties.get(i);
if (beanProperty.isWriteOnly()) {
continue;
}
return exp.invoke("toString", TypeDef.STRING).returning();
})
ExpressionDef propertyValue = parameterDef.get(0).getPropertyValue(beanProperty);
expressions.add(ExpressionDef.constant(beanProperty.getName() + "="));
expressions.add(propertyValue);
expressions.add(ExpressionDef.constant((i == properties.size() - 1) ? "]" : ", "));
}
return JavaIdioms.concatStrings(expressions).returning();
}
);
classDefBuilder.addMethod(method);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@
package io.micronaut.sourcegen.model;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.MethodElement;
import io.micronaut.inject.ast.PropertyElement;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
Expand Down Expand Up @@ -54,6 +57,66 @@ public final class JavaIdioms {

private static final ClassTypeDef ARRAYS_TYPE = ClassTypeDef.of(Arrays.class);

private static final Method STRING_BUILDER_APPEND_STRING = ReflectionUtils.getRequiredMethod(StringBuilder.class, "append", String.class);
private static final Method STRING_BUILDER_APPEND_OBJECT = ReflectionUtils.getRequiredMethod(StringBuilder.class, "append", Object.class);
private static final Method STRING_BUILDER_TO_STRING = ReflectionUtils.getRequiredMethod(StringBuilder.class, "toString");

/**
* Concat strings using {@link StringBuilder}.
*
* @param stringExpressions The expression
* @return The string builder expression
*/
public static ExpressionDef concatStrings(ExpressionDef... stringExpressions) {
return concatStrings(Arrays.asList(stringExpressions));
}

/**
* Concat strings using {@link StringBuilder}.
*
* @param stringExpressions The expression
* @return The string builder expression
*/
public static ExpressionDef concatStrings(List<? extends ExpressionDef> stringExpressions) {
if (stringExpressions.isEmpty()) {
return ExpressionDef.constant("");
}
if (stringExpressions.size() == 1) {
return convertToStringIfNeeded(stringExpressions.get(0));
}
ExpressionDef stringBuilderExp = null;
for (ExpressionDef expression : stringExpressions) {
if (stringBuilderExp == null) {
stringBuilderExp = ClassTypeDef.of(StringBuilder.class).instantiate(
convertToStringIfNeeded(expression)
);
} else if (expression.type().equals(TypeDef.STRING)) {
stringBuilderExp = stringBuilderExp.invoke(STRING_BUILDER_APPEND_STRING, expression);
} else if (expression.type().isArray()) {
stringBuilderExp = stringBuilderExp.invoke(STRING_BUILDER_APPEND_STRING, convertToStringIfNeeded(expression));
} else {
stringBuilderExp = stringBuilderExp.invoke(STRING_BUILDER_APPEND_OBJECT, expression);
}
}
return stringBuilderExp.invoke(STRING_BUILDER_TO_STRING);
}

/**
* Convert the expression to {@link String} if it's not one.
* @param expression The expression
* @return The string expression
*/
public static ExpressionDef convertToStringIfNeeded(ExpressionDef expression) {
TypeDef type = expression.type();
if (type.equals(TypeDef.STRING)) {
return expression;
}
if (type.isArray()) {
return ClassTypeDef.of(Arrays.class).invokeStatic("toString", TypeDef.STRING, expression);
}
return ClassTypeDef.of(String.class).invokeStatic("valueOf", TypeDef.STRING, expression);
}

/**
* The equals structurally idiom.
*
Expand Down

0 comments on commit 0fbe0d5

Please sign in to comment.