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 number Diff line number Diff line change
Expand Up @@ -125,11 +125,24 @@
/**
* The types for which builders are generated. Every listed type must be resolvable on the
* classpath or in the current compilation and must be constructible through accessible Java APIs
* (e.g. an accessible constructor). At least one type is required.
* (e.g. an accessible constructor). At least one of {@link #value()} or {@link #sourcePackages()}
* must be set.
*
* @return the external types to generate builders for
*/
Class<?>[] value();
Class<?>[] value() default {};

/**
* Packages scanned for types to generate builders for: every top-level constructible type
* declared in a listed package gets a builder, without listing the types one by one in {@link
* #value()}. Interfaces, abstract classes, enums and annotation types are skipped; nested types
* keep requiring an explicit {@link #value()} entry. Types a package contributes are ordered by
* qualified name. These are the packages the types come <em>from</em> — the package the builders
* are generated <em>into</em> is {@link SimpleBuilder.Options#packageName()}.
*
* @return the packages whose top-level types get builders
*/
String[] sourcePackages() default {};

/**
* Configuration options for the generated builders, reusing the {@link SimpleBuilder.Options}
Expand Down
13 changes: 13 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,19 @@ import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;

The generated builders are placed in the annotated package - or in the package of the provider class when a class is annotated instead. `options` reuses `@SimpleBuilder.Options` and is optional - compiler defaults apply when omitted.

`sourcePackages` lists whole packages to scan instead of naming types one by one: every top-level constructible type declared in a listed package gets a builder, combined with the types of `value` (an explicit `value` entry wins over the same type found by scanning; a `sourcePackages`-only declaration without `value` is valid). It names the packages the types come *from* - not to be confused with `options.packageName`, the package builders are generated *into*:

```java
// Generates a builder for every top-level class and record in com.thirdparty.users
@SimpleBuilderFor(
sourcePackages = "com.thirdparty.users",
options = @SimpleBuilder.Options(packageName = "com.example.generated"))
public class ExternalBuildersProvider {
}
```

Interfaces, abstract classes, enums and annotation types are skipped (a debug log lists each skip), and package scanning only sees *top-level* types - nested types keep requiring an explicit `value` entry. Only packages visible to the compilation can be resolved: an unresolvable package name produces a warning and is skipped. Types a package contributes are ordered by qualified name, so the generated set is deterministic.

The target type must be constructible through accessible Java APIs from the builder's package: it needs a visible type and an accessible constructor, otherwise generation fails with a compile-time diagnostic. The target type's own annotations are not consulted - the explicit declaration wins, so `@Ignore4BuilderGeneration` on the target does not suppress generation either. `@SimpleBuilderFor` is not `@Inherited`, and the provider class or package itself never gets a builder.

`@SimpleBuilderFor` is `@Repeatable`: several declarations may be placed on the same holder or `package-info.java` when the listed types need different options - for example a different `packageName` per declaration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
Expand All @@ -48,8 +50,12 @@
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder.Template;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilderFor;
Expand Down Expand Up @@ -338,7 +344,7 @@ private List<ElementToGenerate> planGenerationOfTypeByHolder(
if (targets.isEmpty()) {
context.warning(
holder,
"simple-builders: @SimpleBuilderFor on '%s' does not list any types - nothing to generate",
"simple-builders: @SimpleBuilderFor on '%s' resolves no types - nothing to generate",
holder.getSimpleName());
continue;
}
Expand Down Expand Up @@ -392,26 +398,86 @@ private Optional<ElementToGenerate> planExternalTarget(
}

/**
* Reads the {@code value} attribute of one {@code @SimpleBuilderFor} annotation instance on the
* holder and resolves each entry to the {@link TypeElement} the builder is generated for.
* Resolves the types one {@code @SimpleBuilderFor} annotation instance declares builders for: the
* types listed in {@code value} first (a type listed there wins over the same type found by
* scanning), then the constructible top-level types of every {@code sourcePackages} package,
* ordered by qualified name for deterministic output.
*/
private List<TypeElement> extractExternalTargetTypes(
Element holder, AnnotationMirror annotationInstance) throws BuilderException {
Optional<AnnotationValue> valueAttribute =
JavaLangAnalyser.findAnnotationAttribute(annotationInstance, "value", context);
List<TypeElement> results = new ArrayList<>();
Map<String, TypeElement> targets = new LinkedHashMap<>();
// For an array-valued attribute javac always delivers a list, even for a single entry.
if (valueAttribute.isEmpty() || !(valueAttribute.get().getValue() instanceof List<?> items)) {
for (Object item : readArrayAttribute(annotationInstance, "value")) {
TypeElement target = resolveExternalTargetType(holder, item);
targets.put(target.getQualifiedName().toString(), target);
}
for (String packageName : readSourcePackageNames(annotationInstance)) {
for (TypeElement target : findPackageTargetTypes(holder, packageName)) {
targets.putIfAbsent(target.getQualifiedName().toString(), target);
}
}
return List.copyOf(targets.values());
}

/** Reads an array-typed attribute of an annotation mirror as the raw item list. */
private List<?> readArrayAttribute(AnnotationMirror annotationInstance, String attributeName) {
return JavaLangAnalyser.findAnnotationAttribute(annotationInstance, attributeName, context)
.map(AnnotationValue::getValue)
.filter(List.class::isInstance)
.map(List.class::cast)
.orElse(List.of());
}

/**
* Reads the {@code sourcePackages} attribute of one {@code @SimpleBuilderFor} annotation
* instance.
*/
private List<String> readSourcePackageNames(AnnotationMirror annotationInstance) {
return readArrayAttribute(annotationInstance, "sourcePackages").stream()
.map(item -> item instanceof AnnotationValue value ? value.getValue() : null)
.filter(String.class::isInstance)
.map(String.class::cast)
.toList();
}

/**
* Finds the constructible top-level types of one {@code sourcePackages} entry: only concrete
* classes and records qualify; interfaces, abstract classes, enums and annotation types are
* skipped with a debug note.
*/
private List<TypeElement> findPackageTargetTypes(Element holder, String packageName) {
PackageElement packageElement = context.getPackageElement(packageName);
if (packageElement == null) {
context.warning(
holder,
"simple-builders: could not read the 'value' attribute of @SimpleBuilderFor on '%s' - nothing to generate",
"simple-builders: package '%s' in @SimpleBuilderFor on '%s' could not be resolved - skipped",
packageName,
holder.getSimpleName());
return results;
return List.of();
}
for (Object item : items) {
results.add(resolveExternalTargetType(holder, item));
List<TypeElement> targets = new ArrayList<>();
for (TypeElement type : ElementFilter.typesIn(packageElement.getEnclosedElements())) {
if (isConstructibleTopLevelType(type)) {
targets.add(type);
} else {
context.debug(
"simple-builders: skipping %s '%s' in package '%s' declared by @SimpleBuilderFor on '%s' - no builder can be generated",
type.getKind().toString().toLowerCase(),
type.getQualifiedName(),
packageName,
holder.getSimpleName());
}
}
return results;
targets.sort(Comparator.comparing(type -> type.getQualifiedName().toString()));
return targets;
}

/** Whether a package member is a type a builder can be generated for. */
private static boolean isConstructibleTopLevelType(TypeElement type) {
if (type.getKind() == ElementKind.RECORD) {
return true;
}
return type.getKind() == ElementKind.CLASS && !type.getModifiers().contains(Modifier.ABSTRACT);
}

/** Resolves one entry of a {@code @SimpleBuilderFor} {@code value} attribute to its type. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,16 @@ public PackageElement getPackageOf(Element element) {
return elementUtils.getPackageOf(element);
}

/**
* Get the package element for a qualified package name.
*
* @param name the qualified package name
* @return the package element, or null if no such package is visible to the compilation
*/
public PackageElement getPackageElement(String name) {
return elementUtils.getPackageElement(name);
}

/**
* Get the package name of an element.
*
Expand Down
Loading
Loading