From 76c8e0205250e2289417ccc7063c0e94b442037c Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 15:37:20 +0800 Subject: [PATCH 01/10] fix(rename-class): improve conflict and rename log readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggregate rename conflicts into one multi-line WARN with old→new detail lines (simple-name, ancestor-nested, object-factory). Log each applied rename at DEBUG and keep INFO as a count only. Enrich invalid mapping warnings with the rule pattern. --- .../jaxb/plugin/RenameClassPlugin.java | 131 ++++++++++++------ 1 file changed, 90 insertions(+), 41 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java index 4c94f0b..1e22d73 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java @@ -72,7 +72,8 @@ *

*

* Conflict policy. Conflicting types keep their original names; non-conflicting - * renames still apply. Conflicts are reported as warnings (build does not fail). Checks cover: + * renames still apply. Conflicts are reported as a single non-fatal warning with one detail + * line per clash ({@code old→new}). Checks cover: *

* *

+ * Logging: each applied rename is {@code DEBUG}; the total count is {@code INFO}. Conflicts + * are one {@code WARN} (summary + detail lines). Invalid mapping/strip results are {@code WARN}. + *

+ *

* When both this plugin and {@link PromoteNestedClassPlugin} are active, running rename * before promote lets named global types claim short names first; promote-first is * safer for some ObjectFactory edge cases but can leave dual names ({@code Foo} + {@code FooType}). @@ -136,7 +141,6 @@ private static List collect(Model model) { var result = new ArrayList(); for (var bean : model.beans().values()) { result.add(new Candidate( - "bean", bean.shortName, bean.fullName(), bean.parent(), @@ -148,7 +152,6 @@ private static List collect(Model model) { } for (var enumInfo : model.enums().values()) { result.add(new Candidate( - "enum", enumInfo.shortName, enumInfo.fullName(), enumInfo.parent, @@ -163,7 +166,6 @@ private static List collect(Model model) { continue; } result.add(new Candidate( - "element", element.shortName(), element.fullName(), element.parent, @@ -176,6 +178,33 @@ private static List collect(Model model) { return result; } + /** {@code Original→Desired} for a provisional rename (or unchanged name). */ + private static String arrow(Candidate candidate, Map names) { + var desired = names.get(candidate); + return candidate.shortName.equals(desired) + ? candidate.shortName + : candidate.shortName + "→" + desired; + } + + private static String joinArrows(Collection group, Map names) { + var parts = new ArrayList(group.size()); + for (var candidate : group) { + parts.add(arrow(candidate, names)); + } + return String.join(", ", parts); + } + + private static void reportConflicts(List conflicts) { + if (conflicts.isEmpty()) { + return; + } + log.warn( + "Blocked {} rename conflict(s); kept original name(s):\n {}", + conflicts.size(), + String.join("\n ", conflicts) + ); + } + private static String localPart(QName typeName) { return typeName == null ? null : typeName.getLocalPart(); } @@ -241,9 +270,11 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { names.put(candidate, mapName(candidate)); } - blockDuplicateSimpleNames(model, candidates, names); - blockParentChildClashes(candidates, names); - blockObjectFactoryClashes(model, candidates, names); + var conflicts = new ArrayList(); + blockDuplicateSimpleNames(model, candidates, names, conflicts); + blockParentChildClashes(candidates, names, conflicts); + blockObjectFactoryClashes(model, candidates, names, conflicts); + reportConflicts(conflicts); var renamed = 0; for (var candidate : candidates) { @@ -251,6 +282,7 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { if (!next.equals(candidate.shortName)) { candidate.apply.accept(next); renamed++; + log.debug("Renamed {} → {}", candidate.shortName, next); } } if (renamed > 0) { @@ -270,7 +302,8 @@ public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) { private void blockDuplicateSimpleNames( Model model, List candidates, - Map names + Map names, + List conflicts ) { // Slot: (parent, case-insensitive desired name). size > 1 → conflict group. Map> bySlot = new LinkedHashMap<>(); @@ -285,22 +318,15 @@ private void blockDuplicateSimpleNames( if (group.size() <= 1 || !anyPending(group, names)) { continue; } - // Prefer a desired name that was actually produced by a rename (original case). - var desired = group.stream() - .filter(c -> isPending(c, names)) - .map(names::get) - .findFirst() - .orElseGet(() -> names.get(group.getFirst())); - var involved = group.stream().map(c -> c.fullName).toList(); + conflicts.add( + "simple-name under '" + + parentLabel(group.getFirst().parent) + + "': " + + joinArrows(group, names) + ); for (var candidate : group) { names.put(candidate, candidate.shortName); } - log.warn( - "Class name conflict after rename under '{}': desired '{}' for {}; keeping original names", - parentLabel(group.getFirst().parent), - desired, - involved - ); } } @@ -313,14 +339,15 @@ private void blockDuplicateSimpleNames( */ private void blockParentChildClashes( List candidates, - Map names + Map names, + List conflicts ) { var beans = beansByClass(candidates); boolean changed; do { changed = false; for (var child : candidates) { - if (revertAncestorNameClash(child, beans, names)) { + if (revertAncestorNameClash(child, beans, names, conflicts)) { changed = true; } } @@ -329,14 +356,15 @@ private void blockParentChildClashes( /** * When {@code child} and any enclosing bean would share a simple name after rename, - * reverts one of them (prefer the ancestor) and reports a warning. + * reverts one of them (prefer the ancestor) and records a conflict detail. * * @return {@code true} if a rename was reverted */ private boolean revertAncestorNameClash( Candidate child, Map beans, - Map names + Map names, + List conflicts ) { var childName = normalize(names.get(child)); CClassInfoParent current = child.parent; @@ -348,13 +376,31 @@ private boolean revertAncestorNameClash( if (undo == null) { return false; } + var undoDesired = names.get(undo); + if (undo == ancestor) { + conflicts.add( + "ancestor-nested: " + + ancestor.shortName + + "→" + + undoDesired + + " conflicts with nested " + + child.fullName + + "; kept " + + ancestor.shortName + ); + } else { + conflicts.add( + "ancestor-nested: nested " + + child.fullName + + "→" + + undoDesired + + " conflicts with ancestor " + + ancestor.fullName + + "; kept " + + child.shortName + ); + } names.put(undo, undo.shortName); - log.warn( - "Ancestor-nested name conflict after rename ('{}' / '{}'); keeping original name for {}", - ancestor.fullName, - child.fullName, - undo.fullName - ); return true; } current = parentBean.parent(); @@ -377,7 +423,8 @@ private boolean revertAncestorNameClash( private void blockObjectFactoryClashes( Model model, List candidates, - Map names + Map names, + List conflicts ) { var beans = beansByClass(candidates); if (beans.isEmpty()) { @@ -402,17 +449,18 @@ private void blockObjectFactoryClashes( // Clash already present with original names — XJC will report it; nothing we can undo. continue; } - // Capture squeezed name before reverts change short names on the model. + // Capture squeezed name and arrows before reverts change short names on the model. var squeezed = group.getFirst().getSqueezedName(); + conflicts.add( + "object-factory: squeezed '" + + squeezed + + "'; " + + joinArrows(toRevert, names) + ); for (var candidate : toRevert) { names.put(candidate, candidate.shortName); setFieldValue(CCLASSINFO_SHORTNAME_FIELD, candidate.bean, candidate.shortName); } - log.warn( - "ObjectFactory name conflict '{}'; keeping original name(s) for {}", - squeezed, - toRevert.stream().map(c -> c.fullName).toList() - ); changed = true; } } while (changed); @@ -435,8 +483,10 @@ private String mapName(Candidate candidate) { } if (!JJavaName.isJavaIdentifier(next)) { log.warn( - "Invalid Java class name after mapping: '{}' (from {}); skipping rule", + "Invalid Java class name '{}' after mapping /{}/->{} on {}; skipping rule", next, + mapping.from.pattern(), + mapping.to, candidate.fullName ); continue; @@ -449,7 +499,7 @@ private String mapName(Candidate candidate) { if (stripped != null && !stripped.equals(current)) { if (!JJavaName.isJavaIdentifier(stripped)) { log.warn( - "Invalid Java class name after Type suffix strip: '{}' (from {}); skipping strip", + "Invalid Java class name '{}' after Type suffix strip on {}; skipping strip", stripped, candidate.fullName ); @@ -522,7 +572,6 @@ public static class MappingConfig { * named; {@code null} for anonymous beans and element classes. */ private record Candidate( - String kind, String shortName, String fullName, CClassInfoParent parent, From 52b072bb46897038af441d50626dd5957a08aae7 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:05:22 +0800 Subject: [PATCH 02/10] fix(element-wrapper): align logging with rename-class style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use DEBUG for each flatten with Owner.prop → Wrapper, INFO summaries with counts for flatten/remove, and multi-line WARN for kept wrappers (with reason) and stale annotation owners after later model merges. --- .../jaxb/plugin/ElementWrapperPlugin.java | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java index 2ef0eef..7045170 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java @@ -76,6 +76,12 @@ * rewrites that shape into a repeated element list with * {@code @XmlElementWrapper(nillable = true)} and drops the synthetic local element info. *

+ *

+ * Logging: each flattened field is {@code DEBUG}; the flatten count and removed wrapper list + * are {@code INFO}. Skipped wrapper removals and stale annotation owners (plugin-order + * issues) are one multi-line {@code WARN} each. Per-field flatten failures stay single-line + * {@code WARN}. + *

* * @author Rawvoid */ @@ -136,22 +142,25 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { @Override public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) throws SAXException { + var skipped = new ArrayList(); for (var flattened : flattenedFields) { // Never use Outline#getClazz to test liveness: it lazily creates ClassOutline for // beans already removed from the model and desynchronizes beans vs classes. if (!outline.getModel().beans().containsValue(flattened.owner())) { // Stale after a later merge (e.g. dedupe): the class is not generated. Isomorphic // hosts already have their own FlattenedField from this plugin's model pass. - log.info( - "Skipping @XmlElementWrapper for removed flatten owner '{}' property '{}' " - + "(prefer -Xelement-wrapper after other model-mutating plugins)", - flattened.owner().fullName(), - flattened.propertyName() - ); + skipped.add("%s.%s".formatted(flattened.owner().fullName(), flattened.propertyName())); continue; } annotateXmlElementWrapper(outline, flattened); } + if (!skipped.isEmpty()) { + log.warn( + "Skipped {} @XmlElementWrapper annotation(s); owner removed (prefer -Xelement-wrapper after other model-mutating plugins):\n {}", + skipped.size(), + String.join("\n ", skipped) + ); + } return true; } @@ -205,12 +214,19 @@ private void flattenOwner( if (resolved.orphanElementInfo() != null && !removeElementInfo(model, resolved.orphanElementInfo())) { - log.warn("Could not remove synthetic element info for {}.{}", - owner.fullName(), outer.getName(false)); + log.warn( + "Could not remove synthetic element info for {}.{}", + owner.fullName(), + outer.getName(false) + ); } - log.debug("Flattened {}.{} (wrapper {})", - owner.fullName(), outer.getName(false), resolved.wrapper().fullName()); + log.debug( + "Flattened {}.{} → {}", + owner.fullName(), + outer.getName(false), + resolved.wrapper().fullName() + ); } } @@ -367,8 +383,11 @@ private boolean replaceProperty( CElementPropertyInfo replacement ) { if (replacement.ref().isEmpty()) { - log.warn("Skip flattening {}.{}: replacement has no type refs", - owner.fullName(), outer.getName(false)); + log.warn( + "Skip flattening {}.{}: replacement has no type refs", + owner.fullName(), + outer.getName(false) + ); return false; } @@ -378,8 +397,11 @@ private boolean replaceProperty( owner.addProperty(replacement); if (properties.size() != sizeBefore + 1 || properties.getLast() != replacement) { // addProperty no-op'd — model unchanged. - log.warn("Skip flattening {}.{}: addProperty did not append the replacement", - owner.fullName(), outer.getName(false)); + log.warn( + "Skip flattening {}.{}: addProperty did not append the replacement", + owner.fullName(), + outer.getName(false) + ); return false; } @@ -402,21 +424,29 @@ private void removeUnusedWrappers(Model model, Set wrappers) { continue; } if (isReferenced(model, wrapper)) { - kept.add(wrapper.fullName()); + kept.add("%s (still referenced)".formatted(wrapper.fullName())); continue; } if (removeClass(model, wrapper)) { removed.add(wrapper.fullName()); } else { - kept.add(wrapper.fullName()); + kept.add("%s (removeClass failed)".formatted(wrapper.fullName())); } } if (!removed.isEmpty()) { - log.info("Removed wrapper classes:\n {}", String.join("\n ", removed)); + log.info( + "Removed {} wrapper class(es):\n {}", + removed.size(), + String.join("\n ", removed) + ); } if (!kept.isEmpty()) { - log.warn("Skipped removing wrapper classes:\n {}", String.join("\n ", kept)); + log.warn( + "Skipped removing {} wrapper class(es):\n {}", + kept.size(), + String.join("\n ", kept) + ); } } @@ -467,8 +497,11 @@ private void annotateXmlElementWrapper(Outline outline, FlattenedField flattened var classOutline = outline.getClazz(flattened.owner()); var field = classOutline.implClass.fields().get(flattened.propertyName()); if (field == null) { - log.warn("Could not find field {} on {}", - flattened.propertyName(), flattened.owner().fullName()); + log.warn( + "Could not find field {} on {}", + flattened.propertyName(), + flattened.owner().fullName() + ); return; } From cc5f575e661b113238ba12767483ae1e23e5176a Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 15:51:32 +0800 Subject: [PATCH 03/10] refactor(rename-class): prefer formatted templates in conflict messages Replace string concatenation with String.formatted for conflict detail lines and arrow formatting; keep SLF4J placeholders for actual log calls. --- .../jaxb/plugin/RenameClassPlugin.java | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java index 1e22d73..a9cedeb 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java @@ -183,7 +183,7 @@ private static String arrow(Candidate candidate, Map names) { var desired = names.get(candidate); return candidate.shortName.equals(desired) ? candidate.shortName - : candidate.shortName + "→" + desired; + : "%s→%s".formatted(candidate.shortName, desired); } private static String joinArrows(Collection group, Map names) { @@ -318,12 +318,10 @@ private void blockDuplicateSimpleNames( if (group.size() <= 1 || !anyPending(group, names)) { continue; } - conflicts.add( - "simple-name under '" - + parentLabel(group.getFirst().parent) - + "': " - + joinArrows(group, names) - ); + conflicts.add("simple-name under '%s': %s".formatted( + parentLabel(group.getFirst().parent), + joinArrows(group, names) + )); for (var candidate : group) { names.put(candidate, candidate.shortName); } @@ -379,25 +377,15 @@ private boolean revertAncestorNameClash( var undoDesired = names.get(undo); if (undo == ancestor) { conflicts.add( - "ancestor-nested: " - + ancestor.shortName - + "→" - + undoDesired - + " conflicts with nested " - + child.fullName - + "; kept " - + ancestor.shortName + "ancestor-nested: %s→%s conflicts with nested %s; kept %s".formatted( + ancestor.shortName, undoDesired, child.fullName, ancestor.shortName + ) ); } else { conflicts.add( - "ancestor-nested: nested " - + child.fullName - + "→" - + undoDesired - + " conflicts with ancestor " - + ancestor.fullName - + "; kept " - + child.shortName + "ancestor-nested: nested %s→%s conflicts with ancestor %s; kept %s".formatted( + child.fullName, undoDesired, ancestor.fullName, child.shortName + ) ); } names.put(undo, undo.shortName); @@ -451,12 +439,9 @@ private void blockObjectFactoryClashes( } // Capture squeezed name and arrows before reverts change short names on the model. var squeezed = group.getFirst().getSqueezedName(); - conflicts.add( - "object-factory: squeezed '" - + squeezed - + "'; " - + joinArrows(toRevert, names) - ); + conflicts.add("object-factory: squeezed '%s'; %s".formatted( + squeezed, joinArrows(toRevert, names) + )); for (var candidate : toRevert) { names.put(candidate, candidate.shortName); setFieldValue(CCLASSINFO_SHORTNAME_FIELD, candidate.bean, candidate.shortName); From b58a6841d0b4a38d792a8cbcc72a8d67fa399c8a Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:14:51 +0800 Subject: [PATCH 04/10] fix(element-wrapper): log removed wrapper names at DEBUG Keep INFO as the removed count only; emit the class list at DEBUG so default Maven builds stay quiet while details remain available when debugging. --- .../rawvoid/jaxb/plugin/ElementWrapperPlugin.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java index 7045170..bfc2105 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java @@ -77,10 +77,10 @@ * {@code @XmlElementWrapper(nillable = true)} and drops the synthetic local element info. *

*

- * Logging: each flattened field is {@code DEBUG}; the flatten count and removed wrapper list - * are {@code INFO}. Skipped wrapper removals and stale annotation owners (plugin-order - * issues) are one multi-line {@code WARN} each. Per-field flatten failures stay single-line - * {@code WARN}. + * Logging: each flattened field and the removed-wrapper name list are {@code DEBUG}; the + * flatten count and removed-wrapper count are {@code INFO}. Skipped wrapper removals and + * stale annotation owners (plugin-order issues) are one multi-line {@code WARN} each. + * Per-field flatten failures stay single-line {@code WARN}. *

* * @author Rawvoid @@ -435,9 +435,9 @@ private void removeUnusedWrappers(Model model, Set wrappers) { } if (!removed.isEmpty()) { - log.info( - "Removed {} wrapper class(es):\n {}", - removed.size(), + log.info("Removed {} wrapper class(es)", removed.size()); + log.debug( + "Removed wrapper class(es):\n {}", String.join("\n ", removed) ); } From 2b9a05209adbf4c258766e5a70084e6838bc8c32 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:18:55 +0800 Subject: [PATCH 05/10] fix(dedupe-class): align logging with rename/element-wrapper style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log each accepted merge and enum merge at DEBUG with victim → host form; keep INFO for merge and element-class-clear counts only. Aggregate ObjectFactory collisions into one multi-line WARN. --- .../jaxb/plugin/DedupeClassPlugin.java | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java index fca2b9c..d6394d7 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java @@ -93,6 +93,12 @@ * Shell-to-shell exact merges remain allowed. Default is auto: on when * {@code -Xelement-wrapper} is also active; force with {@code true}/{@code false}. *

+ *

+ * Logging: each accepted merge is {@code DEBUG}; merge and element-class-clear counts are + * {@code INFO}. Routine skips stay {@code DEBUG}; element-class root mismatches and + * ObjectFactory collisions after dedupe are {@code WARN} (ObjectFactory: one multi-line + * summary). + *

* * @author Rawvoid */ @@ -869,7 +875,11 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { warnObjectFactoryCollisions(model); } if (merged > 0) { - log.info("Deduped {} bean merge(s){}", merged, session.dry ? " (dry-run)" : ""); + if (session.dry) { + log.info("Deduped {} bean merge(s) (dry-run)", merged); + } else { + log.info("Deduped {} bean merge(s)", merged); + } } } @@ -1019,7 +1029,7 @@ private boolean tryMerge( if (!isPackageLevel(host) && host.parent() != victim.parent() && !allowCrossNestedParent) { log.debug( - "Skip dedupe {}: cross-hierarchy nested '{}' vs '{}'", + "Skip dedupe {}: cross-hierarchy nested {} vs {}", reason, victim.fullName(), host.fullName() ); return false; @@ -1028,11 +1038,11 @@ private boolean tryMerge( if (victim.isElement() && host.isElement() && !Objects.equals(victim.getElementName(), host.getElementName())) { log.warn( - "Skip dedupe {}: both '{}' and '{}' are element-classes with different roots ({} vs {})", + "Skip dedupe {}: both element-classes with different roots: {} ({}) vs {} ({})", reason, victim.fullName(), - host.fullName(), victim.getElementName(), + host.fullName(), host.getElementName() ); return false; @@ -1044,7 +1054,7 @@ private boolean tryMerge( && ModelUtils.isPureCollectionShell(victim) && !ModelUtils.isPureCollectionShell(host)) { log.debug( - "Skip dedupe {}: pure collection shell '{}' must not merge into non-shell '{}'", + "Skip dedupe {}: pure collection shell {} must not merge into non-shell {}", reason, victim.fullName(), host.fullName() ); return false; @@ -1057,8 +1067,8 @@ private boolean tryMerge( return false; } - log.info( - "Dedupe {}: '{}' -> '{}' (nameKey={})", + log.debug( + "Dedupe {}: {} → {} (nameKey={})", reason, victim.fullName(), host.fullName(), nameKey(victim.shortName) ); @@ -1102,7 +1112,11 @@ private boolean prepareNestedMerges(Model model, Session session, CClassInfo vic var childNorm = child.shortName.toLowerCase(Locale.ROOT); for (var hc : directNestedBeans(model, host)) { if (hc.shortName.toLowerCase(Locale.ROOT).equals(childNorm)) { - log.debug("Skip dedupe: nested name clash {} under {}", child.shortName, host.fullName()); + log.debug( + "Skip dedupe: nested name clash {} under {}", + child.shortName, + host.fullName() + ); return false; } } @@ -1165,9 +1179,10 @@ private boolean prepareNestedEnums(Model model, Session session, CClassInfo vict } } if (match != null) { - log.info( - "Dedupe exact-enum: '{}' -> '{}'", - fullEnumName(victimEnum), fullEnumName(match) + log.debug( + "Dedupe exact-enum: {} → {}", + fullEnumName(victimEnum), + fullEnumName(match) ); session.countedMerges.add(IdentityPair.directed(victimEnum, match)); if (!session.dry) { @@ -1298,7 +1313,7 @@ private static void collapseRedundantElementClasses(Model model, Set mer if (mergedPackageNameKeys.isEmpty()) { return; } - var cleared = 0; + var cleared = new ArrayList(); for (var elementInfo : model.getAllElements()) { if (!elementInfo.hasClass()) { continue; @@ -1319,18 +1334,29 @@ private static void collapseRedundantElementClasses(Model model, Set mer continue; } setFieldValue(CELEMENTINFO_CLASSNAME_FIELD, elementInfo, null); - cleared++; + cleared.add(elementInfo.fullName()); } - if (cleared > 0) { - log.info("Cleared {} redundant element class name(s) after dedupe", cleared); + if (!cleared.isEmpty()) { + log.info("Cleared {} redundant element class name(s) after dedupe", cleared.size()); + log.debug( + "Cleared element class name(s):\n {}", + String.join("\n ", cleared) + ); } } private static void warnObjectFactoryCollisions(Model model) { + var collisions = new ArrayList(); for (var group : ModelUtils.objectFactorySqueezedCollisions(model)) { + var squeezed = group.getFirst().getSqueezedName(); + var names = group.stream().map(CClassInfo::fullName).toList(); + collisions.add("squeezed '%s': %s".formatted(squeezed, names)); + } + if (!collisions.isEmpty()) { log.warn( - "ObjectFactory squeezed-name collision after dedupe (package-local createXxx): {}", - group.stream().map(CClassInfo::fullName).toList() + "ObjectFactory name collision(s) after dedupe ({}):\n {}", + collisions.size(), + String.join("\n ", collisions) ); } } From 68d96243d49b258c3a93d73dd58e858994da7f94 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:24:49 +0800 Subject: [PATCH 06/10] fix(promote-nested-class): align logging with rename/element-wrapper style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log each successful one-level lift at DEBUG as type (from → to); keep INFO as the hop count only. Polish ObjectFactory skip wording for consistency. --- .../jaxb/plugin/PromoteNestedClassPlugin.java | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/PromoteNestedClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/PromoteNestedClassPlugin.java index 224acde..d64ce9b 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/PromoteNestedClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/PromoteNestedClassPlugin.java @@ -89,6 +89,11 @@ * is also why a lift can collide with another type whose squeezed name already * equals the shortened form — those lifts are undone. *

+ *

+ * Logging: each successful one-level lift is {@code DEBUG}; the hop count is + * {@code INFO}. ObjectFactory collision rollbacks stay {@code DEBUG}. Name-slot + * contention is silent (types simply stop). + *

* * @author Rawvoid */ @@ -129,6 +134,11 @@ private static String normalize(String name) { return name.toLowerCase(Locale.ROOT); } + private static String parentLabel(CClassInfoParent parent) { + var label = parent.fullName(); + return label == null || label.isEmpty() ? "(default package)" : label; + } + /** * Nesting is defined on the model; changing parents here is enough for * BeanGenerator to emit the right containers. {@link #run} is a no-op. @@ -145,7 +155,7 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { } if (hops > 0) { // Counts one-level moves, not distinct types (a deep type may hop several times). - log.info("Promoted {} nested type placement(s) (beans and enums)", hops); + log.info("Promoted {} nested type placement(s)", hops); } } @@ -213,17 +223,33 @@ private int promoteOneLevel(Model model) { for (var move : shortNameOk) { if (move.bean != null) { var previous = move.bean.parent(); + // fullName still reflects the old parent until re-parent succeeds. + var typeName = move.bean.fullName(); setFieldValue(CCLASSINFO_PARENT_FIELD, move.bean, move.target); if (ModelUtils.hasObjectFactorySqueezedCollision(model)) { setFieldValue(CCLASSINFO_PARENT_FIELD, move.bean, previous); log.debug( - "Skip promoting {} — ObjectFactory squeezed name collision", - move.bean.fullName() + "Skip promote {}: ObjectFactory name collision", + typeName ); continue; } + log.debug( + "Promoted {} ({} → {})", + typeName, + parentLabel(previous), + parentLabel(move.target) + ); } else { + var previous = move.enumInfo.parent; + var typeName = move.enumInfo.fullName(); setFieldValue(CENUMLEAFINFO_PARENT_FIELD, move.enumInfo, move.target); + log.debug( + "Promoted {} ({} → {})", + typeName, + parentLabel(previous), + parentLabel(move.target) + ); } moved++; } From f70780f1a528c217d6612d865e209066b956cc02 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:28:20 +0800 Subject: [PATCH 07/10] fix(remove-unused-class): align logging with rename/element-wrapper style Always log removed counts at INFO and name lists at DEBUG; drop the custom -verbose flag in favor of SLF4J levels. Update the test and wiki accordingly. --- .../jaxb/plugin/RemoveUnusedClassPlugin.java | 47 +++++++++++++------ .../plugin/RemoveUnusedClassPluginTest.java | 2 +- wiki/remove-unused-class.md | 5 +- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPlugin.java index 84d5daa..2d87325 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPlugin.java @@ -32,9 +32,14 @@ /** * XJC plugin that removes unreferenced JAXB classes and enums from the {@link Model} * during {@link #postProcessModel(Model, ErrorHandler)}. - * - *

Uses a graph reachability analysis (Mark & Sweep) starting from global XML root elements - * and user-configured white-list patterns to identify and prune unreachable classes and enums.

+ *

+ * Uses a graph reachability analysis (Mark & Sweep) starting from global XML root elements + * and user-configured white-list patterns to identify and prune unreachable classes and enums. + *

+ *

+ * Logging: the removed count is {@code INFO}; the class/enum name list is {@code DEBUG}. + * When nothing is removed, only a {@code DEBUG} line is emitted. + *

* * @author Rawvoid */ @@ -49,9 +54,6 @@ public class RemoveUnusedClassPlugin extends OptionPlugin { @Option(name = "preserve-polymorphism", defaultValue = "false", description = "Whether to treat subclasses of a reachable base class as reachable (default: false)") Boolean preservePolymorphism = false; - @Option(name = "verbose", defaultValue = "false", description = "Enable detailed logging of reachability and deleted classes (default: false)") - Boolean verbose = false; - @Override public boolean run(Outline outline, Options opt, ErrorHandler errorHandler) { return true; @@ -86,30 +88,45 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { .toList(); if (deadClasses.isEmpty() && deadEnums.isEmpty()) { - if (Boolean.TRUE.equals(verbose)) { - log.info("[Xremove-unused-class] No unreferenced classes or enums found."); - } + log.debug("No unreferenced classes or enums found"); return; } // 4. Remove dead classes + var removedClasses = new ArrayList(deadClasses.size()); for (var deadClass : deadClasses) { ModelUtils.removeClass(model, deadClass); - if (Boolean.TRUE.equals(verbose)) { - log.info("[Xremove-unused-class] Removed unreferenced class: {}", deadClass.fullName()); - } + removedClasses.add(deadClass.fullName()); } // 5. Remove dead enums + var removedEnums = new ArrayList(deadEnums.size()); for (var deadEnum : deadEnums) { ModelUtils.removeEnum(model, deadEnum); - if (Boolean.TRUE.equals(verbose)) { - log.info("[Xremove-unused-class] Removed unreferenced enum: {}", deadEnum.fullName()); - } + removedEnums.add(deadEnum.fullName()); } // 6. Clean up orphan CElementInfo objects pointing to dead classes/enums cleanOrphanElements(model, deadClasses, deadEnums); + + log.info( + "Removed {} unreferenced type(s) ({} class(es), {} enum(s))", + removedClasses.size() + removedEnums.size(), + removedClasses.size(), + removedEnums.size() + ); + if (!removedClasses.isEmpty()) { + log.debug( + "Removed unreferenced class(es):\n {}", + String.join("\n ", removedClasses) + ); + } + if (!removedEnums.isEmpty()) { + log.debug( + "Removed unreferenced enum(s):\n {}", + String.join("\n ", removedEnums) + ); + } } private void collectRoots(Model model, Set roots, Queue queue) { diff --git a/plugins/src/test/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPluginTest.java b/plugins/src/test/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPluginTest.java index d759fd7..eddadce 100644 --- a/plugins/src/test/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPluginTest.java +++ b/plugins/src/test/java/io/github/rawvoid/jaxb/plugin/RemoveUnusedClassPluginTest.java @@ -65,7 +65,7 @@ void baselineGeneratesAllClasses() throws Exception { @Test void removesUnusedClassesAndEnums() throws Exception { - var args = List.of(optionCmd, "-verbose"); + var args = List.of(optionCmd); var classes = testExecute(args, ".*", null); var names = classes.stream().map(Class::getName).toList(); diff --git a/wiki/remove-unused-class.md b/wiki/remove-unused-class.md index 922a726..424f996 100644 --- a/wiki/remove-unused-class.md +++ b/wiki/remove-unused-class.md @@ -18,7 +18,6 @@ Removes unreferenced JAXB classes and enums from the model during `postProcessMo | `-Xremove-unused-class` | flag | — | Enable the plugin | | `-keep-classes` | regex (repeatable) | — | Forcibly keep matching classes or enums as roots (`find` on full name or short name) | | `-preserve-polymorphism` | boolean | `false` | Treat subclasses of a reachable base class as reachable | -| `-verbose` | boolean | `false` | Detailed logging of reachability and deleted types | ## Behavior @@ -52,9 +51,11 @@ Only `CClassInfo`, `CEnumLeafInfo`, and `CElementInfo` targets are tracked as re ```text -Xremove-unused-class -Xremove-unused-class -keep-classes=com\.example\.KeepMe --Xremove-unused-class -preserve-polymorphism=true -verbose=true +-Xremove-unused-class -preserve-polymorphism=true ``` +Logging: removed count is `INFO`; the class/enum name list is `DEBUG` (enable the logger for details). + ## Limitations / notes - Without `-preserve-polymorphism`, unused subclasses of a reachable base are pruned even if the base is kept. From 1df365b97bb606e800912dac28de747b4035ce67 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 16:30:25 +0800 Subject: [PATCH 08/10] fix(flatten-multi-element-prop): align logging with rename/element-wrapper style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log each successful property split at DEBUG as Owner.prop → [fields]; keep INFO as the flatten count only. --- .../plugin/FlattenMultiElementPropPlugin.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java index fd0b754..77ef408 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java @@ -60,6 +60,10 @@ * Mutual exclusion: do not enable this plugin together with * {@link RenameMultiElementPropPlugin} — one renames while the other splits. *

+ *

+ * Logging: each successful split is {@code DEBUG}; the property count is {@code INFO}. + * Failed {@code addProperty} / empty replacement cases are single-line {@code WARN}. + *

* * @author Rawvoid */ @@ -112,7 +116,7 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { flattened += handleClass(bean); } if (flattened > 0) { - log.info("Flattened {} multi-element property(ies) into individual fields", flattened); + log.info("Flattened {} multi-element property(ies)", flattened); } } @@ -137,12 +141,19 @@ private int handleClass(CClassInfo bean) { continue; } + var originalName = prop.getName(false); // Replace the original property at position i with all the new ones. if (replaceProperty(bean, i, prop, replacements)) { // After replacement, i now points at the first new property. // Advance past all inserted properties so the loop continues correctly. i += replacements.size() - 1; count++; + log.debug( + "Flattened {}.{} → {}", + bean.fullName(), + originalName, + replacements.stream().map(r -> r.getName(false)).toList() + ); } } return count; @@ -375,14 +386,20 @@ private boolean replaceProperty( if (properties.size() == sizeBefore + added + 1) { added++; } else { - log.warn("Skip adding {}.{}: addProperty did not append", - owner.fullName(), replacement.getName(false)); + log.warn( + "Skip adding {}.{}: addProperty did not append", + owner.fullName(), + replacement.getName(false) + ); } } if (added == 0) { - log.warn("Skip flattening {}.{}: no replacements were added", - owner.fullName(), original.getName(false)); + log.warn( + "Skip flattening {}.{}: no replacements were added", + owner.fullName(), + original.getName(false) + ); return false; } From af9dcb9f33d7b474f976e0e27ac13690d5e8fdc4 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 17:01:50 +0800 Subject: [PATCH 09/10] fix(plugins): address logging review findings Capture element FQCNs before clearing className in dedupe; log renames and conflict arrows with full names; DEBUG only fields actually added by flatten; use labeled wrapper wording in element-wrapper DEBUG lines. --- .../jaxb/plugin/DedupeClassPlugin.java | 4 ++- .../jaxb/plugin/ElementWrapperPlugin.java | 2 +- .../plugin/FlattenMultiElementPropPlugin.java | 27 ++++++++++--------- .../jaxb/plugin/RenameClassPlugin.java | 15 ++++++----- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java index d6394d7..f0139b8 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/DedupeClassPlugin.java @@ -1333,8 +1333,10 @@ private static void collapseRedundantElementClasses(Model model, Set mer && !mergedPackageNameKeys.contains(pkg + '\0' + contentKey)) { continue; } - setFieldValue(CELEMENTINFO_CLASSNAME_FIELD, elementInfo, null); + // Capture before clearing className: fullName() falls back to type.fullName() + // (JAXBElement<…>) once className is null. cleared.add(elementInfo.fullName()); + setFieldValue(CELEMENTINFO_CLASSNAME_FIELD, elementInfo, null); } if (!cleared.isEmpty()) { log.info("Cleared {} redundant element class name(s) after dedupe", cleared.size()); diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java index bfc2105..ee4189b 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/ElementWrapperPlugin.java @@ -222,7 +222,7 @@ private void flattenOwner( } log.debug( - "Flattened {}.{} → {}", + "Flattened {}.{} (wrapper {})", owner.fullName(), outer.getName(false), resolved.wrapper().fullName() diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java index 77ef408..6d81b8d 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/FlattenMultiElementPropPlugin.java @@ -143,16 +143,17 @@ private int handleClass(CClassInfo bean) { var originalName = prop.getName(false); // Replace the original property at position i with all the new ones. - if (replaceProperty(bean, i, prop, replacements)) { + var addedNames = replaceProperty(bean, i, prop, replacements); + if (!addedNames.isEmpty()) { // After replacement, i now points at the first new property. // Advance past all inserted properties so the loop continues correctly. - i += replacements.size() - 1; + i += addedNames.size() - 1; count++; log.debug( "Flattened {}.{} → {}", bean.fullName(), originalName, - replacements.stream().map(r -> r.getName(false)).toList() + addedNames ); } } @@ -371,8 +372,10 @@ private static String resolveJavadoc(QName elementName, CElement element, CClass * new property. The appended properties are then moved from the tail back to * the original position. *

+ * + * @return short names of properties that were actually appended (empty if none) */ - private boolean replaceProperty( + private List replaceProperty( CClassInfo owner, int index, CPropertyInfo original, List replacements ) { @@ -380,11 +383,11 @@ private boolean replaceProperty( var sizeBefore = properties.size(); // 1. Append all replacements via addProperty (triggers setParent). - var added = 0; + var addedNames = new ArrayList(); for (var replacement : replacements) { owner.addProperty(replacement); - if (properties.size() == sizeBefore + added + 1) { - added++; + if (properties.size() == sizeBefore + addedNames.size() + 1) { + addedNames.add(replacement.getName(false)); } else { log.warn( "Skip adding {}.{}: addProperty did not append", @@ -394,26 +397,26 @@ private boolean replaceProperty( } } - if (added == 0) { + if (addedNames.isEmpty()) { log.warn( "Skip flattening {}.{}: no replacements were added", owner.fullName(), original.getName(false) ); - return false; + return List.of(); } // 2. Remove original at index. properties.remove(index); - // 3. Move the `added` properties from the tail to index. + // 3. Move the appended properties from the tail to index. // Since removeLast() retrieves the last added property first, inserting // them successively at `index` naturally restores their original order. - for (var j = 0; j < added; j++) { + for (var j = 0; j < addedNames.size(); j++) { properties.add(index, properties.removeLast()); } - return true; + return addedNames; } /** diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java index a9cedeb..281a34c 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java @@ -178,12 +178,14 @@ private static List collect(Model model) { return result; } - /** {@code Original→Desired} for a provisional rename (or unchanged name). */ + /** + * Label for conflict details: FQCN when unchanged, otherwise {@code fullName→desiredShort}. + */ private static String arrow(Candidate candidate, Map names) { var desired = names.get(candidate); return candidate.shortName.equals(desired) - ? candidate.shortName - : "%s→%s".formatted(candidate.shortName, desired); + ? candidate.fullName + : "%s→%s".formatted(candidate.fullName, desired); } private static String joinArrows(Collection group, Map names) { @@ -280,9 +282,10 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { for (var candidate : candidates) { var next = names.get(candidate); if (!next.equals(candidate.shortName)) { + // fullName is the pre-rename FQCN (shortName is not rewritten on Candidate). + log.debug("Renamed {} → {}", candidate.fullName, next); candidate.apply.accept(next); renamed++; - log.debug("Renamed {} → {}", candidate.shortName, next); } } if (renamed > 0) { @@ -378,13 +381,13 @@ private boolean revertAncestorNameClash( if (undo == ancestor) { conflicts.add( "ancestor-nested: %s→%s conflicts with nested %s; kept %s".formatted( - ancestor.shortName, undoDesired, child.fullName, ancestor.shortName + ancestor.fullName, undoDesired, child.fullName, ancestor.fullName ) ); } else { conflicts.add( "ancestor-nested: nested %s→%s conflicts with ancestor %s; kept %s".formatted( - child.fullName, undoDesired, ancestor.fullName, child.shortName + child.fullName, undoDesired, ancestor.fullName, child.fullName ) ); } From 78571fddbdcbb25b734eef80d9a83e4b05285357 Mon Sep 17 00:00:00 2001 From: Rawvoid Date: Sun, 2 Aug 2026 17:50:23 +0800 Subject: [PATCH 10/10] fix(rename-multi-element-prop): align logging style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Log each property rename at DEBUG as Owner.prop → newName; keep INFO as the rename count only. Document logging in the class Javadoc. --- .../jaxb/plugin/RenameMultiElementPropPlugin.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameMultiElementPropPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameMultiElementPropPlugin.java index d27209b..a6a911c 100644 --- a/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameMultiElementPropPlugin.java +++ b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameMultiElementPropPlugin.java @@ -50,6 +50,10 @@ *

* Field renames do not affect {@code ObjectFactory} (which keys on class squeezed names). *

+ *

+ * Logging: each renamed property is {@code DEBUG}; the rename count is {@code INFO}. + * Invalid {@code -name} values are a single {@code WARN} before falling back to {@code items}. + *

* * @author Rawvoid */ @@ -102,7 +106,7 @@ public void postProcessModel(Model model, ErrorHandler errorHandler) { renamed += handleClass(bean); } if (renamed > 0) { - log.info("Renamed {} multi-element property name(s) using base '{}'", renamed, name); + log.info("Renamed {} multi-element property name(s)", renamed); } } @@ -132,7 +136,8 @@ private int handleClass(CClassInfo bean) { var count = 0; for (var prop : targets) { - occupied.remove(normalize(prop.getName(false))); + var originalName = prop.getName(false); + occupied.remove(normalize(originalName)); var seed = allocateName(occupied); var privateName = NAMES.toVariableName(seed); var publicName = NAMES.toPropertyName(seed); @@ -140,6 +145,12 @@ private int handleClass(CClassInfo bean) { prop.setName(false, privateName); prop.setName(true, publicName); count++; + log.debug( + "Renamed {}.{} → {}", + bean.fullName(), + originalName, + privateName + ); } return count; }