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..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 @@ -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; @@ -1318,19 +1333,32 @@ private static void collapseRedundantElementClasses(Model model, Set mer && !mergedPackageNameKeys.contains(pkg + '\0' + contentKey)) { continue; } + // 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); - cleared++; } - 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) ); } } 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..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 @@ -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 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 */ @@ -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 {}.{} (wrapper {})", + 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)", removed.size()); + log.debug( + "Removed wrapper class(es):\n {}", + 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; } 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..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 @@ -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,20 @@ 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)) { + 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, + addedNames + ); } } return count; @@ -360,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 ) { @@ -369,34 +383,40 @@ 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", - 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)); - return false; + if (addedNames.isEmpty()) { + log.warn( + "Skip flattening {}.{}: no replacements were added", + owner.fullName(), + original.getName(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/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++; } 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/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java b/plugins/src/main/java/io/github/rawvoid/jaxb/plugin/RenameClassPlugin.java index 4c94f0b..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 @@ -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: *

*
    *
  • Same simple name under one parent (beans / enums / element classes)
  • @@ -85,6 +86,10 @@ * {@code getSqueezedName()} after writing provisional short names onto the model *
*

+ * 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,35 @@ private static List collect(Model model) { return result; } + /** + * 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.fullName + : "%s→%s".formatted(candidate.fullName, 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,14 +272,18 @@ 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) { 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++; } @@ -270,7 +305,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 +321,13 @@ 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 '%s': %s".formatted( + 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 +340,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 +357,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 +377,21 @@ private boolean revertAncestorNameClash( if (undo == null) { return false; } + var undoDesired = names.get(undo); + if (undo == ancestor) { + conflicts.add( + "ancestor-nested: %s→%s conflicts with nested %s; kept %s".formatted( + 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.fullName + ) + ); + } 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 +414,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 +440,15 @@ 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 '%s'; %s".formatted( + 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 +471,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 +487,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 +560,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, 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; } 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.