Skip to content
Open
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@@ -61,6 +61,8 @@
* usingArrayListBuilderWithElementBuilders, usingHashSetBuilder,
* usingHashSetBuilderWithElementBuilders, usingHashMapBuilder (all default: true)
* <li><b>Integration:</b> generateWithInterface (default: true)
* <li><b>Builder Scoping:</b> builderGenerationPackages, builderUsagePackages (default: "" = all
* annotated DTOs; comma-separated package list, subpackages included)
* </ul>
*
* <p>This annotation is itself a built-in {@link Template}: it is meta-annotated with
Expand DownExpand Up@@ -666,6 +668,46 @@
*/
String jacksonModulePackage() default "";

/**
* Comma-separated list of packages for which builders should be generated by this processor.
* <br>
* When set, builder generation is restricted to DTOs whose package equals or is a subpackage of
* one of the listed packages. Builder references to DTOs inside this scope are emitted directly
* without an extra existence check.
*
* <p>Subpackages are included automatically ({@code com.example} also matches {@code
* com.example.sub}).
*
* <p>Default: "" (empty - no package restriction; generate builders for all annotated DTOs)
* <br>
* Compiler option: -Asimplebuilder.builderGenerationPackages
*
* <p>This option is intended as a project-wide setting, usually configured via the compiler
* arguments for an annotation processor.
*
* @return the packages for which builders are generated
*/
String builderGenerationPackages() default "";

/**
* Comma-separated list of packages whose builders may be referenced as helper methods by other
* generated builders. DTOs in a listed package but <b>not</b> covered by {@link
* #builderGenerationPackages()} are only referenced when the processor can resolve the compiled
* builder type on the classpath.
*
* <p>Subpackages are included automatically ({@code com.example} also matches {@code
* com.example.sub}).
*
* <p>Default: "" (empty - any annotated type may be referenced; no type existence check) <br>
* Compiler option: -Asimplebuilder.builderUsagePackages
*
* <p>This option is intended as a project-wide setting, usually configured via the compiler
* arguments for an annotation processor.
*
* @return the packages whose builders may be used by other builders
*/
String builderUsagePackages() default "";

