Repository navigation
Accept static factories in the referenced-builder contract (#309) - #315
Merged
AndreasIgel merged 13 commits intoOct 3, 2026
Merged
Conversation
) 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
Bot
force-pushed
the
devin/static-factory-contract
branch
from
October 3, 2026 08:36
a35ec60 to
e82f1a1
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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:
B()or an accessible static parameterless factory returningB(e.g.create(), MapStruct-style).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
newwhen the builder offers one — including our own generated builders, which resolve withcreate()as their empty path sinceCoreMethodsEnhanceralways emits it.How it travels
resolveUsableBuilderTypenow returnsResolvedBuilder— a small record (builderTypeName+ optional factory method names for the empty and copy paths) stored onTypeName/TypeNameGenericin place of the plain builderTypeName.getBuilderType()/getElementBuilderType()keep their signatures; generators that instantiate usegetResolvedBuilder()/getElementResolvedBuilder().Latent gap fixed along the way
createFieldConsumerWithElementBuildersunconditionally emittedElementBuilder::create— a method reference that does not exist on contract-satisfying builders lacking a factory. It now emits the resolved instantiation (B::createwhen present,B::newotherwise).Tests
resolverUsageScope_AcceptsBuilderWithStaticFactories— a builder whose only instantiation paths arecreate()/of(T)resolves, carrying the factory names.usageScope_InstantiatesViaStaticFactoryWhenPresent— generated code emitsB.create()for the empty path andnew B(value)when no copy factory exists.usageScope_InstantiatesViaFactoriesOnly— a private-ctor factory-only builder is emitted purely viaof(...)/create(), nevernew.B.create().478 processor tests green.
Notes
create()rather thannew— consistent with "call the factory when present"; happy to keepnewthere if you prefer.create, thenof, then any other accessible static method with a matching signature (deterministic order) —getInstance-style names rank last intentionally.