Skip to content

Commit 52a80c5

Browse files
AddToTagVisitor: opt-in flag to skip insert when a semantically-equal child exists
Raw XML allows duplicate sibling tags, so "no semantic duplicates" is a schema-specific concern (Maven POM, csproj, etc.) rather than something that should be baked into the schema-agnostic rewrite-xml primitive. This change makes the SemanticallyEqual guard opt-in via a new `skipIfEquivalentExists` boolean (default `false`). Callers wanting idempotent inserts pass `true`; existing call sites keep their current duplicate-permitting behavior unchanged. Two new constructor overloads are added so callers don't have to pass `null` for `tagComparator` just to opt in: - `(Xml.Tag, Xml.Tag, boolean)` - `(Xml.Tag, Xml.Tag, @nullable Comparator<Content>, boolean)`
1 parent ea81d80 commit 52a80c5

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

rewrite-xml/src/main/java/org/openrewrite/xml/AddToTagVisitor.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,37 @@ public class AddToTagVisitor<P> extends XmlVisitor<P> {
3333
@Nullable
3434
private final Comparator<Content> tagComparator;
3535

36+
private final boolean skipIfEquivalentExists;
37+
3638
public AddToTagVisitor(Xml.Tag scope, Xml.Tag tagToAdd) {
37-
this(scope, tagToAdd, null);
39+
this(scope, tagToAdd, null, false);
40+
}
41+
42+
public AddToTagVisitor(Xml.Tag scope, Xml.Tag tagToAdd, boolean skipIfEquivalentExists) {
43+
this(scope, tagToAdd, null, skipIfEquivalentExists);
3844
}
3945

4046
public AddToTagVisitor(Xml.Tag scope, Xml.Tag tagToAdd, @Nullable Comparator<Content> tagComparator) {
47+
this(scope, tagToAdd, tagComparator, false);
48+
}
49+
50+
public AddToTagVisitor(Xml.Tag scope, Xml.Tag tagToAdd, @Nullable Comparator<Content> tagComparator, boolean skipIfEquivalentExists) {
4151
this.scope = scope;
4252
this.tagToAdd = tagToAdd;
4353
this.tagComparator = tagComparator;
54+
this.skipIfEquivalentExists = skipIfEquivalentExists;
4455
}
4556

4657
@Override
4758
public Xml visitTag(Xml.Tag t, P p) {
4859
if (scope.isScope(t)) {
60+
if (skipIfEquivalentExists && t.getContent() != null) {
61+
for (Content existing : t.getContent()) {
62+
if (existing instanceof Xml.Tag && SemanticallyEqual.areEqual(existing, tagToAdd)) {
63+
return super.visitTag(t, p);
64+
}
65+
}
66+
}
4967
assert getCursor().getParent() != null;
5068
if (t.getClosing() == null) {
5169
t = t.withClosing(autoFormat(new Xml.Tag.Closing(Tree.randomId(), "\n",

rewrite-xml/src/test/java/org/openrewrite/xml/AddToTagTest.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,73 @@ public Xml visitDocument(Xml.Document x, ExecutionContext ctx) {
143143
);
144144
}
145145

146+
@Test
147+
void doesNotAddSemanticallyEqualDuplicate() {
148+
rewriteRun(
149+
spec -> spec.recipe(toRecipe(() -> new XmlVisitor<>() {
150+
@Override
151+
public Xml visitDocument(Xml.Document x, ExecutionContext ctx) {
152+
doAfterVisit(new AddToTagVisitor<>(x.getRoot(), Xml.Tag.build("<bean id=\"myBean\"/>"), true));
153+
return super.visitDocument(x, ctx);
154+
}
155+
})),
156+
xml(
157+
"""
158+
<beans>
159+
<bean id="myBean"/>
160+
</beans>
161+
"""
162+
)
163+
);
164+
}
165+
166+
@Test
167+
void doesNotAddDuplicateIgnoringAttributeOrder() {
168+
rewriteRun(
169+
spec -> spec.recipe(toRecipe(() -> new XmlVisitor<>() {
170+
@Override
171+
public Xml visitDocument(Xml.Document x, ExecutionContext ctx) {
172+
doAfterVisit(new AddToTagVisitor<>(x.getRoot(),
173+
Xml.Tag.build("<bean class=\"C\" id=\"myBean\"/>"), true));
174+
return super.visitDocument(x, ctx);
175+
}
176+
})),
177+
xml(
178+
"""
179+
<beans>
180+
<bean id="myBean" class="C"/>
181+
</beans>
182+
"""
183+
)
184+
);
185+
}
186+
187+
@Test
188+
void addsWhenChildrenShareNameButDifferentAttributes() {
189+
rewriteRun(
190+
spec -> spec.recipe(toRecipe(() -> new XmlVisitor<>() {
191+
@Override
192+
public Xml visitDocument(Xml.Document x, ExecutionContext ctx) {
193+
doAfterVisit(new AddToTagVisitor<>(x.getRoot(), Xml.Tag.build("<bean id=\"myBean2\"/>"), true));
194+
return super.visitDocument(x, ctx);
195+
}
196+
})),
197+
xml(
198+
"""
199+
<beans>
200+
<bean id="myBean"/>
201+
</beans>
202+
""",
203+
"""
204+
<beans>
205+
<bean id="myBean"/>
206+
<bean id="myBean2"/>
207+
</beans>
208+
"""
209+
)
210+
);
211+
}
212+
146213
@Issue("https://github.com/openrewrite/rewrite/issues/1392")
147214
@Test
148215
void preserveNonTagContent() {

0 commit comments

Comments
 (0)