Skip to content

Emit direct setter last within same-name builder method overloads - #325

Merged
AndreasIgel merged 5 commits into
java-helpers:mainfrom
igel-devin-ai:devin/1791101170-setter-last-order
Oct 4, 2026
Merged

AndreasIgel merged 5 commits into
java-helpers:mainfrom
igel-devin-ai:devin/1791101170-setter-last-order

Conversation

@igel-devin-ai

@igel-devin-ai igel-devin-ai commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Preparation for #300 — fixes #328. No behavior change for Java callers.

Generated builders emit several overloads per property name: the direct setter (name(String), nickNames(List)) plus helper methods (name(Supplier<String>), name(Consumer<...>), nameUpdate(...), varargs/format helpers). Their emission order was incidental — driven by signature sort — which left the direct setter buried before the helpers.

This PR makes member order deterministic and keeps each field's methods together — and it moves the decision into the generation scope where field context exists: BuilderToGenerationTypeMapper sorts the (field, method) pairs by ordering (create 200 → fluent 1000 → conditional 1100 → build 1200 → toString 2000 → private helpers 2100), access modifier (public first, private last), non-static before static, originating field (alphabetically, builder-level methods last), method name — and within a same-named overload group the direct property setter last — then emits already-ordered MethodDtos. MethodDto drops its ordering field and comparator entirely: it carries no generation-scope metadata. The code generator's remaining signature safety net degrades to keep-first-occurrence with a warning (error in strict mode); the real priority-based conflict resolution stays in BuilderDefinitionCreator untouched.

Why

Consumers that resolve overloaded builder methods last-wins then bind to the plain setter — the motivating consumer is MapStruct (#300): it takes the last single-argument write accessor per property name on a builder type. Field cohesion is preserved: every birthdate*/name* cluster stays adjacent instead of setters being pulled to the end. Documented in docs/CUSTOMIZING.md next to the generator priority table.

Notes

  • Java callers are unaffected: javac resolves overloads by signature, not declaration order.
  • example/generated-example-builder/ is regenerated — the large diff is pure method reordering, no content changes.
  • Golden tests updated to the new deterministic order; directSetters_emittedAfterHelperOverloads verifies the setter-last guarantee for every property.

@codecov

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

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1791101170-setter-last-order branch from ffccf3f to 62f4b9e Compare October 4, 2026 09:01
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1791101170-setter-last-order branch from 62f4b9e to 2de1c6f Compare October 4, 2026 09:47
…ld, name

Introduces a member ordering that keeps each field's methods together
and emits the direct property setter last within its overload group,
so consumers that resolve overloaded builder methods last-wins (e.g.
MapStruct's builder detection) bind to the plain setter.

The emission order is decided on the generation side:
BuilderToGenerationTypeMapper sorts the (field, method) pairs by
ordering value, access modifier (public first), non-static before
static, originating field (alphabetically, builder-level methods
last), method name, direct setter last, then the existing
tie-breakers, and emits already-ordered MethodDtos. MethodDto drops
its ordering field and comparator — it no longer carries
generation-scope metadata. The generator-side signature safety net
degrades to keep-first-occurrence with a warning (error in strict
mode); real conflict resolution stays priority-based in
BuilderDefinitionCreator. Regenerates example builders and golden
test fixtures.
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1791101170-setter-last-order branch from 2de1c6f to afb03e0 Compare October 4, 2026 10:03
Comment on lines +317 to +304
return signatureToMethod.values().stream().sorted(comparator).toList();
return List.copyOf(signatureToMethod.values());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need here something, that preserves the ordering?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The incoming list is already in final emission order (sorted in BuilderToGenerationTypeMapper on the generation side), so preserving insertion order is all that's needed — now explicit via the Set+List dedup in b31b344.

" Unexpected duplicate method signature: '%s' — keeping first occurrence (safety net)",
signature);
for (MethodDto method : methods) {
if (signatureToMethod.putIfAbsent(method.getSignatureKey(), method) != null) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting it to a map and then doing a list out of it does not preserve the ordering!?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedHashMap does preserve insertion order — but you're right that the map indirection hid the intent. Simplified in b31b344 to a Set<String> of seen signature keys + a List<MethodDto>: seen.add() returns false on duplicates, so the result keeps the mapper's emission order explicitly with take-first semantics.

Comment on lines +254 to +263
if (groupKey1 == null && groupKey2 == null) {
return 0;
}
if (groupKey1 == null) {
return 1;
}
if (groupKey2 == null) {
return -1;
}
return groupKey1.compareTo(groupKey2);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there nothing from String, Strings or StringUtils?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — StringUtils.compare(a, b, false) does exactly this (nulls-last). Replaced compareGroupKeys with it in 49efe2c.

Consumer<DefaultFormatDtoBuilder> yesCondition) {
return conditional(condition, yesCondition, null);
}
/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and in whole file the starting spaces are changed now. Looks not correct to me! And is an unnecessary change in git history

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 49efe2c — the block keeps its original indentation now, so the diff only shows the ordering changes.

…t compare

- FormattingModeTest: keep expectedSection2's original block
  indentation so the diff shows only ordering changes.
- BuilderToGenerationTypeMapper: use StringUtils.compare(a, b, false)
  for the nulls-last group-key comparison instead of a hand-rolled
  helper.
- RoasterCodeGenerator: note that LinkedHashMap preserves the
  mapper's emission order through the safety-net dedup.
Keeps a HashSet of seen signature keys and appends each unseen method
to the result list, so duplicate signatures keep the first occurrence
in emission order and no map indirection is needed.
…eTest

The duplicate-signature branch is unreachable through the builder
pipeline because BuilderDefinitionCreator resolves conflicts first;
feeding identical MethodDto signatures straight into the generator
verifies the take-first semantics and the strict-mode warning.
Replaces the three accumulation loops with a Stream.concat pipeline
feeding the emission-order sort; semantics unchanged.
@sonarqubecloud

sonarqubecloud Bot commented Oct 4, 2026

Copy link
Copy Markdown

@AndreasIgel
AndreasIgel merged commit b31a55c into java-helpers:main Oct 4, 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.

Field-aware member ordering in generated builders

2 participants