From 0fbe0d5e9668716a60db10a107fc0377d9a05d6f Mon Sep 17 00:00:00 2001 From: Denis Stepanov Date: Thu, 9 Jan 2025 19:16:29 +0200 Subject: [PATCH] Add Java idiom for concatenating strings (#220) --- .../bytecode/ByteCodeWriterTest.java | 113 ++++++++++++++++++ .../visitors/ObjectAnnotationVisitor.java | 53 +++----- .../micronaut/sourcegen/model/JavaIdioms.java | 63 ++++++++++ 3 files changed, 191 insertions(+), 38 deletions(-) diff --git a/sourcegen-bytecode-writer/src/test/java/io/micronaut/sourcegen/bytecode/ByteCodeWriterTest.java b/sourcegen-bytecode-writer/src/test/java/io/micronaut/sourcegen/bytecode/ByteCodeWriterTest.java index b770aeb7..fffab299 100644 --- a/sourcegen-bytecode-writer/src/test/java/io/micronaut/sourcegen/bytecode/ByteCodeWriterTest.java +++ b/sourcegen-bytecode-writer/src/test/java/io/micronaut/sourcegen/bytecode/ByteCodeWriterTest.java @@ -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; @@ -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 ()V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()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. (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 ()V + ALOAD 0 + INVOKESPECIAL java/lang/Object. ()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") diff --git a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ObjectAnnotationVisitor.java b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ObjectAnnotationVisitor.java index 442aeb75..a874e2c9 100644 --- a/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ObjectAnnotationVisitor.java +++ b/sourcegen-generator/src/main/java/io/micronaut/sourcegen/generator/visitors/ObjectAnnotationVisitor.java @@ -29,6 +29,7 @@ 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; @@ -36,7 +37,6 @@ 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; @@ -59,16 +59,6 @@ public final class ObjectAnnotationVisitor implements TypeElementVisitor processed = new HashSet<>(); @Override @@ -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 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); } diff --git a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/JavaIdioms.java b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/JavaIdioms.java index 7a17de3b..e04ce80b 100644 --- a/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/JavaIdioms.java +++ b/sourcegen-model/src/main/java/io/micronaut/sourcegen/model/JavaIdioms.java @@ -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; /** @@ -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 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. *