Skip to content

Commit

Permalink
Fix generation of annotations, including lists, nested annotations et…
Browse files Browse the repository at this point in the history
…c. (#9182)

EnumValue now has correct equals and hashcode methods.
  • Loading branch information
tomas-langer authored Aug 21, 2024
1 parent 0873e92 commit 40bac37
Show file tree
Hide file tree
Showing 19 changed files with 1,079 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void writeComponent(ModelWriter writer, Set<String> declaredTokens, ImportOrgani
if (parameters.size() == 1) {
AnnotationParameter parameter = parameters.get(0);
if (parameter.name().equals("value")) {
writer.write(parameter.value());
parameter.writeValue(writer, imports);
} else {
parameter.writeComponent(writer, declaredTokens, imports, classType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
package io.helidon.codegen.classmodel;

import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import io.helidon.common.types.Annotation;
import io.helidon.common.types.EnumValue;
import io.helidon.common.types.TypeName;

Expand All @@ -27,13 +32,14 @@
*/
public final class AnnotationParameter extends CommonComponent {

private final String value;
private final TypeName importedType;
private final Set<TypeName> importedTypes;
private final Object objectValue;

private AnnotationParameter(Builder builder) {
super(builder);
this.value = resolveValueToString(builder.type(), builder.value);
this.importedType = resolveImport(builder.value);

this.objectValue = builder.value;
this.importedTypes = resolveImports(builder.value);
}

/**
Expand All @@ -45,51 +51,135 @@ public static Builder builder() {
return new Builder();
}

@Override
public String toString() {
return objectValue + " (" + type().simpleTypeName() + ")";
}

@Override
void writeComponent(ModelWriter writer, Set<String> declaredTokens, ImportOrganizer imports, ClassType classType)
throws IOException {
writer.write(name() + " = " + value);
writer.write(name() + " = ");
writeValue(writer, imports);
}

@Override
void addImports(ImportOrganizer.Builder imports) {
if (importedType != null) {
imports.addImport(importedType);
}
importedTypes.forEach(imports::addImport);
}

void writeValue(ModelWriter writer, ImportOrganizer imports) throws IOException {
writer.write(resolveValueToString(imports, type(), objectValue));
}

private static TypeName resolveImport(Object value) {
private static Set<TypeName> resolveImports(Object value) {
Set<TypeName> imports = new HashSet<>();

resolveImports(imports, value);

return imports;
}

private static void resolveImports(Set<TypeName> imports, Object value) {
if (value.getClass().isEnum()) {
return TypeName.create(value.getClass());
imports.add(TypeName.create(value.getClass()));
return;
}
if (value instanceof TypeName tn) {
return tn;
switch (value) {
case TypeName tn -> imports.add(tn);
case EnumValue ev -> imports.add(ev.type());
case Annotation an -> {
imports.add(an.typeName());
an.values()
.values()
.forEach(nestedValue -> resolveImports(imports, nestedValue));
}
default -> {
}
if (value instanceof EnumValue ev) {
return ev.type();
}
return null;
}

private static String resolveValueToString(Type type, Object value) {
// takes the annotation value objects and converts it to its string representation (as seen in class source)
private static String resolveValueToString(ImportOrganizer imports, Type type, Object value) {
Class<?> valueClass = value.getClass();
if (valueClass.isEnum()) {
return valueClass.getSimpleName() + "." + ((Enum<?>) value).name();
} else if (type.fqTypeName().equals(String.class.getName())) {
return imports.typeName(Type.fromTypeName(TypeName.create(valueClass)), true)
+ "." + ((Enum<?>) value).name();
}
if (type != null && type.fqTypeName().equals(String.class.getName())) {
String stringValue = value.toString();
if (!stringValue.startsWith("\"") && !stringValue.endsWith("\"")) {
return "\"" + stringValue + "\"";
}
} else if (value instanceof TypeName typeName) {
return typeName.classNameWithEnclosingNames() + ".class";
} else if (value instanceof EnumValue enumValue) {
return enumValue.type().classNameWithEnclosingNames() + "." + enumValue.name();
return stringValue;
}

if (type != null && type.fqTypeName().equals(Object.class.getName())) {
// we expect this to be "as is" - such as when parsing annotations
return value.toString();
}

return switch (value) {
case TypeName typeName -> imports.typeName(Type.fromTypeName(typeName), true) + ".class";
case EnumValue enumValue -> imports.typeName(Type.fromTypeName(enumValue.type()), true)
+ "." + enumValue.name();
case Character character -> "'" + character + "'";
case Long longValue -> longValue + "L";
case Float floatValue -> floatValue + "F";
case Double doubleValue -> doubleValue + "D";
case Byte byteValue -> "(byte) " + byteValue;
case Short shortValue -> "(short) " + shortValue;
case Class<?> clazz -> imports.typeName(Type.fromTypeName(TypeName.create(clazz)), true) + ".class";
case Annotation annotation -> nestedAnnotationValue(imports, annotation);
case List<?> list -> nestedListValue(imports, list);
case String str -> str.startsWith("\"") && str.endsWith("\"") ? str : "\"" + str + "\"";
default -> value.toString();
};

}

private static String nestedListValue(ImportOrganizer imports, List<?> list) {
if (list.isEmpty()) {
return "{}";
}
StringBuilder result = new StringBuilder();
if (list.size() > 1) {
result.append("{");
}
return value.toString();

result.append(list.stream()
.map(it -> resolveValueToString(imports, null, it))
.collect(Collectors.joining(", ")));

if (list.size() > 1) {
result.append("}");
}
return result.toString();
}

String value() {
return value;
private static String nestedAnnotationValue(ImportOrganizer imports, Annotation annotation) {
StringBuilder sb = new StringBuilder("@");
sb.append(imports.typeName(Type.fromTypeName(annotation.typeName()), true));

Map<String, Object> values = annotation.values();
if (values.isEmpty()) {
return sb.toString();
}

sb.append("(");
if (values.size() == 1 && values.containsKey("value")) {
sb.append(resolveValueToString(imports, null, values.get("value")));
} else {
values.forEach((key, value) -> {
sb.append(key)
.append(" = ")
.append(resolveValueToString(imports, null, value))
.append(", ");
});
sb.delete(sb.length() - 2, sb.length());
}
sb.append(")");
return sb.toString();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.helidon.common.types.AccessModifier;
import io.helidon.common.types.Annotation;
import io.helidon.common.types.ElementKind;
import io.helidon.common.types.EnumValue;
import io.helidon.common.types.Modifier;
import io.helidon.common.types.TypeName;
import io.helidon.common.types.TypeNames;
Expand Down Expand Up @@ -164,7 +165,9 @@ private static void addAnnotationValue(ContentBuilder<?> contentBuilder, Object
case Class<?> value -> contentBuilder.addContentCreate(TypeName.create(value));
case TypeName value -> contentBuilder.addContentCreate(value);
case Annotation value -> contentBuilder.addContentCreate(value);
case Enum<?> value -> toEnumValue(contentBuilder, value);
case Enum<?> value -> toEnumValue(contentBuilder,
EnumValue.create(TypeName.create(value.getDeclaringClass()), value.name()));
case EnumValue value -> toEnumValue(contentBuilder, value);
case List<?> values -> toListValues(contentBuilder, values);
default -> throw new IllegalStateException("Unexpected annotation value type " + objectValue.getClass()
.getName() + ": " + objectValue);
Expand All @@ -185,9 +188,17 @@ private static void toListValues(ContentBuilder<?> contentBuilder, List<?> value
contentBuilder.addContent(")");
}

private static void toEnumValue(ContentBuilder<?> contentBuilder, Enum<?> enumValue) {
contentBuilder.addContent(enumValue.getDeclaringClass())
.addContent(".")
.addContent(enumValue.name());
private static void toEnumValue(ContentBuilder<?> contentBuilder, EnumValue enumValue) {
// it would be easier to just use Enum.VALUE, but annotations and their dependencies
// may not be on runtime classpath, so we have to work around it

// EnumValue.create(TypeName.create(...), "VALUE")
contentBuilder.addContent(EnumValue.class)
.addContent(".create(")
.addContentCreate(enumValue.type())
.addContent(",")
.addContent("\"")
.addContent(enumValue.name())
.addContent("\")");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,12 @@ String typeName(Type type, boolean includedImport) {
}
Type checkedType = type.declaringClass().orElse(type);
String fullTypeName = checkedType.fqTypeName();
String simpleTypeName = checkedType.simpleTypeName();

if (!includedImport) {
return fullTypeName;
}

String simpleTypeName = checkedType.simpleTypeName();
if (forcedFullImports.contains(fullTypeName)) {
return type.fqTypeName();
} else if (noImport.contains(fullTypeName) || imports.contains(fullTypeName)) {
Expand Down
Loading

0 comments on commit 40bac37

Please sign in to comment.