Skip to content

feat: add @SimpleBuilderFor for generating builders of external types (#293) - #296

Open
igel-devin-ai wants to merge 18 commits into
java-helpers:mainfrom
igel-devin-ai:devin/simple-builder-for-external-types
Open

igel-devin-ai wants to merge 18 commits into
java-helpers:mainfrom
igel-devin-ai:devin/simple-builder-for-external-types

Conversation

@igel-devin-ai

@igel-devin-ai igel-devin-ai commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Implements #293: a new @SimpleBuilderFor annotation that generates builders for types that cannot carry @SimpleBuilder themselves — external/library classes or any type you don't want to annotate.

@SimpleBuilderFor(
    value = {ExternalUser.class, ExternalOrder.class},
    options = @SimpleBuilder.Options(builderSuffix = "Factory"))
public class ExternalBuilders {}

Placement: value accepts one or more Class<?>; options reuses SimpleBuilder.Options and falls back to compiler defaults. Generated builders are written to the holder class's package (the target-package selection stays a separate concern, #294). Naming follows the existing <SimpleName><builderSuffix> convention.

Processor wiring (BuilderProcessor): holders are collected via getElementsAnnotatedWith(SimpleBuilderFor.class), expanded into ElementToGenerate(element, config, builderPackage, reportingElement) where builderPackage is the holder's package and reportingElement routes diagnostics to the holder. Config resolves via a new BuilderConfigurationReader.resolveExternalConfiguration (defaults → compiler args → options attribute only — the foreign type's own annotations are ignored). Duplicate builder names (two holders, or direct @SimpleBuilder + @SimpleBuilderFor) are skipped with a warning, as are @Ignore4BuilderGeneration targets.

Scoping semantics: an explicit declaration always generates — builderGenerationPackages does not filter @SimpleBuilderFor targets; a conflicting scope emits a warning instead. BuilderScopeResolver now registers the actual builder TypeName per target (registerGeneratedBuilders(Map)); the builderUsagePackages filter keeps applying to all references, including generated-in-round builders — its position ahead of the generated-builder lookup is unchanged.

Accessibility: generation fails with a clear diagnostic (warning; error under -Asimplebuilder.strict=true) when the target type isn't visible from the builder package or has no accessible constructor — ProcessingContext.isMemberAccessibleFromBuilderPackage gates constructor, setter, and getter selection, so generated code only calls members it may legally call (also fixes latent private-constructor/member selection on the @SimpleBuilder path).

Jackson: JacksonModuleGenerator now groups modules by the generated builder's package instead of the target's.

Example: example/ExternalTypeBuilders + external/ExternalAddress produce example/ExternalAddressBuilder (committed under generated-example-builder/), exercised by ExternalAddressBuilderTest. The surefire/junit-engine harness fix for the example module is split out: #297

Tests: new SimpleBuilderForTest (12 cases: multi-value, options, records, inaccessible members, no-accessible-ctor, strict mode, collisions, scope override warning, consumer wiring). Updated BuilderScopeResolverTest for the Map-based registration and the verbose log assertions for the new log lines. mvn clean verify: BUILD SUCCESS (462 processor tests + 30 example tests).

@codecov

codecov Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Comment thread docs/CONFIGURATION.md Outdated
Comment thread docs/CONTRIBUTING.md Outdated
Comment thread docs/CONFIGURATION.md Outdated
Comment thread docs/CONFIGURATION.md Outdated
devin-ai-integration Bot and others added 18 commits September 25, 2026 15:50
…java-helpers#293)

Introduce @SimpleBuilderFor on a holder class to generate builders for
types that cannot carry @SimpleBuilder themselves (e.g. third-party
library classes). The builders are generated in the holder's package,
configured via the annotation's options attribute and compiler options.

- New @SimpleBuilderFor annotation in core with Class<?>[] value and
  optional SimpleBuilder.Options options
- BuilderProcessor expands holders into generation targets; builder
  name collisions and @Ignore4BuilderGeneration targets are skipped
  with warnings on the holder
- Explicitly declared types always get a builder, even outside
  builderGenerationPackages (contradictory config warns)
- Builders generated in the same round are now trusted and exempt
  from builderUsagePackages - a self-generated builder is always used
- Member selection (constructors, setters, getters) now respects
  member accessibility from the generated builder's package, so
  generated code only calls members it may legally call
- Clear compile-time diagnostics when no accessible constructor or
  no accessible target type exists
- Jackson module defaults to the generated builder's package
- Example module: holder + external type + test; also enable surefire
  so the module's tests actually execute (they silently ran 0 before)
