Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
Expand DownExpand Up@@ -384,6 +385,9 @@ private static Optional<String> getValueMember(AnnotationMirror mirror) {
* <li>{@code String} — wrapped in double quotes, e.g. {@code "GENERAL"}
* <li>{@code char} — wrapped in single quotes, e.g. {@code 'A'}
* <li>numeric/boolean primitives — used as-is, e.g. {@code 0.0}, {@code true}
* <li>enum types — a simple constant name is qualified with the enum class name, e.g. {@code
* GOOD} becomes {@code ItemCondition.GOOD}; already-qualified names or complex expressions
* are left as-is
* <li>complex types (List, custom objects) — used as a raw Java expression, e.g. {@code
* List.of()}
* </ul>
Expand All@@ -399,6 +403,24 @@ public static String formatDefaultExpression(String rawValue, TypeName fieldType
if (fieldType.equals(TypeNamePrimitive.CHAR)) {
return "'%s'".formatted(rawValue);
}
if (fieldType.isEnumType() && isSimpleIdentifier(rawValue)) {
return fieldType.getClassName() + "." + rawValue;
}
return rawValue;
}

/**
* Checks whether the given raw value is a simple Java identifier (e.g. an enum constant name like
* {@code GOOD}).
*
* <p>Values containing dots (e.g. {@code ItemCondition.GOOD}), parentheses (e.g. {@code new
* IntegerDto(12)}), spaces, or other non-identifier characters are not simple identifiers and
* will not be auto-qualified.
*
* @param rawValue the raw string to check
* @return {@code true} if the value is a simple Java identifier
*/
private static boolean isSimpleIdentifier(String rawValue) {
return SourceVersion.isIdentifier(rawValue);
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,6 +33,7 @@
import java.util.Optional;
import java.util.Set;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
Expand DownExpand Up@@ -649,6 +650,9 @@ public TypeName visitDeclared(DeclaredType t, Void p) {
String simpleClassName = elementOfParameter.getSimpleName().toString();
String packageName = context.getPackageName(elementOfParameter);
TypeName rawType = new TypeName(packageName, simpleClassName);
if (elementOfParameter.getKind() == ElementKind.ENUM) {
rawType.setEnumType(true);
}
TypeMirror enclosingType = t.getEnclosingType();
TypeName enclosing =
(enclosingType.getKind() != NONE)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,6 +51,9 @@ public class TypeName {
/** Whether this type has an empty constructor. */
private boolean hasEmptyConstructor = false;

/** Whether this type represents an enum. */
private boolean enumType = false;

/**
* Constructor for TypeName.
*
Expand DownExpand Up@@ -157,6 +160,24 @@ public void setHasEmptyConstructor(boolean hasEmptyConstructor) {
this.hasEmptyConstructor = hasEmptyConstructor;
}

/**
* Returns whether this type represents an enum.
*
* @return true if this type is an enum
*/
public boolean isEnumType() {
return enumType;
}

/**
* Sets whether this type represents an enum.
*
* @param enumType true if this type is an enum
*/
public void setEnumType(boolean enumType) {
this.enumType = enumType;
}

/**
* Helper function to hold a specific inner type.Is empty if this is a class without generic parts
* or a class has multiple generics.
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,6 +30,9 @@
import com.google.testing.compile.Compilation;
import java.util.stream.Stream;
import javax.tools.JavaFileObject;
import org.javahelpers.simple.builders.processor.analysis.FieldAnnotationExtractor;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.model.type.TypeNamePrimitive;
import org.javahelpers.simple.builders.processor.testing.ProcessorAsserts;
import org.javahelpers.simple.builders.processor.testing.ProcessorTestUtils;
import org.junit.jupiter.api.Test;
Expand DownExpand Up@@ -129,6 +132,48 @@ public PlainRecord build() {
PlainRecord result = new PlainRecord(this.name.value(), this.age.value());
return result;
}
"""),
Arguments.of(
"EnumDefaultRecord",
"""
package test;

import org.javahelpers.simple.builders.core.annotations.Default;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;

@SimpleBuilder
public record EnumDefaultRecord(
String name,
@Default("ACTIVE") Status status) {}

enum Status { ACTIVE, INACTIVE, PENDING }
""",
"""
public EnumDefaultRecord build() {
EnumDefaultRecord result = new EnumDefaultRecord(this.name.value(), this.status.valueOr(Status.ACTIVE));
return result;
}
"""),
Arguments.of(
"EnumQualifiedDefaultRecord",
"""
package test;

import org.javahelpers.simple.builders.core.annotations.Default;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;

@SimpleBuilder
public record EnumQualifiedDefaultRecord(
String name,
@Default("Priority.HIGH") Priority priority) {}

enum Priority { LOW, MEDIUM, HIGH }
""",
"""
public EnumQualifiedDefaultRecord build() {
EnumQualifiedDefaultRecord result = new EnumQualifiedDefaultRecord(this.name.value(), this.priority.valueOr(Priority.HIGH));
return result;
}
"""));
}

Expand DownExpand Up@@ -197,6 +242,124 @@ public OrderDto build() {
generatedCode, "public OrderDtoBuilder status(String status)");
}

/**
* Verifies that a {@code @Default} annotation on an enum-typed setter field qualifies the raw
* constant name with the enum class name, producing e.g. {@code .orElse(ItemCondition.GOOD)}
* instead of the unqualified {@code .orElse(GOOD)} that would fail to compile.
*
* @see <a href="https://github.com/java-helpers/simple-builders/issues/260">Issue #260</a>
*/
@Test
void defaultAppliedWhenUnset_setterField_enumQualified() {
String className = "ChoralScore";
String builderClassName = className + "Builder";

JavaFileObject enumSource =
ProcessorTestUtils.forSource(
"""
package test;

public enum ItemCondition { GOOD, FAIR, POOR }
""");

JavaFileObject sourceFile =
ProcessorTestUtils.forSource(
"""
package test;

import org.javahelpers.simple.builders.core.annotations.Default;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;

@SimpleBuilder
public class ChoralScore {
private String title;
@Default("GOOD")
private ItemCondition condition;

public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public ItemCondition getCondition() { return condition; }
public void setCondition(ItemCondition condition) { this.condition = condition; }
}
""");

Compilation compilation = compile(enumSource, sourceFile);
String generatedCode = loadGeneratedSource(compilation, builderClassName);
assertGenerationSucceeded(compilation, builderClassName, generatedCode);

// build() must use ifSet().orElse() with the qualified enum constant
ProcessorAsserts.assertContaining(
generatedCode,
"""
public ChoralScore build() {
ChoralScore result = new ChoralScore();
this.condition.ifSet(result::setCondition).orElse(ItemCondition.GOOD);
this.title.ifSet(result::setTitle);
return result;
}
""");
}

/**
* Verifies that a {@code @Default} annotation with a complex expression (e.g. {@code new
* ScoreValue(12)}) on a non-enum field is used as-is without any qualification, supporting
* constructor calls and other arbitrary Java expressions.
*/
@Test
void defaultAppliedWhenUnset_setterField_complexExpressionUsedAsIs() {
String className = "ScoreDto";
String builderClassName = className + "Builder";

JavaFileObject scoreValueSource =
ProcessorTestUtils.forSource(
"""
package test;

public class ScoreValue {
private final int value;
public ScoreValue(int value) { this.value = value; }
public int getValue() { return value; }
}
""");

JavaFileObject sourceFile =
ProcessorTestUtils.forSource(
"""
package test;

import org.javahelpers.simple.builders.core.annotations.Default;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;

@SimpleBuilder
public class ScoreDto {
private String name;
@Default("new ScoreValue(12)")
private ScoreValue score;

public String getName() { return name; }
public void setName(String name) { this.name = name; }
public ScoreValue getScore() { return score; }
public void setScore(ScoreValue score) { this.score = score; }
}
""");

Compilation compilation = compile(scoreValueSource, sourceFile);
String generatedCode = loadGeneratedSource(compilation, builderClassName);
assertGenerationSucceeded(compilation, builderClassName, generatedCode);

// build() must use the complex expression as-is (no qualification attempted)
ProcessorAsserts.assertContaining(
generatedCode,
"""
public ScoreDto build() {
ScoreDto result = new ScoreDto();
this.name.ifSet(result::setName);
this.score.ifSet(result::setScore).orElse(new ScoreValue(12));
return result;
}
""");
}

/**
* Verifies that a setter-based class field <em>without</em> {@code @Default} generates plain
* {@code ifSet(result::setStatus);} with no {@code .orElse()} call. This is a regression guard to
Expand DownExpand Up@@ -356,4 +519,30 @@ public JakartaRecord build() {
}
""");
}

// === formatDefaultExpression unit tests ===

private static Stream<Arguments> formatDefaultExpressionCases() {
TypeName enumType = new TypeName("test", "Status");
enumType.setEnumType(true);
TypeName nonEnumType = new TypeName("test", "Other");
return Stream.of(
Arguments.of(enumType, "ACTIVE", "Status.ACTIVE"),
Arguments.of(enumType, "", ""),
Arguments.of(enumType, "1BAD", "1BAD"),
Arguments.of(enumType, "GO OD", "GO OD"),
Arguments.of(enumType, "Status.ACTIVE", "Status.ACTIVE"),
Arguments.of(enumType, "new Status()", "new Status()"),
Arguments.of(nonEnumType, "ACTIVE", "ACTIVE"),
Arguments.of(TypeName.of(String.class), "hello", "\"hello\""),
Arguments.of(TypeNamePrimitive.CHAR, "X", "'X'"));
}

@ParameterizedTest
@MethodSource("formatDefaultExpressionCases")
void formatDefaultExpression_producesExpectedOutput(
TypeName fieldType, String rawValue, String expected) {
org.junit.jupiter.api.Assertions.assertEquals(
expected, FieldAnnotationExtractor.formatDefaultExpression(rawValue, fieldType));
}
}
Loading