Skip to content

Commit 8727772

Browse files
AddToTagVisitor: skip insert when a semantically-equal child exists
Make `AddToTagVisitor` idempotent: if `scope` already contains a `SemanticallyEqual` child, return without mutating the tree. This preserves reference identity on no-op runs so that "did this recipe change anything" checks (and dependent dirty-tracking) stay accurate. Comments and attribute order are ignored by the equality check, so re-running a recipe over input it has already processed no longer produces a duplicate child.
1 parent 4864a79 commit 8727772

2 files changed

Lines changed: 74 additions & 0 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ public AddToTagVisitor(Xml.Tag scope, Xml.Tag tagToAdd, @Nullable Comparator<Con
4646
@Override
4747
public Xml visitTag(Xml.Tag t, P p) {
4848
if (scope.isScope(t)) {
49+
if (t.getContent() != null) {
50+
for (Content existing : t.getContent()) {
51+
if (existing instanceof Xml.Tag && SemanticallyEqual.areEqual(existing, tagToAdd)) {
52+
return super.visitTag(t, p);
53+
}
54+
}
55+
}
4956
assert getCursor().getParent() != null;
5057
if (t.getClosing() == null) {
5158
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\"/>")));
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\"/>")));
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\"/>")));
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)