Skip to content

Accept static factories in the referenced-builder contract (#309) - #315

Merged
AndreasIgel merged 13 commits into
java-helpers:mainfrom
igel-devin-ai:devin/static-factory-contract
Oct 3, 2026
Merged

AndreasIgel merged 13 commits into
java-helpers:mainfrom
igel-devin-ai:devin/static-factory-contract

Conversation

@igel-devin-ai

Copy link
Copy Markdown
Contributor

Issues: resolves #309 · sub-task of #301 · builds on #313 (#314)

What changed

The referenced-builder contract no longer requires constructors specifically — it requires instantiation paths:

  • Empty instance: a no-arg B() or an accessible static parameterless factory returning B (e.g. create(), MapStruct-style).
  • Value-seeded instance: a B(T) or an accessible static factory accepting the referenced type (create(T) / of(T)). A factory is preferred over the copy constructor when both exist.
  • build() unchanged.

Generated helper code calls the factory instead of new when the builder offers one — including our own generated builders, which resolve with create() as their empty path since CoreMethodsEnhancer always emits it.

How it travels

resolveUsableBuilderType now returns ResolvedBuilder — a small record (builder TypeName + optional factory method names for the empty and copy paths) stored on TypeName/TypeNameGeneric in place of the plain builder TypeName. getBuilderType()/getElementBuilderType() keep their signatures; generators that instantiate use getResolvedBuilder()/getElementResolvedBuilder().

Latent gap fixed along the way

createFieldConsumerWithElementBuilders unconditionally emitted ElementBuilder::create — a method reference that does not exist on contract-satisfying builders lacking a factory. It now emits the resolved instantiation (B::create when present, B::new otherwise).

Tests

  • resolverUsageScope_AcceptsBuilderWithStaticFactories — a builder whose only instantiation paths are create()/of(T) resolves, carrying the factory names.
  • usageScope_InstantiatesViaStaticFactoryWhenPresent — generated code emits B.create() for the empty path and new B(value) when no copy factory exists.
  • usageScope_InstantiatesViaFactoriesOnly — a private-ctor factory-only builder is emitted purely via of(...)/create(), never new.
  • Golden expectations updated where same-round builders now emit B.create().

478 processor tests green.

Notes

  • Same-round generated builders resolve via create() rather than new — consistent with "call the factory when present"; happy to keep new there if you prefer.
  • Factory name matching prefers create, then of, then any other accessible static method with a matching signature (deterministic order) — getInstance-style names rank last intentionally.

)

The builder contract now accepts static factories as instantiation paths
alongside constructors: a parameterless create()/of()-style method for
the empty instance and a create(T)/of(T) method for the value-seeded
instance, each verified accessible like the other contract members.
Factories are preferred over constructors when both exist, and generated
consumer code calls them instead of new.

Resolution results are carried in a new ResolvedBuilder record (builder
TypeName + chosen factory methods) stored on TypeName/TypeNameGeneric in
place of the plain builder TypeName, so generators can emit the right
instantiation. Builders generated in the same round resolve with
create() as their empty path since our generators always emit it.

This also fixes a latent gap in createFieldConsumerWithElementBuilders,
which unconditionally emitted ElementBuilder::create and broke for
referenced builders without that factory - it now emits the resolved
instantiation as a method reference (B::create or B::new).
ResolvedBuilder no longer carries factory method names as Optional<String>
where absence silently meant 'use the constructor'. The instantiation
decision the contract check made is now recorded explicitly as a sealed
BuilderInstantiation - ConstructorCall or StaticFactoryCall(methodName) -
and generation sites render exactly that path for call sites and method
references.
resolveByBuilderContract now reads as find-and-assign: resolving the
empty and prefilled instantiation paths is delegated to
resolveFuncForEmptyBuilder and resolveFuncForPrefilledBuilder, and the
ResolvedBuilder components are renamed accordingly.

findStaticFactory drops the (parameterCount, singleExpectedType)
asymmetry for a List<String> of expected parameter types matched by a
hasParameters helper - the same shape for zero, one, or any arity.
The resolver no longer composes a generic find with preferred-name
selection: StaticFactoryCall.forEmptyBuilder and forPrefilledBuilder
encapsulate the classpath lookup and the create/of preference, so
resolveFuncForEmptyBuilder/resolveFuncForPrefilledBuilder read as a
single find per path before the constructor check.
One shared scan - accessible members filtered by parameter types and
return type - now backs findStaticFunction (names of static matches,
expectedReturnType null meaning void) and hasBuildMethodReturning
(name 'build' additionally). hasParameters also serves both constructor
checks, dropping the duplicated size/first-param comparisons.
…hodsStatic

Expected parameter and return types in the member-scanning API are now
TypeName instead of qualified-name strings; the resolver carries the
referenced type as TypeName end to end. findStaticFunction and
findFunctionsBySignature are renamed to findMethodsStatic and findMethods
to match the method-finding vocabulary.
The factory-candidate lookup and create/of preference live in
BuilderScopeResolver.findStaticFactoryCall again; StaticFactoryCall is
back to a pure methodName value holder with no JavaLangAnalyser or
ProcessingContext dependency.
A lightweight package+simple-name mapping alongside map2TypeName, which
also resolves builder/constructor info and would recurse back into the
resolver; both resolution sites now share the mapper.
Return-type and parameter-type checks shared the same
toString-equals-fullQualifiedName comparison; both now go through
hasType(TypeMirror, TypeName).
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/static-factory-contract branch from a35ec60 to e82f1a1 Compare October 3, 2026 08:36
@codecov

codecov Bot commented Oct 3, 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!

@sonarqubecloud

sonarqubecloud Bot commented Oct 3, 2026

Copy link
Copy Markdown

@AndreasIgel
AndreasIgel merged commit 6693c0e into java-helpers:main Oct 3, 2026
6 checks passed
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.

Support static factory methods for referenced builder creation

2 participants