Scope builder generation and builder usage by package (#114) - #288
Conversation
The class-level Javadoc and docs/CONFIGURATION.md implied that @SimpleBuilder is inherited by subclasses, but the annotation was not meta-annotated with @inherited. As a result BuilderProcessor, which collects types via RoundEnvironment.getElementsAnnotatedWith(...), only produced builders for the exact type carrying @SimpleBuilder and not for unannotated subclasses. Add @inherited to @SimpleBuilder so subclasses are treated as if they also carried the annotation, mirroring the existing behaviour of @SimpleBuilder.Template (which is already @inherited). Update the Javadoc to document the inheritance explicitly and clarify the CONFIGURATION.md wording. @Ignore4BuilderGeneration still suppresses generation for the exact type it is placed on, so opt-outs continue to work as before. Add SimpleBuilderInheritanceTest covering direct inheritance, the opt-out interaction, and multi-level (grandchild) inheritance. Closes java-helpers#244 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ance Rename SimpleBuilderInheritanceTest to BuilderAnnotationInheritanceTest so the name reflects that it covers both builder-triggering annotations. Add unannotatedSubclassGetsBuilderFromInheritedTemplate, which verifies that a custom @inherited template annotation (meta-annotated with @SimpleBuilder.Template) propagates to unannotated subclasses, matching the existing behaviour of @SimpleBuilder itself. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The Template Javadoc and CONFIGURATION.md "Template Annotations" section did not explain that @SimpleBuilder.Template is @inherited, nor that a custom template annotation must additionally declare @inherited to propagate to unannotated subclasses. Add explicit documentation and an example showing the @inherited custom annotation pattern. Also move assertNoBuilderGenerated to ProcessorAsserts so it is shared by BuilderAnnotationInheritanceTest and Ignore4BuilderGenerationTest instead of being duplicated as a private helper in each test class. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
While @SimpleBuilder and @inherited template annotations now correctly trigger builder generation for unannotated subclasses, the configuration options declared on the parent's @SimpleBuilder(options = ...) or template are not yet applied to inherited subclass builders — they use default options instead. This is tracked separately in issue java-helpers#245. Add caveats to the SimpleBuilder Javadoc, the CONFIGURATION.md Template Annotations section, and the Template Annotations Not Working troubleshooting section so users are not surprised by this limitation. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Add explicit guidance that @SimpleBuilder.Template is a meta-annotation for custom annotation declarations (@interface) only and cannot be placed directly on a class or record. @SimpleBuilder is for direct one-off annotation of classes/records. - SimpleBuilder.java: add 'When to use' section to class-level Javadoc - SimpleBuilder.Template Javadoc: state it can only be placed on annotation types (ANNOTATION_TYPE), not on classes/records - CONFIGURATION.md 'Template Annotations': add comparison table and introductory paragraph - CONFIGURATION.md troubleshooting: add item about @SimpleBuilder.Template not being a class annotation Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…Javadoc Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…arget The compiler and IDE already enforce @target(ANNOTATION_TYPE) and show a clear error when @SimpleBuilder.Template is placed on a class/record, so this troubleshooting item adds no value. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…rs#114) Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…a-helpers#114) Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Upstream split the completeness test into buildFullyConfigured() helper; kept the refactor and added the new builder scoping options to it.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| */ | ||
| public static boolean isInScope(String packageName, Set<String> packageScopes) { | ||
| for (String scope : packageScopes) { | ||
| if (StringUtils.equalsIgnoreCase(packageName, scope) |
There was a problem hiding this comment.
Already fixed in an earlier commit.
There was a problem hiding this comment.
Confirmed — the deprecated StringUtils calls were replaced with Strings.CI equivalents in an earlier commit.
| public static boolean isInScope(String packageName, Set<String> packageScopes) { | ||
| for (String scope : packageScopes) { | ||
| if (StringUtils.equalsIgnoreCase(packageName, scope) | ||
| || StringUtils.startsWithIgnoreCase(packageName, scope + ".")) { |
There was a problem hiding this comment.
already done...
There was a problem hiding this comment.
Already fixed in an earlier commit.
There was a problem hiding this comment.
Confirmed — the string getters were removed in the latest commit. Only the set getters remain.
StringUtils.equalsIgnoreCase/startsWithIgnoreCase are deprecated in commons-lang3 in favor of Strings.CI.
…derProcessor.process (java-helpers#114) Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
The helper is still wanted on this branch; it was dropped together with other dead-code removals in d58af17.
BuilderConfiguration held a raw comma-separated string plus the matching/parsing logic, re-parsing on every access. The record now stores an immutable PackageScopes; scope matching moved out of the configuration holder.
Restores the historical name from BuilderConfigurationReader and folds the String.valueOf into it, so the applier wrappers read parse*(extractEnumName(value)).
apply() already no-ops for options without a builder applier; the semantic check stays in parseOptionsFromMirror where it feeds the unknown-option warning.
parseOptionState/parseAccessModifier moved to new OptionValueParsers in the processing package; the enum now only registers option names and appliers.
ROUND START log now precedes element collection, and the collection + sort work is measured as a new top-level "Element Collection" phase so the report covers the whole round.
The unscoped shortcut, package extraction and skip logging are scope semantics and belong to the resolver; the processor now asks isInGenerationScope(element, config) directly.
- Add assertNotesInOrder to ProcessorAsserts: asserts exact note count and ordered substring match so any added, removed, or modified log message fails the test - Rewrite BuilderProcessorTest verbose test to use assertNotesInOrder with all 65 expected notes in order - Remove duplicate "Configuration resolved" log from BuilderProcessor (already logged by BuilderConfigurationReader as operation closer) - Update DEBUG_LOGGING.md and CONTRIBUTING.md examples to include scope-filter summary and second-round messages - Update CONFIGURATION.md phase list to include Element Collection Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ckerTest Add two dedicated tests covering the Element Collection phase: - Text report contains the phase name - JSON phaseBreakdown includes the phase with elapsedNanos/seconds/percentage Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
resolveUsableBuilderType reads config from context and assumes initConfigurationForProcessingTarget was already called; isInGenerationScope takes config as explicit parameter because it runs before that init. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Production code: - OptionValueParsers: log warnings for unrecognized non-blank values before falling back to UNSET/DEFAULT; logger is now a required parameter (no more no-logger overloads or null checks) - CompilerArgumentsEnum: thread ProcessingLogger through apply() via new OptionApplier functional interface; remove no-logger apply overload - CompilerArgumentsReader: replace Arrays.stream with Stream.of; remove no-arg readBuilderConfiguration overload and test-only readOptionState/ readAccessModifier convenience methods - BuilderConfigurationReader: remove .stream() from forEach; extract lambda body to applyOption helper method; pass logger to apply() - BuilderConfiguration: remove raw string getters getBuilderGenerationPackages() and getBuilderUsagePackages(); use getBuilderGenerationPackagesSet() / getBuilderUsagePackagesSet() instead - SimpleBuilder.java: clarify builderUsagePackages default javadoc Tests: - CapturingProcessingLogger: new shared test helper in testing package, replacing duplicated CapturingMessager/createLogger boilerplate - OptionValueParsersTest: new test class covering parsing and warning logging - ActivePerformanceTrackerTest: use CapturingProcessingLogger; replace cryptic ternary with explicit loop and clarifying comments - CompilerArgumentsReaderTest: switch to set getters; add assertion messages; remove redundant readOptionState/readAccessModifier tests (parsing logic is covered by OptionValueParsersTest) - CompilerArgumentsEnumTest: pass logger to apply() - ConfigurationProcessingTest: undo formatter-induced string concatenations; switch from removed string getters to set getters - BuilderConfigurationReaderTest: remove redundant scope options from test - BuilderScopeResolverTest: rename tests to match behavior; add assertNotSame for cache invalidation verification 412 processor tests pass. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…javadoc The name isBuilderOption was ambiguous — it could mean "an option of the builder processor" (which includes process flags) or "an option that configures the builder" (which excludes them). hasValueApplier is positive and describes the mechanism: whether the enum constant has an applier that maps the raw option value to a BuilderConfiguration.Builder setter. Added javadoc explaining why not all options have an applier: process-control flags (VERBOSE, PERFORMANCE_TRACKING, PERFORMANCE_OUTPUT_FILE, DEACTIVATE_GENERATION_COMPONENTS) are read directly via CompilerArgumentsReader.readValue/readBooleanValue instead of being applied to a builder configuration. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
even Non-SimpleBuilder Builders could be used it should not depend on the SimpleBuilder annotation Builder-Name suffix should be possibly different on consumed builders generated builders should be trusted (but if they are not in usage scope, they should be ignored too)
Normalize package scopes for stable output and keep parsing and matching coverage with the PackageScopes abstraction. Remove redundant configuration scope helpers and their duplicate tests. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace formatter-specific logging stubs with CapturingProcessingLogger and remove the obsolete note assertion helper now superseded by ordered checks. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|



Summary
Previously the processor emitted a builder reference for every
@SimpleBuilder-annotated field/collection-element type. That breaks when the referenced type is a precompiled library DTO whose builder was never generated: the generated code references a class that does not exist.Two comma-separated package-list options now scope this, and the "may a builder be used here?" decision is centralized in one new class so generators/enhancers stay ignorant of the mechanism — they keep checking only
TypeName.getBuilderType().isPresent().builderGenerationPackages— when non-empty,BuilderProcessoronly generates builders for annotated DTOs in scope. Generation-scope packages are automatically included in the usage scope.builderUsagePackages— packages whose builders may be used as helpers. The candidate builder must be verified on the classpath before a reference is emitted; unresolvable ⇒ the field falls back to a plain setter.builderUsageSuffix— suffix used when looking up usage builders on the classpath (defaults tobuilderSuffixwhen empty). Allows referencing builders generated with a different suffix, e.g.LibraryDtoFactoryinstead ofLibraryDtoBuilder.Builder contract instead of annotation check
Classpath lookup does not check for
@SimpleBuilder(or any other annotation). Instead, the candidate class must satisfy the builder contract:build()method returning the referenced type (used to obtain the built value)This is annotation-agnostic, so builders generated with custom
@SimpleBuilder.Templateannotations or external tools are supported. It also avoids false positives likeString→StringBuilder.Decision logic
All in
BuilderScopeResolver(analysis/), created once in theProcessingContextconstructor (getBuilderScopeResolver()); it reads the current target configuration on use, re-parses its package scopes only when that configuration changes, and caches the per-type result (Optional<TypeName>by FQN) so each referenced type is analysed once per configuration:The effective usage scope merges
builderGenerationPackagesandbuilderUsagePackagesviaPackageScopes.merge(...), so generation-scope packages never need to be repeated in the usage scope.generatedTypeNamescovers builders being generated in the current processing round — they are trusted without a classpath lookup because our own generators always satisfy the contract.BuilderProcessorresolves configuration and applies the generation filter for all elements first, registers the survivors viaBuilderScopeResolver.registerGeneratedTypes(...), and only then generates. Types in neither scope stay unreferenced even when generated in the same round.Opt-out precedence is structural:
@Ignore4BuilderGenerationis checked first in the resolver, so an opted-out type can never be referenced, even when its package is in scope. The previous check inJavaLangMapperwas folded into the resolver so there is a single decision point; the mapper now only doesresolveUsableBuilderType(...).ifPresent(typeName::setBuilderType)(and the collection-element equivalent).Package matching is case-insensitive and boundary-aware —
Strings.CI.equals(pkg, p) || Strings.CI.startsWith(pkg, p + ".")(soa.bcdoes not match scopea.b, whileCOM.Examplematchescom.example) — implemented once inPackageScopes(model/core/) and shared by the resolver andBuilderProcessor.PackageScopes.mergecombines two scopes into a union;BuilderConfiguration.overrideScopes(renamed frommergeScopes— it is an override, not a merge) resolves annotation-level values over compiler-argument-level ones.Wiring is the usual three layers:
SimpleBuilder.Options,CompilerArgumentsEnum/CompilerArgumentsReader(-Asimplebuilder.builderGenerationPackages,-Asimplebuilder.builderUsagePackages,-Asimplebuilder.builderUsageSuffix),BuilderConfigurationReader.parseOptionsFromMirror, andBuilderConfiguration(record field,DEFAULT, builder setter viatrimToNull,merge,toString, getters).Example module
A runnable demo lives in
org.javahelpers.simple.builders.example.scoping, with the generated result committed underexample/generated-example-builder/.ScopedOwnerDtoconfigures both scopes inline and has three fields covering all three outcomes:ScopedOwnerDtoBuildertrustedtrusted(Consumer<TrustedHelperDtoBuilder>)emittedlibrarysponsorSponsorDtoBuilderexistsLibraryHelperDtois annotated but configured outside the generation scope, so no builder is generated for it — that is how a precompiled library DTO without a builder is simulated inside one compilation.Note: the
examplemodule's tests are compiled but never executed by Maven (the reactor inherits Surefire 2.12.4 and the module has no JUnit 5 engine), soScopedOwnerDtoBuilderTestis compile-checked in CI and was run out-of-band via the JUnit Platform launcher (2/2 passing). This is pre-existing and affects the module's other test classes too.Tests
BuilderScopeProcessingTestcovers the compile-level behavior: both options unset (regression), exact and subpackage generation matching, out-of-scope DTOs getting no builder, usage scope with an existing vs. missing builder, a same-round helper with only the usage scope set, opt-out precedence, custombuilderUsageSuffix, external builders without@SimpleBuilder, and both the compiler-arg and inline@SimpleBuilder(options = ...)forms.BuilderScopeResolverTestcovers the plain-setter fallback, configuration changes on the same resolver instance, result caching, and registration/scope interaction.PackageScopesTestcovers comma-splitting/trimming, blank ⇒ unscoped, exact-vs-subpackage matching, prefix-boundary rejection (com.examplefoodoes not matchcom.example), case-insensitivity, andmergesemantics. Processor suite: 547 tests green, full reactormvn testgreen.Docs
docs/CONFIGURATION.mdgets "Builder Scoping" sections forbuilderGenerationPackages,builderUsagePackages, andbuilderUsageSuffixplus entries in the compiler-option and complete-example references;docs/CUSTOMIZING.mdnotes that custom generators/enhancers need no changes;README.mdgets a "Builder Scoping Example" section linking the demo files in the same style as the other example sections.