// === Naming ===
/**
* Suffix to append to the DTO name to generate the builder class name. <br>
Expand Down
34 changes: 34 additions & 0 deletions docs/CONFIGURATION.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -678,6 +678,32 @@ See [**CUSTOMIZING.md**](CUSTOMIZING.md) for complete implementation examples an

---

### Builder Scoping

#### `builderGenerationPackages`

**Default**: `""` (empty) | **Compiler Option**: `-Asimplebuilder.builderGenerationPackages=pkg1,pkg2,...`

Restricts builder generation to DTOs whose package equals or is a subpackage of one of the listed packages. Subpackages are included automatically (`com.example` also matches `com.example.sub`).

When set, the processor only generates builders for annotated DTOs inside this scope. Because the processor is generating these builders, references to them from other generated builders can be emitted directly without an extra type-existence check.

This option is intended as a project-wide setting, usually configured via the processor's compiler arguments.

#### `builderUsagePackages`

**Default**: `""` (empty) | **Compiler Option**: `-Asimplebuilder.builderUsagePackages=pkg1,pkg2,...`

Allows generated builders in other packages to reference builders from the listed packages. Subpackages are included automatically.

A DTO inside the usage scope but **not** inside the generation scope is only referenced as a builder when the processor can resolve the compiled builder type on the classpath. If the builder type cannot be found, the field falls back to a plain setter.

**Typical use case**: referencing builders from precompiled library DTOs that carry the `@SimpleBuilder` annotation but are processed in a different compilation unit.

**Backward compatibility**: When both options are empty (the default), the processor behaves exactly as before and references any `@SimpleBuilder`-annotated type without a type-existence check.

---

### Integration & Annotations

#### `generateWithInterface`
Expand DownExpand Up@@ -1277,6 +1303,10 @@ methodAccess = AccessModifier.PRIVATE
# Component Filtering
-Asimplebuilder.deactivateGenerationComponents=pattern1,pattern2,...

# Builder Scoping
-Asimplebuilder.builderGenerationPackages=pkg1,pkg2,...
-Asimplebuilder.builderUsagePackages=pkg1,pkg2,...

# Integration & Annotations
-Asimplebuilder.generateWithInterface=ENABLED|DISABLED
-Asimplebuilder.implementsBuilderBase=ENABLED|DISABLED
Expand DownExpand Up@@ -1328,6 +1358,10 @@ methodAccess = AccessModifier.PRIVATE
usingBuilderImplementationAnnotation = OptionState.ENABLED,
usingJacksonDeserializerAnnotation = OptionState.ENABLED,

// Builder Scoping (project-wide compiler arguments are recommended)
builderGenerationPackages = "",
builderUsagePackages = "",

// Naming
builderSuffix = "Builder",
setterSuffix = ""
Expand Down
1 change: 1 addition & 0 deletions docs/CUSTOMIZING.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -296,6 +296,7 @@ compileJava {
- **Unified Registry** - A single `GeneratorRegistry` loads all generators from one service file
- **Type-Based Separation** - The registry automatically separates generators by type using `instanceof`
- **Single Service File** - All generators (both method and builder) are registered in one place
- **Builder Scope Resolution** - The decision whether a DTO type may be referenced as a builder is centralized in `BuilderScopeResolver`. Custom generators and enhancers should continue to rely on `TypeName.getBuilderType().isPresent()` instead of reimplementing the scoping rules.

### Priority Management

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -164,6 +164,25 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
return false;
});

// Filter out elements whose package is not in the configured builder generation scope.
// An empty scope means "unscoped" and keeps the current backward-compatible behavior.
BuilderConfiguration globalConfig = context.getConfigurationReader().getGlobalConfiguration();
elementsToProcess.removeIf(
element -> {
String packageName = context.getPackageName(element);
if (!globalConfig.isInGenerationScope(packageName)) {
context.debug(
"Skipping element '%s' because package '%s' is outside builderGenerationPackages.",
element.getSimpleName(), packageName);
return true;
}
return false;
});

// Register the types that will actually be generated so the builder scope resolver can trust
// in-compilation references without an expensive type-existence search.
context.setGeneratedTypeNames(elementsToProcess);

context.info("simple-builders: PROCESSING ROUND START");
context.debug(
"simple-builders: Processing round started. Found %d annotated elements.",
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
/*
* MIT License
*
* Copyright (c) 2026 Andreas Igel
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package org.javahelpers.simple.builders.processor.analysis;

import java.util.Optional;
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.TypeElement;
import org.javahelpers.simple.builders.core.annotations.SimpleBuilder;
import org.javahelpers.simple.builders.processor.model.core.BuilderConfiguration;
import org.javahelpers.simple.builders.processor.model.type.TypeName;
import org.javahelpers.simple.builders.processor.processing.ProcessingContext;

/**
* Resolves whether a builder type may be referenced for a given DTO type.
*
* <p>This resolver centralizes the decision so that generators and enhancers can keep relying on
* {@link TypeName#getBuilderType()} without knowing about package scoping rules.
*/
public class BuilderScopeResolver {
private final ProcessingContext context;

public BuilderScopeResolver(ProcessingContext context) {
this.context = context;
}

/**
* Returns whether the given package is inside the configured builder generation scope.
*
* <p>An empty generation scope means "unscoped" and returns {@code true} for every package.
*/
public boolean isInGenerationScope(String packageName) {
return context
.getConfigurationReader()
.getGlobalConfiguration()
.isInGenerationScope(packageName);
}

/**
* Returns whether the given package is inside the configured builder usage scope.
*
* <p>An empty usage scope means "unscoped" and returns {@code true} for every package.
*/
public boolean isInUsageScope(String packageName) {
return context.getConfiguration().isInUsageScope(packageName);
}

/**
* Resolves the builder type that may be used for the referenced type, if any.
*
* <p>The decision is based on the configured {@code builderGenerationPackages} and {@code
* builderUsagePackages} lists and preserves the existing opt-out rules for types that must not
* have a builder reference.
*
* @param referencedType the type for which a builder reference may be emitted
* @param resolverContext the processing context used for type lookup
* @return the builder type to use, or empty if no reference is allowed
*/
public Optional<TypeName> resolveUsableBuilderType(
TypeElement referencedType, ProcessingContext resolverContext) {
if (JavaLangAnalyser.findAnnotation(
referencedType,
org.javahelpers.simple.builders.core.annotations.Ignore4BuilderGeneration.class)
.isPresent()) {
return Optional.empty();
}

if (!hasBuilderTemplateAnnotation(referencedType)) {
return Optional.empty();
}

TypeName candidate = JavaLangMapper.createBuilderTypeName(referencedType, resolverContext);

// Trust builders that the current processing round will actually generate, regardless of
// package scoping. This must be the first decision so a filtered local type is never trusted.
if (resolverContext.isGeneratedType(referencedType)) {
return Optional.of(candidate);
}

String packageName =
JavaLangMapper.extractPackageName(referencedType.getQualifiedName().toString());

// For external/precompiled types, use the globally configured generation scope as the source
// of truth and the per-target usage scope for optional references.
BuilderConfiguration globalConfig =
resolverContext.getConfigurationReader().getGlobalConfiguration();
BuilderConfiguration config = resolverContext.getConfiguration();

Set<String> generationPackages = globalConfig.getBuilderGenerationPackagesSet();
Set<String> usagePackages = config.getBuilderUsagePackagesSet();

if (generationPackages.isEmpty() && usagePackages.isEmpty()) {
return Optional.of(candidate);
}

if (isPackageIn(generationPackages, packageName)) {
return Optional.of(candidate);
}

if (isPackageIn(usagePackages, packageName)
&& resolverContext.getTypeElement(candidate.getFullQualifiedName()) != null) {
return Optional.of(candidate);
}

return Optional.empty();
}

private boolean hasBuilderTemplateAnnotation(TypeElement typeElement) {
for (AnnotationMirror mirror : context.getAllAnnotationMirrors(typeElement)) {
TypeElement annotationTypeElement = (TypeElement) mirror.getAnnotationType().asElement();
if (annotationTypeElement != null
&& JavaLangAnalyser.findAnnotation(annotationTypeElement, SimpleBuilder.Template.class)
.isPresent()) {
return true;
}
}
return false;
}

private static boolean isPackageIn(Set<String> packages, String packageName) {
if (packageName == null || packages.isEmpty()) {
return false;
}
for (String p : packages) {
if (packageName.equals(p) || packageName.startsWith(p + ".")) {
return true;
}
}
return false;
}
}
Loading