- Tests: SimpleBuilderForTest with 12 cases; updated scope tests for
  the trusted-builder semantics and the new round-start log line

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…r package from reporting element, tighten docs

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…ts, concrete map types

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>
Generated-in-round builders must honor the usage scope like any other
candidate: restoring the original resolve() ordering — usage-scope check
first, then the registered-builder lookup. The Map-based registration
(registerGeneratedBuilders) stays, since @SimpleBuilderFor targets need
their builder names resolved in non-default packages.

Reverts the scoping example and test expectations to upstream semantics.

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…n wins, API cleanup

- Example + docs now declare @SimpleBuilderFor on package-info.java
  (holder class renamed to ExternalBuildersProvider in javadoc examples)
- @Ignore4BuilderGeneration on an explicitly listed target no longer
  suppresses generation: the external type's own annotations are not
  consulted; docs and test updated accordingly
- Round-start logging counts both element kinds identically; the
  'nothing found' message only appears when both are empty
- resolveExternalConfiguration(Element) locates the annotation mirror
  itself; the unreachable orElseThrow guard is dropped
- BuilderScopeResolver offers a single
  registerGeneratedBuilders(Map<TypeName,TypeName>) entry point
- builderPackageOf always returns a concrete package (the reporting
  element's package), so the builder-package slot in ProcessingContext
  never carries a null sentinel; getBuilderPackageName/
  isMemberAccessibleFromBuilderPackage simplified

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- builderTypeName now returns TypeName instead of a qualified-name String;
  plannedBuilderNames is Set<TypeName> and the direct-target call site
  passes the type's own package (no null sentinel)
- drop 'No elements to process.' — the unconditional 'Found N ...' counts
  already report empty rounds

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
Introduce ProcessingTarget (configuration + builderPackage) as the
documented holder for per-target processing state. ProcessingContext now
carries a single field instead of two loosely related ones, set once via
initProcessingTarget before extraction. The record's javadoc explains
why the builder package is captured explicitly (not derivable for
@SimpleBuilderFor targets).

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
… holder

The target->builder-name mapping now lives in a dedicated GeneratedBuilders
registry (analysis package) with add/find/clear helpers and javadoc
documenting why the builder TypeName is stored explicitly (holder-package
builders for @SimpleBuilderFor). BuilderScopeResolver owns it and exposes
it via generatedBuilders(); mutations clear the resolution cache through
an onChange callback.

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
…y API

- renames: planGenerationOfAnnotatedElement, planGenerationOfTypeByHolder,
  alreadyPlannedBuilders (with comment explaining its conflict-detection
  role), results, resolveSimpleBuilderForConfiguration
- attribute reading moved to JavaLangAnalyser.findAnnotationAttribute;
  also tolerates a non-list 'value' attribute value
- GeneratedBuilders is now a pure holder keyed by TypeName
  (add/findBuilder/clear, add returns whether it was a new registration);
  BuilderScopeResolver gained registerGeneratedBuilder and
  resetGeneratedBuilders which also drop the resolution cache - replaces
  the onChange callback

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- Objects.requireNonNull on holder/element at entry points - Sonar's
  null analysis flagged getSimpleName() derefs after the defensive
  null-check in findAnnotation propagated 'may be null' upstream
- lambdas replaced by context::isMemberAccessibleFromBuilderPackage
  method references
- dropped unused annotatedType parameter from isMethodRelevantForBuilder

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- rename targets to results in extractExternalTargetTypes
- drop the non-list 'value' tolerance - javac always delivers Class<?>[]
  attributes as a list, single-value declarations included
- rename to resolveHolderConfiguration - 'For' in the name was misleading

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- warn when the 'value' attribute of @SimpleBuilderFor cannot be read,
  instead of returning silently
- javadoc on options(): spell out the UNSET default resolution
  (compiler argument, then built-in default per Options member)

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- SimpleBuilderFor: link 'Generating Builders for External Types' and
  'Compiler Options' sections
- JacksonAnnotationEnhancer: fix stale CONFIGURATION.md link (file moved
  to docs/, repo moved to java-helpers)

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
- branch predated java-helpers#297 and its pom revert dropped the merged surefire /
  junit-jupiter-engine setup; pom restored to upstream so the example
  tests keep running
- javadoc: state explicitly that @SimpleBuilderFor is not @inherited

Co-Authored-By: Andreas Igel <andreas.igel@computacenter.com>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/simple-builder-for-external-types branch from 0d361b2 to 834969f Compare September 25, 2026 15:52
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants