Skip to content

Commit 10aeced

Browse files
timtebeekclaude
andauthored
Fix XPathMatcher to support text() predicate in XPath expressions (#6315)
The CONDITION_CONJUNCTION_PATTERN regex was missing support for text() function, causing RemoveXmlTag to ignore text predicates and remove all matching elements instead of only those with specific text content. Fixes #6314 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <[email protected]>
1 parent 3573030 commit 10aeced

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-3
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public class XPathMatcher {
4444
// Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` or foo[@bar='baz']
4545
private static final Pattern ELEMENT_WITH_CONDITION_PATTERN = Pattern.compile("(@)?([-:\\w]+|\\*)(\\[.+])");
4646
private static final Pattern CONDITION_PATTERN = Pattern.compile("(\\[.*?])+?");
47-
private static final Pattern CONDITION_CONJUNCTION_PATTERN = Pattern.compile("(((local-name|namespace-uri)\\(\\)|(@)?([-\\w:]+|\\*))\\h*=\\h*[\"'](.*?)[\"'](\\h?(or|and)\\h?)?)+?");
47+
private static final Pattern CONDITION_CONJUNCTION_PATTERN = Pattern.compile("(((local-name|namespace-uri|text)\\(\\)|(@)?([-\\w:]+|\\*))\\h*=\\h*[\"'](.*?)[\"'](\\h?(or|and)\\h?)?)+?");
4848

4949
private final String expression;
5050
private final boolean startsWithSlash;
@@ -296,8 +296,10 @@ private boolean matchesWithoutDoubleSlashesAt(Cursor cursor, int doubleSlashInde
296296
break;
297297
}
298298
}
299-
} else if (isFunctionCondition) { // [local-name()='name'] pattern
300-
if (isAttributeElement) {
299+
} else if (isFunctionCondition) { // [local-name()='name'] or [text()='value'] pattern
300+
if ("text()".equals(selector)) {
301+
matchCurrentCondition = tag.getValue().map(v -> v.equals(value)).orElse(false);
302+
} else if (isAttributeElement) {
301303
for (Xml.Attribute a : tag.getAttributes()) {
302304
if (matchesElementAndFunction(new Cursor(cursor, a), element, selector, value)) {
303305
matchCurrentCondition = true;

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import org.junit.jupiter.api.Test;
1919
import org.openrewrite.DocumentExample;
20+
import org.openrewrite.Issue;
2021
import org.openrewrite.test.RecipeSpec;
2122
import org.openrewrite.test.RewriteTest;
2223

@@ -102,6 +103,52 @@ void fileMatcherEmpty() {
102103
);
103104
}
104105

106+
@Test
107+
@Issue("https://github.com/openrewrite/rewrite/issues/6314")
108+
void removeOnlyElementMatchingTextPredicate() {
109+
rewriteRun(
110+
spec -> spec.recipe(new RemoveXmlTag("/test/foo[text()='bar']", null)),
111+
xml(
112+
"""
113+
<test>
114+
<foo>bar</foo>
115+
<foo>notBar</foo>
116+
<foo/>
117+
</test>
118+
""",
119+
"""
120+
<test>
121+
<foo>notBar</foo>
122+
<foo/>
123+
</test>
124+
"""
125+
)
126+
);
127+
}
128+
129+
@Test
130+
@Issue("https://github.com/openrewrite/rewrite/issues/6314")
131+
void removeOnlyElementMatchingLocalNameAndTextPredicate() {
132+
rewriteRun(
133+
spec -> spec.recipe(new RemoveXmlTag("/test/*[local-name()='foo' and text()='bar']", null)),
134+
xml(
135+
"""
136+
<test>
137+
<foo>bar</foo>
138+
<foo>notBar</foo>
139+
<foo/>
140+
</test>
141+
""",
142+
"""
143+
<test>
144+
<foo>notBar</foo>
145+
<foo/>
146+
</test>
147+
"""
148+
)
149+
);
150+
}
151+
105152
@Test
106153
void removeEmptyParentTag() {
107154
rewriteRun(

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,43 @@ void matchMultipleConditions() {
412412
assertThat(match("//*[@ns3:attr='test2'][local-name()='element4'][namespace-uri()='http://www.example.com/namespaceX']", namespacedXml)).isFalse();
413413
}
414414

415+
@Test
416+
@Issue("https://github.com/openrewrite/rewrite/issues/6314")
417+
void matchTextFunctionCondition() {
418+
SourceFile xml = new XmlParser().parse(
419+
"""
420+
<?xml version="1.0" encoding="UTF-8"?>
421+
<test>
422+
<foo>bar</foo>
423+
<foo>notBar</foo>
424+
<foo/>
425+
</test>
426+
"""
427+
).toList().getFirst();
428+
429+
// text() predicate should only match element with specific text content
430+
assertThat(match("/test/foo[text()='bar']", xml)).isTrue();
431+
assertThat(match("/test/foo[text()='notBar']", xml)).isTrue();
432+
assertThat(match("/test/foo[text()='nonexistent']", xml)).isFalse();
433+
assertThat(match("//foo[text()='bar']", xml)).isTrue();
434+
assertThat(match("//foo[text()='notBar']", xml)).isTrue();
435+
assertThat(match("//foo[text()='nonexistent']", xml)).isFalse();
436+
437+
// wildcard element with text() predicate
438+
assertThat(match("/test/*[text()='bar']", xml)).isTrue();
439+
assertThat(match("//*[text()='bar']", xml)).isTrue();
440+
assertThat(match("//*[text()='notBar']", xml)).isTrue();
441+
442+
// combining text() with local-name()
443+
assertThat(match("//*[local-name()='foo' and text()='bar']", xml)).isTrue();
444+
assertThat(match("//*[local-name()='foo' and text()='notBar']", xml)).isTrue();
445+
assertThat(match("//*[local-name()='foo' and text()='nonexistent']", xml)).isFalse();
446+
447+
// combining text() with or
448+
assertThat(match("/test/foo[text()='bar' or text()='notBar']", xml)).isTrue();
449+
assertThat(match("/test/foo[text()='nonexistent' or text()='bar']", xml)).isTrue();
450+
}
451+
415452
@Test
416453
void matchConditionsWithConjunctions() {
417454
// T&T, T&F, F&T, F&F

0 commit comments

Comments
 (0)