diff --git a/rewrite-xml/src/main/antlr/XPathLexer.g4 b/rewrite-xml/src/main/antlr/XPathLexer.g4
new file mode 100644
index 0000000000..dbc9b3800c
--- /dev/null
+++ b/rewrite-xml/src/main/antlr/XPathLexer.g4
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * XPath lexer for a limited subset of XPath expressions.
+ * Supports absolute and relative paths, wildcards, predicates,
+ * attribute access, and common XPath functions.
+ */
+lexer grammar XPathLexer;
+
+// Whitespace
+WS : [ \t\r\n]+ -> skip ;
+
+// Path separators
+SLASH : '/' ;
+DOUBLE_SLASH : '//' ;
+AXIS_SEP : '::' ;
+
+// Brackets
+LBRACKET : '[' ;
+RBRACKET : ']' ;
+LPAREN : '(' ;
+RPAREN : ')' ;
+
+// Operators
+AT : '@' ;
+DOTDOT : '..' ; // Must come before DOT for proper lexing
+DOT : '.' ;
+COMMA : ',' ;
+EQUALS : '=' ;
+NOT_EQUALS : '!=' ;
+LTE : '<=' ; // Must come before LT for proper lexing
+GTE : '>=' ; // Must come before GT for proper lexing
+LT : '<' ;
+GT : '>' ;
+WILDCARD : '*' ;
+
+// Numbers
+NUMBER : [0-9]+ ('.' [0-9]+)? ;
+
+// Logical operators (for predicate conditions)
+AND : 'and' ;
+OR : 'or' ;
+
+// XPath functions
+LOCAL_NAME : 'local-name' ;
+NAMESPACE_URI : 'namespace-uri' ;
+
+// String literals
+STRING_LITERAL
+ : '\'' (~['])* '\''
+ | '"' (~["])* '"'
+ ;
+
+// NCName (Non-Colonized Name) - XML name without colons
+// QName (Qualified Name) - NCName with optional prefix
+// QNAME must come before NCNAME to match longer token first
+QNAME
+ : NCNAME_CHARS ':' NCNAME_CHARS
+ ;
+
+NCNAME
+ : NCNAME_CHARS
+ ;
+
+fragment NCNAME_CHARS
+ : NAME_START_CHAR NAME_CHAR*
+ ;
+
+fragment NAME_START_CHAR
+ : [a-zA-Z_]
+ | '\u00C0'..'\u00D6'
+ | '\u00D8'..'\u00F6'
+ | '\u00F8'..'\u02FF'
+ | '\u0370'..'\u037D'
+ | '\u037F'..'\u1FFF'
+ | '\u200C'..'\u200D'
+ | '\u2070'..'\u218F'
+ | '\u2C00'..'\u2FEF'
+ | '\u3001'..'\uD7FF'
+ | '\uF900'..'\uFDCF'
+ | '\uFDF0'..'\uFFFD'
+ ;
+
+fragment NAME_CHAR
+ : NAME_START_CHAR
+ | '-'
+ | '.'
+ | [0-9]
+ | '\u00B7'
+ | '\u0300'..'\u036F'
+ | '\u203F'..'\u2040'
+ ;
diff --git a/rewrite-xml/src/main/antlr/XPathLexer.tokens b/rewrite-xml/src/main/antlr/XPathLexer.tokens
new file mode 100644
index 0000000000..b17440a9a7
--- /dev/null
+++ b/rewrite-xml/src/main/antlr/XPathLexer.tokens
@@ -0,0 +1,33 @@
+WS=1
+SLASH=2
+DOUBLE_SLASH=3
+LBRACKET=4
+RBRACKET=5
+LPAREN=6
+RPAREN=7
+AT=8
+DOT=9
+COMMA=10
+EQUALS=11
+WILDCARD=12
+AND=13
+OR=14
+LOCAL_NAME=15
+NAMESPACE_URI=16
+STRING_LITERAL=17
+QNAME=18
+'/'=2
+'//'=3
+'['=4
+']'=5
+'('=6
+')'=7
+'@'=8
+'.'=9
+','=10
+'='=11
+'*'=12
+'and'=13
+'or'=14
+'local-name'=15
+'namespace-uri'=16
diff --git a/rewrite-xml/src/main/antlr/XPathParser.g4 b/rewrite-xml/src/main/antlr/XPathParser.g4
new file mode 100644
index 0000000000..3563dd797e
--- /dev/null
+++ b/rewrite-xml/src/main/antlr/XPathParser.g4
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * XPath parser for a limited subset of XPath expressions.
+ *
+ * Supports:
+ * - Absolute paths: /root/child
+ * - Relative paths: child/grandchild
+ * - Descendant-or-self: //element
+ * - Wildcards: /root/*
+ * - Attribute access: /root/@attr, /root/element/@*
+ * - Node type tests: /root/element/text(), /root/comment(), etc.
+ * - Predicates with conditions: /root/element[@attr='value']
+ * - Child element predicates: /root/element[child='value']
+ * - Positional predicates: /root/element[1], /root/element[last()]
+ * - Parenthesized expressions with predicates: (/root/element)[1], (/root/a)[last()]
+ * - XPath functions: local-name(), namespace-uri(), text(), contains(), position(), last(), etc.
+ * - Logical operators in predicates: and, or
+ * - Multiple predicates: /root/element[@attr='value'][local-name()='element']
+ * - Top-level function expressions: contains(/root/element, 'value')
+ * - Boolean expressions: not(contains(...)), string-length(...) > 2
+ * - Abbreviated syntax: . (self), .. (parent)
+ * - Parent axis: parent::node(), parent::element
+ */
+parser grammar XPathParser;
+
+options { tokenVocab=XPathLexer; }
+
+// Entry point for XPath expression
+xpathExpression
+ : booleanExpr
+ | filterExpr
+ | absoluteLocationPath
+ | relativeLocationPath
+ ;
+
+// Filter expression - parenthesized path with predicates and optional trailing path: (/root/a)[1]/child
+filterExpr
+ : LPAREN (absoluteLocationPath | relativeLocationPath) RPAREN predicate+ (pathSeparator relativeLocationPath)?
+ ;
+
+// Boolean expression (function calls with optional comparison)
+booleanExpr
+ : functionCall comparisonOp comparand
+ | functionCall
+ ;
+
+// Comparison operators
+comparisonOp
+ : EQUALS
+ | NOT_EQUALS
+ | LT
+ | GT
+ | LTE
+ | GTE
+ ;
+
+// Value to compare against
+comparand
+ : stringLiteral
+ | NUMBER
+ ;
+
+// Absolute path starting with / or //
+absoluteLocationPath
+ : SLASH relativeLocationPath?
+ | DOUBLE_SLASH relativeLocationPath
+ ;
+
+// Relative path (series of steps)
+relativeLocationPath
+ : step (pathSeparator step)*
+ ;
+
+// Path separator between steps
+pathSeparator
+ : SLASH
+ | DOUBLE_SLASH
+ ;
+
+// A single step in the path
+step
+ : axisStep predicate*
+ | nodeTest predicate*
+ | attributeStep predicate*
+ | nodeTypeTest
+ | abbreviatedStep
+ ;
+
+// Axis step - explicit axis like parent::node()
+axisStep
+ : axisName AXIS_SEP nodeTest
+ ;
+
+// Supported axis names (NCName - no namespace prefix)
+axisName
+ : NCNAME // parent, ancestor, self, child, etc. - validated at runtime
+ ;
+
+// Abbreviated step - . or ..
+abbreviatedStep
+ : DOTDOT // parent::node()
+ | DOT // self::node()
+ ;
+
+// Node type test - text(), comment(), node(), processing-instruction()
+// Validation of which functions are valid node type tests happens at runtime
+nodeTypeTest
+ : NCNAME LPAREN RPAREN
+ ;
+
+// Attribute step (@attr, @ns:attr, or @*)
+attributeStep
+ : AT (QNAME | NCNAME | WILDCARD)
+ ;
+
+// Node test (element name, ns:element, or wildcard)
+nodeTest
+ : QNAME
+ | NCNAME
+ | WILDCARD
+ ;
+
+// Predicate in square brackets
+predicate
+ : LBRACKET predicateExpr RBRACKET
+ ;
+
+// Predicate expression (supports and/or)
+predicateExpr
+ : orExpr
+ ;
+
+// OR expression (lowest precedence)
+orExpr
+ : andExpr (OR andExpr)*
+ ;
+
+// AND expression (higher precedence than OR)
+andExpr
+ : primaryExpr (AND primaryExpr)*
+ ;
+
+// Primary expression in a predicate
+primaryExpr
+ : predicateValue comparisonOp comparand // any value expression with comparison
+ | predicateValue // standalone value (last(), position(), number, boolean)
+ ;
+
+// A value-producing expression in a predicate
+predicateValue
+ : functionCall // local-name(), last(), position(), contains(), etc.
+ | attributeStep // @attr, @*
+ | relativeLocationPath // bar/baz/text()
+ | childElementTest // child, *
+ | NUMBER // positional predicate [1], [2], etc.
+ ;
+
+// XPath function call - unified for both top-level and predicate use
+// Function names are NCNames (no namespace prefix in standard XPath 1.0)
+functionCall
+ : LOCAL_NAME LPAREN RPAREN
+ | NAMESPACE_URI LPAREN RPAREN
+ | NCNAME LPAREN functionArgs? RPAREN
+ ;
+
+// Function arguments (comma-separated)
+functionArgs
+ : functionArg (COMMA functionArg)*
+ ;
+
+// A single function argument
+// Note: functionCall must come before relativeLocationPath
+// because both can start with QNAME, but we need to check for '(' to distinguish them
+functionArg
+ : absoluteLocationPath
+ | functionCall
+ | relativeLocationPath
+ | stringLiteral
+ | NUMBER
+ ;
+
+// Child element test in predicate (element name, ns:element, or wildcard)
+childElementTest
+ : QNAME
+ | NCNAME
+ | WILDCARD
+ ;
+
+// String literal value
+stringLiteral
+ : STRING_LITERAL
+ ;
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathCompiler.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathCompiler.java
new file mode 100644
index 0000000000..a16f8d27e2
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathCompiler.java
@@ -0,0 +1,1256 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.xml;
+
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.xml.internal.grammar.XPathLexer;
+import org.openrewrite.xml.internal.grammar.XPathParser;
+
+import java.util.List;
+
+/**
+ * Parses and compiles XPath expressions into an optimized representation
+ * for efficient matching against XML cursor positions.
+ */
+final class XPathCompiler {
+
+ // Step characteristic flags (bitmask)
+ static final int FLAG_ABSOLUTE_PATH = 1;
+ static final int FLAG_DESCENDANT_OR_SELF = 1 << 1;
+ static final int FLAG_HAS_DESCENDANT = 1 << 2;
+ static final int FLAG_HAS_ABBREVIATED_STEP = 1 << 3;
+ static final int FLAG_HAS_AXIS_STEP = 1 << 4;
+ static final int FLAG_HAS_ATTRIBUTE_STEP = 1 << 5;
+ static final int FLAG_HAS_NODE_TYPE_TEST = 1 << 6;
+
+ // Expression type constants
+ static final int EXPR_PATH = 0; // Simple path (compiled)
+ static final int EXPR_BOOLEAN = 1; // Boolean expression (compiled)
+ static final int EXPR_FILTER = 2; // Filter expression (compiled)
+
+ // Empty steps array for non-path expressions
+ private static final CompiledStep[] EMPTY_STEPS = new CompiledStep[0];
+
+ private XPathCompiler() {
+ // Utility class
+ }
+
+ /**
+ * Compile an XPath expression into an optimized representation.
+ *
+ * @param expression the XPath expression to compile
+ * @return the compiled XPath representation
+ */
+ public static CompiledXPath compile(String expression) {
+ XPathLexer lexer = new XPathLexer(CharStreams.fromString(expression));
+ XPathParser parser = new XPathParser(new CommonTokenStream(lexer));
+ XPathParser.XpathExpressionContext ctx = parser.xpathExpression();
+ return compileSteps(ctx);
+ }
+
+ /**
+ * Pre-compile step information from parsed XPath context.
+ */
+ private static CompiledXPath compileSteps(XPathParser.XpathExpressionContext ctx) {
+ // Determine expression type first
+ if (ctx.booleanExpr() != null) {
+ CompiledExpr boolExpr = compileBooleanExpr(ctx.booleanExpr());
+ return new CompiledXPath(EMPTY_STEPS, 0, 0, EXPR_BOOLEAN, boolExpr, null);
+ } else if (ctx.filterExpr() != null) {
+ CompiledFilterExpr filterExpr = compileFilterExpr(ctx.filterExpr());
+ return new CompiledXPath(EMPTY_STEPS, 0, 0, EXPR_FILTER, null, filterExpr);
+ }
+
+ // Otherwise it's a path expression
+ XPathParser.RelativeLocationPathContext relPath = null;
+ int flags = 0;
+
+ if (ctx.absoluteLocationPath() != null) {
+ XPathParser.AbsoluteLocationPathContext absCtx = ctx.absoluteLocationPath();
+ if (absCtx.DOUBLE_SLASH() != null) {
+ flags |= FLAG_DESCENDANT_OR_SELF;
+ } else {
+ flags |= FLAG_ABSOLUTE_PATH;
+ }
+ relPath = absCtx.relativeLocationPath();
+ } else if (ctx.relativeLocationPath() != null) {
+ relPath = ctx.relativeLocationPath();
+ }
+
+ CompiledStep[] compiledSteps = EMPTY_STEPS;
+ int compiledElementSteps = 0;
+
+ if (relPath != null) {
+ // Extract steps
+ List stepCtxs = relPath.step();
+ List separators = relPath.pathSeparator();
+ compiledSteps = new CompiledStep[stepCtxs.size()];
+
+ for (int i = 0; i < stepCtxs.size(); i++) {
+ boolean isDescendant = false;
+ if (i > 0 && i - 1 < separators.size()) {
+ isDescendant = separators.get(i - 1).DOUBLE_SLASH() != null;
+ }
+ compiledSteps[i] = CompiledStep.fromStepContext(stepCtxs.get(i), isDescendant);
+ }
+
+ // Set nextIsBacktrack flag and compute step characteristics
+ for (int i = 0; i < compiledSteps.length; i++) {
+ CompiledStep s = compiledSteps[i];
+ if (s.isDescendant) flags |= FLAG_HAS_DESCENDANT;
+
+ switch (s.type) {
+ case ABBREVIATED_DOT:
+ case ABBREVIATED_DOTDOT:
+ flags |= FLAG_HAS_ABBREVIATED_STEP;
+ break;
+ case AXIS_STEP:
+ flags |= FLAG_HAS_AXIS_STEP;
+ break;
+ case ATTRIBUTE_STEP:
+ flags |= FLAG_HAS_ATTRIBUTE_STEP;
+ break;
+ case NODE_TYPE_TEST:
+ flags |= FLAG_HAS_NODE_TYPE_TEST;
+ break;
+ case NODE_TEST:
+ compiledElementSteps++;
+ break;
+ }
+
+ // Set nextIsBacktrack for lookahead optimization
+ if (i + 1 < compiledSteps.length && compiledSteps[i + 1].isBacktrack()) {
+ s.setNextIsBacktrack();
+ }
+ }
+ }
+
+ // Normalize mid-path parent steps (.. or parent::) to existence predicates
+ compiledSteps = normalizeParentSteps(compiledSteps);
+
+ // Recompute flags and element count after normalization
+ flags = recomputeFlags(compiledSteps, flags);
+ compiledElementSteps = countElementSteps(compiledSteps);
+
+ return new CompiledXPath(compiledSteps, flags, compiledElementSteps, EXPR_PATH, null, null);
+ }
+
+ /**
+ * Normalize mid-path parent steps (.. or parent::) into existence predicates.
+ *
+ * Transforms patterns like:
+ * - /a/b/c/../e → /a/b[c]/e (b must have child c)
+ * - /a/b/c/d/../../e → /a/b[c/d]/e (b must have path c/d)
+ *
+ * Leading parent steps (like ../foo) are left unchanged as they work naturally
+ * in bottom-up matching.
+ */
+ private static CompiledStep[] normalizeParentSteps(CompiledStep[] steps) {
+ if (steps.length == 0) {
+ return steps;
+ }
+
+ // Quick check: does this path have any parent steps that aren't at the start?
+ boolean hasNormalizableParent = false;
+ for (int i = 1; i < steps.length; i++) {
+ if (steps[i].isBacktrack() && !isLeadingParentStep(steps, i)) {
+ hasNormalizableParent = true;
+ break;
+ }
+ }
+ if (!hasNormalizableParent) {
+ return steps;
+ }
+
+ // Build normalized step list
+ java.util.ArrayList result = new java.util.ArrayList<>();
+ int i = 0;
+
+ while (i < steps.length) {
+ // Check if this is a parent step that should be normalized
+ if (steps[i].isBacktrack() && !isLeadingParentStep(steps, i)) {
+ // Count consecutive parent steps
+ int parentCount = 0;
+ int parentStart = i;
+ while (i < steps.length && steps[i].isBacktrack()) {
+ parentCount++;
+ i++;
+ }
+
+ // The parentCount element steps before the parent sequence become a predicate
+ // These steps are at positions: parentStart - parentCount to parentStart - 1
+ int predicateStartIdx = parentStart - parentCount;
+
+ if (predicateStartIdx < 0 || predicateStartIdx >= result.size()) {
+ // Not enough steps to consume - this is an edge case
+ // Just skip the parent steps (they'll be handled as leading parents)
+ continue;
+ }
+
+ // Extract the steps that become the predicate
+ int stepsToConvert = Math.min(parentCount, result.size() - predicateStartIdx);
+ CompiledStep[] predicateSteps = new CompiledStep[stepsToConvert];
+ for (int j = 0; j < stepsToConvert; j++) {
+ predicateSteps[j] = result.remove(predicateStartIdx);
+ }
+
+ // Create the predicate expression
+ CompiledExpr predicate = createPathPredicate(predicateSteps);
+
+ // Attach predicate to the step now at predicateStartIdx - 1 (the anchor)
+ if (predicateStartIdx > 0 && predicateStartIdx <= result.size()) {
+ int anchorIdx = predicateStartIdx - 1;
+ result.set(anchorIdx, result.get(anchorIdx).withAdditionalPredicate(predicate));
+ }
+
+ // Continue processing remaining steps after the parent sequence
+ } else {
+ result.add(steps[i]);
+ i++;
+ }
+ }
+
+ return result.toArray(new CompiledStep[0]);
+ }
+
+ /**
+ * Check if a parent step at the given index is a "leading" parent step.
+ * Leading parent steps are those at the start or only preceded by other parent steps.
+ */
+ private static boolean isLeadingParentStep(CompiledStep[] steps, int index) {
+ for (int i = 0; i < index; i++) {
+ if (!steps[i].isBacktrack()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Create a predicate expression from a sequence of steps.
+ * Single step becomes CHILD, multiple steps become PATH.
+ */
+ private static CompiledExpr createPathPredicate(CompiledStep[] steps) {
+ if (steps.length == 1) {
+ return CompiledExpr.child(steps[0].name);
+ }
+
+ // Multiple steps - create PATH expression
+ CompiledExpr[] childExprs = new CompiledExpr[steps.length];
+ for (int i = 0; i < steps.length; i++) {
+ childExprs[i] = CompiledExpr.child(steps[i].name);
+ }
+ return CompiledExpr.path(childExprs, null);
+ }
+
+ /**
+ * Recompute flags after normalization (parent steps may have been removed).
+ */
+ private static int recomputeFlags(CompiledStep[] steps, int originalFlags) {
+ // Keep absolute/descendant-or-self flags from original
+ int flags = originalFlags & (FLAG_ABSOLUTE_PATH | FLAG_DESCENDANT_OR_SELF);
+
+ for (CompiledStep s : steps) {
+ if (s.isDescendant) flags |= FLAG_HAS_DESCENDANT;
+
+ switch (s.type) {
+ case ABBREVIATED_DOT:
+ case ABBREVIATED_DOTDOT:
+ flags |= FLAG_HAS_ABBREVIATED_STEP;
+ break;
+ case AXIS_STEP:
+ flags |= FLAG_HAS_AXIS_STEP;
+ break;
+ case ATTRIBUTE_STEP:
+ flags |= FLAG_HAS_ATTRIBUTE_STEP;
+ break;
+ case NODE_TYPE_TEST:
+ flags |= FLAG_HAS_NODE_TYPE_TEST;
+ break;
+ }
+ }
+ return flags;
+ }
+
+ /**
+ * Count element steps (NODE_TEST type).
+ */
+ private static int countElementSteps(CompiledStep[] steps) {
+ int count = 0;
+ for (CompiledStep s : steps) {
+ if (s.type == StepType.NODE_TEST) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Compile a top-level boolean expression: functionCall [comparisonOp comparand]
+ */
+ private static CompiledExpr compileBooleanExpr(XPathParser.BooleanExprContext ctx) {
+ CompiledExpr funcExpr = compileFunctionCall(ctx.functionCall());
+
+ if (ctx.comparisonOp() != null && ctx.comparand() != null) {
+ ComparisonOp op = compileComparisonOp(ctx.comparisonOp());
+ CompiledExpr comparand = compileComparand(ctx.comparand());
+ return CompiledExpr.comparison(funcExpr, op, comparand);
+ }
+
+ return funcExpr;
+ }
+
+ /**
+ * Compile a filter expression: (path)[predicate] [/trailing]
+ */
+ private static CompiledFilterExpr compileFilterExpr(XPathParser.FilterExprContext ctx) {
+ // Get the path expression (absolute or relative) - first one is inside parentheses
+ String pathExpr;
+ if (ctx.absoluteLocationPath() != null) {
+ pathExpr = ctx.absoluteLocationPath().getText();
+ } else if (!ctx.relativeLocationPath().isEmpty()) {
+ pathExpr = ctx.relativeLocationPath(0).getText();
+ } else {
+ pathExpr = "";
+ }
+
+ // Compile predicates
+ CompiledExpr[] predicates = compilePredicates(ctx.predicate());
+
+ // Check for trailing path
+ String trailingPath = null;
+ boolean trailingIsDescendant = false;
+ if (ctx.pathSeparator() != null && ctx.relativeLocationPath().size() > 1) {
+ trailingPath = ctx.relativeLocationPath(1).getText();
+ trailingIsDescendant = ctx.pathSeparator().DOUBLE_SLASH() != null;
+ }
+
+ return new CompiledFilterExpr(pathExpr, predicates, trailingPath, trailingIsDescendant);
+ }
+
+ /**
+ * Get name from node test (handles QNAME, NCNAME, or WILDCARD).
+ */
+ static String getNodeTestName(XPathParser.@Nullable NodeTestContext nodeTest) {
+ if (nodeTest == null) {
+ return "*";
+ }
+ if (nodeTest.WILDCARD() != null) {
+ return "*";
+ }
+ return nodeTest.getText();
+ }
+
+ /**
+ * Get name from attribute step (handles QNAME, NCNAME, or WILDCARD).
+ */
+ static String getAttributeStepName(XPathParser.AttributeStepContext attrStep) {
+ if (attrStep.WILDCARD() != null) {
+ return "*";
+ }
+ if (attrStep.QNAME() != null) {
+ return attrStep.QNAME().getText();
+ }
+ if (attrStep.NCNAME() != null) {
+ return attrStep.NCNAME().getText();
+ }
+ return "*";
+ }
+
+ /**
+ * Get name from child element test (handles QNAME, NCNAME, or WILDCARD).
+ */
+ static String getChildElementTestName(XPathParser.ChildElementTestContext childTest) {
+ if (childTest.QNAME() != null) {
+ return childTest.QNAME().getText();
+ }
+ if (childTest.NCNAME() != null) {
+ return childTest.NCNAME().getText();
+ }
+ return "*";
+ }
+
+ // ==================== Predicate Compilation ====================
+
+ private static final CompiledExpr[] EMPTY_PREDICATES = new CompiledExpr[0];
+
+ /**
+ * Compile a list of predicates into CompiledExpr array.
+ */
+ static CompiledExpr[] compilePredicates(@Nullable List predicates) {
+ if (predicates == null || predicates.isEmpty()) {
+ return EMPTY_PREDICATES;
+ }
+ CompiledExpr[] result = new CompiledExpr[predicates.size()];
+ for (int i = 0; i < predicates.size(); i++) {
+ result[i] = compilePredicate(predicates.get(i));
+ }
+ return result;
+ }
+
+ /**
+ * Compile a single predicate.
+ */
+ static CompiledExpr compilePredicate(XPathParser.PredicateContext predicate) {
+ if (predicate.predicateExpr() == null || predicate.predicateExpr().orExpr() == null) {
+ return CompiledExpr.unsupported("empty predicate");
+ }
+ return compileOrExpr(predicate.predicateExpr().orExpr());
+ }
+
+ /**
+ * Compile an OR expression (supports: expr or expr or ...)
+ */
+ static CompiledExpr compileOrExpr(XPathParser.OrExprContext orExpr) {
+ List andExprs = orExpr.andExpr();
+ if (andExprs.isEmpty()) {
+ return CompiledExpr.unsupported("empty or expression");
+ }
+ CompiledExpr result = compileAndExpr(andExprs.get(0));
+ for (int i = 1; i < andExprs.size(); i++) {
+ result = CompiledExpr.or(result, compileAndExpr(andExprs.get(i)));
+ }
+ return result;
+ }
+
+ /**
+ * Compile an AND expression (supports: expr and expr and ...)
+ */
+ static CompiledExpr compileAndExpr(XPathParser.AndExprContext andExpr) {
+ List primaries = andExpr.primaryExpr();
+ if (primaries.isEmpty()) {
+ return CompiledExpr.unsupported("empty and expression");
+ }
+ CompiledExpr result = compilePrimaryExpr(primaries.get(0));
+ for (int i = 1; i < primaries.size(); i++) {
+ result = CompiledExpr.and(result, compilePrimaryExpr(primaries.get(i)));
+ }
+ return result;
+ }
+
+ /**
+ * Compile a primary expression (predicateValue with optional comparison).
+ */
+ static CompiledExpr compilePrimaryExpr(XPathParser.PrimaryExprContext primary) {
+ if (primary.predicateValue() == null) {
+ return CompiledExpr.unsupported("missing predicate value");
+ }
+
+ CompiledExpr left = compilePredicateValue(primary.predicateValue());
+
+ // Check for comparison
+ if (primary.comparisonOp() != null && primary.comparand() != null) {
+ ComparisonOp op = compileComparisonOp(primary.comparisonOp());
+ CompiledExpr right = compileComparand(primary.comparand());
+ return CompiledExpr.comparison(left, op, right);
+ }
+
+ return left;
+ }
+
+ /**
+ * Compile a predicate value (function, attribute, child, number).
+ */
+ static CompiledExpr compilePredicateValue(XPathParser.PredicateValueContext pv) {
+ // Numeric predicate [1], [2], etc.
+ if (pv.NUMBER() != null) {
+ try {
+ int value = Integer.parseInt(pv.NUMBER().getText());
+ return CompiledExpr.numeric(value);
+ } catch (NumberFormatException e) {
+ return CompiledExpr.unsupported("invalid number: " + pv.NUMBER().getText());
+ }
+ }
+
+ // Function call: position(), last(), local-name(), contains(), etc.
+ if (pv.functionCall() != null) {
+ return compileFunctionCall(pv.functionCall());
+ }
+
+ // Attribute: @foo, @*
+ if (pv.attributeStep() != null) {
+ return CompiledExpr.attribute(getAttributeStepName(pv.attributeStep()));
+ }
+
+ // Child element test: foo, *
+ if (pv.childElementTest() != null) {
+ return CompiledExpr.child(getChildElementTestName(pv.childElementTest()));
+ }
+
+ // Relative path (e.g., bar/baz/text())
+ if (pv.relativeLocationPath() != null) {
+ return compileRelativePath(pv.relativeLocationPath());
+ }
+
+ return CompiledExpr.unsupported("unknown predicate value");
+ }
+
+ /**
+ * Compile a relative path in a predicate (e.g., bar/baz/text()).
+ */
+ static CompiledExpr compileRelativePath(XPathParser.RelativeLocationPathContext relPath) {
+ List steps = relPath.step();
+ if (steps.isEmpty()) {
+ return CompiledExpr.unsupported("empty path");
+ }
+
+ // Check if last step is a node type test (like text())
+ XPathParser.StepContext lastStep = steps.get(steps.size() - 1);
+ FunctionType terminalFunction = null;
+ int elementStepCount = steps.size();
+
+ if (lastStep.nodeTypeTest() != null) {
+ String funcName = lastStep.nodeTypeTest().NCNAME().getText();
+ terminalFunction = getFunctionType(funcName);
+ elementStepCount = steps.size() - 1;
+ }
+
+ // Single element step with no terminal function -> CHILD
+ if (elementStepCount == 1 && terminalFunction == null && steps.get(0).nodeTest() != null) {
+ return CompiledExpr.child(steps.get(0).nodeTest().getText());
+ }
+
+ // Build array of child expressions for each element step
+ CompiledExpr[] pathSteps = new CompiledExpr[elementStepCount];
+ for (int i = 0; i < elementStepCount; i++) {
+ XPathParser.StepContext step = steps.get(i);
+ if (step.nodeTest() != null) {
+ pathSteps[i] = CompiledExpr.child(step.nodeTest().getText());
+ } else {
+ // Can't handle complex steps in path
+ return CompiledExpr.unsupported("complex step in path");
+ }
+ }
+
+ return CompiledExpr.path(pathSteps, terminalFunction);
+ }
+
+ /**
+ * Compile a function call.
+ */
+ static CompiledExpr compileFunctionCall(XPathParser.FunctionCallContext fc) {
+ // Built-in function tokens
+ if (fc.LOCAL_NAME() != null) {
+ return CompiledExpr.function(FunctionType.LOCAL_NAME);
+ }
+ if (fc.NAMESPACE_URI() != null) {
+ return CompiledExpr.function(FunctionType.NAMESPACE_URI);
+ }
+
+ // Named function (NCNAME)
+ if (fc.NCNAME() != null) {
+ String funcName = fc.NCNAME().getText();
+ FunctionType type = getFunctionType(funcName);
+
+ // Compile arguments if present
+ CompiledExpr[] args = EMPTY_PREDICATES;
+ if (fc.functionArgs() != null) {
+ List argCtxs = fc.functionArgs().functionArg();
+ args = new CompiledExpr[argCtxs.size()];
+ for (int i = 0; i < argCtxs.size(); i++) {
+ args[i] = compileFunctionArg(argCtxs.get(i));
+ }
+ }
+
+ return CompiledExpr.function(type, args);
+ }
+
+ return CompiledExpr.unsupported("unknown function");
+ }
+
+ /**
+ * Compile a function argument.
+ */
+ static CompiledExpr compileFunctionArg(XPathParser.FunctionArgContext arg) {
+ if (arg.stringLiteral() != null) {
+ return CompiledExpr.string(stripQuotes(arg.stringLiteral().getText()));
+ }
+ if (arg.NUMBER() != null) {
+ try {
+ return CompiledExpr.numeric(Integer.parseInt(arg.NUMBER().getText()));
+ } catch (NumberFormatException e) {
+ return CompiledExpr.unsupported("invalid number");
+ }
+ }
+ if (arg.functionCall() != null) {
+ return compileFunctionCall(arg.functionCall());
+ }
+ // Path arguments (for contains(path, 'str'))
+ if (arg.relativeLocationPath() != null) {
+ List steps = arg.relativeLocationPath().step();
+ if (steps.size() == 1 && steps.get(0).nodeTest() != null) {
+ return CompiledExpr.child(steps.get(0).nodeTest().getText());
+ }
+ return CompiledExpr.unsupported("complex path argument");
+ }
+ if (arg.absoluteLocationPath() != null) {
+ return CompiledExpr.absolutePath(arg.absoluteLocationPath().getText());
+ }
+ return CompiledExpr.unsupported("unknown argument type");
+ }
+
+ /**
+ * Compile a comparand (right side of comparison).
+ */
+ static CompiledExpr compileComparand(XPathParser.ComparandContext comparand) {
+ if (comparand.stringLiteral() != null) {
+ return CompiledExpr.string(stripQuotes(comparand.stringLiteral().getText()));
+ }
+ if (comparand.NUMBER() != null) {
+ try {
+ return CompiledExpr.numeric(Integer.parseInt(comparand.NUMBER().getText()));
+ } catch (NumberFormatException e) {
+ return CompiledExpr.unsupported("invalid number");
+ }
+ }
+ return CompiledExpr.unsupported("unknown comparand");
+ }
+
+ /**
+ * Compile a comparison operator.
+ */
+ static ComparisonOp compileComparisonOp(XPathParser.ComparisonOpContext op) {
+ if (op.EQUALS() != null) return ComparisonOp.EQ;
+ if (op.NOT_EQUALS() != null) return ComparisonOp.NE;
+ if (op.LT() != null) return ComparisonOp.LT;
+ if (op.LTE() != null) return ComparisonOp.LE;
+ if (op.GT() != null) return ComparisonOp.GT;
+ if (op.GTE() != null) return ComparisonOp.GE;
+ return ComparisonOp.EQ; // default
+ }
+
+ /**
+ * Map function name to FunctionType.
+ */
+ static FunctionType getFunctionType(String name) {
+ switch (name) {
+ case "position": return FunctionType.POSITION;
+ case "last": return FunctionType.LAST;
+ case "local-name": return FunctionType.LOCAL_NAME;
+ case "namespace-uri": return FunctionType.NAMESPACE_URI;
+ case "contains": return FunctionType.CONTAINS;
+ case "starts-with": return FunctionType.STARTS_WITH;
+ case "ends-with": return FunctionType.ENDS_WITH;
+ case "string-length": return FunctionType.STRING_LENGTH;
+ case "substring-before": return FunctionType.SUBSTRING_BEFORE;
+ case "substring-after": return FunctionType.SUBSTRING_AFTER;
+ case "count": return FunctionType.COUNT;
+ case "text": return FunctionType.TEXT;
+ case "not": return FunctionType.NOT;
+ default:
+ throw new IllegalArgumentException("Unsupported XPath function: " + name + "()");
+ }
+ }
+
+ /**
+ * Strip quotes from string literal.
+ */
+ static String stripQuotes(String s) {
+ if (s.length() < 2) return s;
+ char first = s.charAt(0);
+ if ((first == '\'' || first == '"') && s.charAt(s.length() - 1) == first) {
+ return s.substring(1, s.length() - 1);
+ }
+ return s;
+ }
+
+ /**
+ * Compiled XPath expression - holds all pre-compiled information.
+ * No ANTLR references - fully compiled for efficient matching.
+ */
+ public static final class CompiledXPath {
+ final CompiledStep[] steps;
+ final int flags;
+ final int elementStepCount;
+ final int exprType;
+
+ // For EXPR_BOOLEAN: compiled boolean expression
+ final @Nullable CompiledExpr booleanExpr;
+
+ // For EXPR_FILTER: compiled filter expression
+ final @Nullable CompiledFilterExpr filterExpr;
+
+ CompiledXPath(CompiledStep[] steps,
+ int flags,
+ int elementStepCount,
+ int exprType,
+ @Nullable CompiledExpr booleanExpr,
+ @Nullable CompiledFilterExpr filterExpr) {
+ this.steps = steps;
+ this.flags = flags;
+ this.elementStepCount = elementStepCount;
+ this.exprType = exprType;
+ this.booleanExpr = booleanExpr;
+ this.filterExpr = filterExpr;
+ }
+
+ public boolean isPathExpression() {
+ return exprType == EXPR_PATH;
+ }
+
+ public boolean isBooleanExpression() {
+ return exprType == EXPR_BOOLEAN;
+ }
+
+ public boolean isFilterExpression() {
+ return exprType == EXPR_FILTER;
+ }
+
+ public boolean hasAbsolutePath() {
+ return (flags & FLAG_ABSOLUTE_PATH) != 0;
+ }
+
+ public boolean hasDescendantOrSelf() {
+ return (flags & FLAG_DESCENDANT_OR_SELF) != 0;
+ }
+
+ public boolean hasDescendant() {
+ return (flags & FLAG_HAS_DESCENDANT) != 0;
+ }
+ }
+
+ /**
+ * Compiled filter expression: (/path/expr)[predicate]/trailing
+ */
+ public static final class CompiledFilterExpr {
+ final String pathExpr; // The path inside parentheses
+ final CompiledExpr[] predicates; // Predicates to apply
+ final @Nullable String trailingPath; // Optional trailing path after predicates
+ final boolean trailingIsDescendant; // Is trailing path preceded by //?
+
+ CompiledFilterExpr(String pathExpr, CompiledExpr[] predicates,
+ @Nullable String trailingPath, boolean trailingIsDescendant) {
+ this.pathExpr = pathExpr;
+ this.predicates = predicates;
+ this.trailingPath = trailingPath;
+ this.trailingIsDescendant = trailingIsDescendant;
+ }
+ }
+
+ /**
+ * Step types for compiled steps - avoids ANTLR tree navigation during matching.
+ */
+ public enum StepType {
+ ABBREVIATED_DOT, // .
+ ABBREVIATED_DOTDOT, // ..
+ AXIS_STEP, // parent::node(), self::element, child::*
+ ATTRIBUTE_STEP, // @attr, @*
+ NODE_TYPE_TEST, // text(), comment(), node(), processing-instruction()
+ NODE_TEST // element name or *
+ }
+
+ /**
+ * Axis types for axis steps.
+ */
+ public enum AxisType {
+ PARENT,
+ SELF,
+ CHILD,
+ OTHER // Unsupported axis
+ }
+
+ /**
+ * Node type test types.
+ */
+ public enum NodeTypeTestType {
+ TEXT,
+ COMMENT,
+ NODE,
+ PROCESSING_INSTRUCTION,
+ UNKNOWN
+ }
+
+ /**
+ * Fully compiled step information - all data extracted from ANTLR tree.
+ * No ANTLR context references needed during matching for simple steps.
+ */
+ public static final class CompiledStep {
+ final StepType type;
+ final boolean isDescendant;
+
+ // For NODE_TEST: element name (or "*" for wildcard)
+ // For AXIS_STEP: node test name from axis (e.g., "node" from parent::node())
+ // For ATTRIBUTE_STEP: attribute name (or "*" for @*)
+ final @Nullable String name;
+
+ // For AXIS_STEP only
+ final AxisType axisType;
+
+ // For NODE_TYPE_TEST only
+ final NodeTypeTestType nodeTypeTestType;
+
+ // Compiled predicates - no ANTLR references needed during matching
+ final CompiledExpr[] predicates;
+
+ // Step flags (bitmask)
+ private static final int STEP_FLAG_NEEDS_POSITION = 1;
+ private static final int STEP_FLAG_NEXT_IS_BACKTRACK = 1 << 1;
+
+ int flags;
+
+ private CompiledStep(StepType type, boolean isDescendant, @Nullable String name,
+ AxisType axisType, NodeTypeTestType nodeTypeTestType,
+ CompiledExpr[] predicates, int flags) {
+ this.type = type;
+ this.isDescendant = isDescendant;
+ this.name = name;
+ this.axisType = axisType;
+ this.nodeTypeTestType = nodeTypeTestType;
+ this.predicates = predicates;
+ this.flags = flags;
+ }
+
+ public StepType getType() {
+ return type;
+ }
+
+ public boolean isDescendant() {
+ return isDescendant;
+ }
+
+ public @Nullable String getName() {
+ return name;
+ }
+
+ public AxisType getAxisType() {
+ return axisType;
+ }
+
+ public NodeTypeTestType getNodeTypeTestType() {
+ return nodeTypeTestType;
+ }
+
+ public CompiledExpr[] getPredicates() {
+ return predicates;
+ }
+
+ boolean needsPositionInfo() {
+ return (flags & STEP_FLAG_NEEDS_POSITION) != 0;
+ }
+
+ boolean nextIsBacktrack() {
+ return (flags & STEP_FLAG_NEXT_IS_BACKTRACK) != 0;
+ }
+
+ void setNextIsBacktrack() {
+ flags |= STEP_FLAG_NEXT_IS_BACKTRACK;
+ }
+
+ static CompiledStep fromStepContext(XPathParser.StepContext step, boolean isDescendant) {
+ // Compile predicates into expressions
+ CompiledExpr[] predicates = compilePredicates(step.predicate());
+
+ // Compute flags
+ int flags = predicatesNeedPosition(predicates) ? STEP_FLAG_NEEDS_POSITION : 0;
+
+ // Abbreviated step: . or ..
+ if (step.abbreviatedStep() != null) {
+ if (step.abbreviatedStep().DOTDOT() != null) {
+ return new CompiledStep(StepType.ABBREVIATED_DOTDOT, isDescendant, null,
+ AxisType.OTHER, NodeTypeTestType.UNKNOWN, predicates, flags);
+ } else {
+ return new CompiledStep(StepType.ABBREVIATED_DOT, isDescendant, null,
+ AxisType.OTHER, NodeTypeTestType.UNKNOWN, predicates, flags);
+ }
+ }
+
+ // Axis step: parent::node(), self::element, etc.
+ if (step.axisStep() != null) {
+ XPathParser.AxisStepContext axisStep = step.axisStep();
+ String axisName = axisStep.axisName().NCNAME().getText();
+ String nodeTestName = getNodeTestName(axisStep.nodeTest());
+
+ AxisType axisType;
+ switch (axisName) {
+ case "parent":
+ axisType = AxisType.PARENT;
+ break;
+ case "self":
+ axisType = AxisType.SELF;
+ break;
+ case "child":
+ axisType = AxisType.CHILD;
+ break;
+ default:
+ axisType = AxisType.OTHER;
+ }
+
+ return new CompiledStep(StepType.AXIS_STEP, isDescendant, nodeTestName,
+ axisType, NodeTypeTestType.UNKNOWN, predicates, flags);
+ }
+
+ // Attribute step: @attr, @*
+ if (step.attributeStep() != null) {
+ String attrName = getAttributeStepName(step.attributeStep());
+ return new CompiledStep(StepType.ATTRIBUTE_STEP, isDescendant, attrName,
+ AxisType.OTHER, NodeTypeTestType.UNKNOWN, predicates, flags);
+ }
+
+ // Node type test: text(), comment(), node(), processing-instruction()
+ if (step.nodeTypeTest() != null) {
+ String functionName = step.nodeTypeTest().NCNAME().getText();
+ NodeTypeTestType testType;
+ switch (functionName) {
+ case "text":
+ testType = NodeTypeTestType.TEXT;
+ break;
+ case "comment":
+ testType = NodeTypeTestType.COMMENT;
+ break;
+ case "node":
+ testType = NodeTypeTestType.NODE;
+ break;
+ case "processing-instruction":
+ testType = NodeTypeTestType.PROCESSING_INSTRUCTION;
+ break;
+ default:
+ testType = NodeTypeTestType.UNKNOWN;
+ }
+ return new CompiledStep(StepType.NODE_TYPE_TEST, isDescendant, null,
+ AxisType.OTHER, testType, predicates, flags);
+ }
+
+ // Node test: element name or *
+ if (step.nodeTest() != null) {
+ String nodeName = step.nodeTest().getText();
+ return new CompiledStep(StepType.NODE_TEST, isDescendant, nodeName,
+ AxisType.OTHER, NodeTypeTestType.UNKNOWN, predicates, flags);
+ }
+
+ // Shouldn't reach here - return a non-matching step
+ return new CompiledStep(StepType.NODE_TEST, isDescendant, null,
+ AxisType.OTHER, NodeTypeTestType.UNKNOWN, predicates, flags);
+ }
+
+ /**
+ * Check if any compiled predicate needs position/size info.
+ */
+ private static boolean predicatesNeedPosition(CompiledExpr[] predicates) {
+ for (CompiledExpr pred : predicates) {
+ if (pred.needsPosition()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if this step is a backtrack step (.. or parent::).
+ */
+ boolean isBacktrack() {
+ return type == StepType.ABBREVIATED_DOTDOT ||
+ (type == StepType.AXIS_STEP && axisType == AxisType.PARENT);
+ }
+
+ /**
+ * Create a new step with an additional predicate appended.
+ */
+ CompiledStep withAdditionalPredicate(CompiledExpr predicate) {
+ CompiledExpr[] newPredicates = new CompiledExpr[predicates.length + 1];
+ System.arraycopy(predicates, 0, newPredicates, 0, predicates.length);
+ newPredicates[predicates.length] = predicate;
+ int newFlags = flags;
+ if (predicate.needsPosition()) {
+ newFlags |= STEP_FLAG_NEEDS_POSITION;
+ }
+ return new CompiledStep(type, isDescendant, name, axisType, nodeTypeTestType, newPredicates, newFlags);
+ }
+ }
+
+ // ==================== Compiled Expression Types ====================
+
+ /**
+ * Comparison operators for predicate expressions.
+ */
+ public enum ComparisonOp {
+ EQ, // =
+ NE, // !=
+ LT, // <
+ LE, // <=
+ GT, // >
+ GE // >=
+ }
+
+ /**
+ * Function call types we support.
+ */
+ public enum FunctionType {
+ POSITION, // position()
+ LAST, // last()
+ LOCAL_NAME, // local-name()
+ NAMESPACE_URI, // namespace-uri()
+ CONTAINS, // contains(str, substr)
+ STARTS_WITH, // starts-with(str, prefix)
+ ENDS_WITH, // ends-with(str, suffix)
+ STRING_LENGTH, // string-length(str)
+ SUBSTRING_BEFORE, // substring-before(str, delim)
+ SUBSTRING_AFTER, // substring-after(str, delim)
+ COUNT, // count(nodeset)
+ TEXT, // text()
+ NOT // not(expr)
+ }
+
+ /**
+ * Expression types for compiled predicate expressions.
+ */
+ public enum ExprType {
+ NUMERIC, // [1], [2], etc. - position check
+ STRING, // 'value'
+ COMPARISON, // left op right
+ AND, // expr1 and expr2
+ OR, // expr1 or expr2
+ FUNCTION, // position(), local-name(), contains(), etc.
+ ATTRIBUTE, // @name or @*
+ CHILD, // childName or * (in predicates)
+ PATH, // Multi-step relative path (e.g., bar/baz/text())
+ ABSOLUTE_PATH, // Absolute path like /root/element1
+ BOOLEAN, // true/false
+ }
+
+ /**
+ * Compiled predicate expression - fully compiled, no ANTLR references.
+ * Uses a discriminated union pattern for Java 8 compatibility.
+ */
+ public static final class CompiledExpr {
+ final ExprType type;
+
+ // For NUMERIC
+ final int numericValue;
+
+ // For STRING
+ final @Nullable String stringValue;
+
+ // For COMPARISON
+ final @Nullable CompiledExpr left;
+ final @Nullable ComparisonOp op;
+ final @Nullable CompiledExpr right;
+
+ // For FUNCTION
+ final @Nullable FunctionType functionType;
+ final CompiledExpr @Nullable [] args;
+
+ // For ATTRIBUTE, CHILD
+ final @Nullable String name;
+
+ // For BOOLEAN
+ final boolean booleanValue;
+
+ // Private constructor - use factory methods
+ private CompiledExpr(ExprType type, int numericValue, @Nullable String stringValue,
+ @Nullable CompiledExpr left, @Nullable ComparisonOp op, @Nullable CompiledExpr right,
+ @Nullable FunctionType functionType, CompiledExpr @Nullable [] args,
+ @Nullable String name, boolean booleanValue) {
+ this.type = type;
+ this.numericValue = numericValue;
+ this.stringValue = stringValue;
+ this.left = left;
+ this.op = op;
+ this.right = right;
+ this.functionType = functionType;
+ this.args = args;
+ this.name = name;
+ this.booleanValue = booleanValue;
+ }
+
+ // Factory methods
+ public static CompiledExpr numeric(int value) {
+ return new CompiledExpr(ExprType.NUMERIC, value, null, null, null, null, null, null, null, false);
+ }
+
+ public static CompiledExpr string(String value) {
+ return new CompiledExpr(ExprType.STRING, 0, value, null, null, null, null, null, null, false);
+ }
+
+ public static CompiledExpr comparison(CompiledExpr left, ComparisonOp op, CompiledExpr right) {
+ return new CompiledExpr(ExprType.COMPARISON, 0, null, left, op, right, null, null, null, false);
+ }
+
+ public static CompiledExpr and(CompiledExpr left, CompiledExpr right) {
+ return new CompiledExpr(ExprType.AND, 0, null, left, null, right, null, null, null, false);
+ }
+
+ public static CompiledExpr or(CompiledExpr left, CompiledExpr right) {
+ return new CompiledExpr(ExprType.OR, 0, null, left, null, right, null, null, null, false);
+ }
+
+ public static CompiledExpr function(FunctionType type, CompiledExpr... args) {
+ return new CompiledExpr(ExprType.FUNCTION, 0, null, null, null, null, type, args, null, false);
+ }
+
+ public static CompiledExpr attribute(@Nullable String name) {
+ return new CompiledExpr(ExprType.ATTRIBUTE, 0, null, null, null, null, null, null, name, false);
+ }
+
+ public static CompiledExpr child(@Nullable String name) {
+ return new CompiledExpr(ExprType.CHILD, 0, null, null, null, null, null, null, name, false);
+ }
+
+ /**
+ * Create a PATH expression for multi-step relative paths.
+ * @param steps Array of CHILD expressions representing each step
+ * @param terminalFunction Optional function at the end (e.g., text())
+ */
+ public static CompiledExpr path(CompiledExpr[] steps, @Nullable FunctionType terminalFunction) {
+ return new CompiledExpr(ExprType.PATH, 0, null, null, null, null, terminalFunction, steps, null, false);
+ }
+
+ public static CompiledExpr bool(boolean value) {
+ return new CompiledExpr(ExprType.BOOLEAN, 0, null, null, null, null, null, null, null, value);
+ }
+
+ /**
+ * Create an ABSOLUTE_PATH expression for absolute path arguments.
+ * The path string is stored in stringValue.
+ */
+ public static CompiledExpr absolutePath(String pathExpr) {
+ return new CompiledExpr(ExprType.ABSOLUTE_PATH, 0, pathExpr, null, null, null, null, null, null, false);
+ }
+
+ /**
+ * Throws an exception for unsupported XPath constructs.
+ * Called at compile time to fail fast with a descriptive message.
+ */
+ public static CompiledExpr unsupported(String description) {
+ throw new UnsupportedOperationException("Unsupported XPath expression: " + description);
+ }
+
+ /**
+ * Check if this expression needs position/size context to evaluate.
+ */
+ public boolean needsPosition() {
+ switch (type) {
+ case NUMERIC:
+ return true;
+ case FUNCTION:
+ return functionType == FunctionType.POSITION || functionType == FunctionType.LAST;
+ case COMPARISON:
+ case AND:
+ case OR:
+ return (left != null && left.needsPosition()) || (right != null && right.needsPosition());
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if this is a wildcard attribute or child expression.
+ */
+ public boolean isWildcard() {
+ return (type == ExprType.ATTRIBUTE || type == ExprType.CHILD) &&
+ (name == null || "*".equals(name));
+ }
+
+ /**
+ * Check if this expression contains any relative paths (CHILD or PATH types).
+ * Relative paths should be evaluated from the cursor context, not from root.
+ * ABSOLUTE_PATH expressions are not considered relative.
+ */
+ public boolean hasRelativePath() {
+ switch (type) {
+ case CHILD:
+ case PATH:
+ return true;
+ case COMPARISON:
+ case AND:
+ case OR:
+ return (left != null && left.hasRelativePath()) ||
+ (right != null && right.hasRelativePath());
+ case FUNCTION:
+ if (args != null) {
+ for (CompiledExpr arg : args) {
+ if (arg.hasRelativePath()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if this expression contains only pure absolute paths (starting with / but not //).
+ * Pure absolute paths like /foo/bar require cursor to be at root.
+ * Descendant paths like //foo can match at any cursor position.
+ * Returns true if the expression has at least one ABSOLUTE_PATH and all are pure absolute.
+ */
+ public boolean hasPureAbsolutePath() {
+ switch (type) {
+ case ABSOLUTE_PATH:
+ // Check if path starts with // (descendant) vs / (pure absolute)
+ return stringValue != null && stringValue.startsWith("/") && !stringValue.startsWith("//");
+ case COMPARISON:
+ case AND:
+ case OR:
+ // Both sides must have pure absolute paths (if they have any paths)
+ boolean leftPure = left == null || !left.hasAnyAbsolutePath() || left.hasPureAbsolutePath();
+ boolean rightPure = right == null || !right.hasAnyAbsolutePath() || right.hasPureAbsolutePath();
+ boolean hasAny = (left != null && left.hasAnyAbsolutePath()) ||
+ (right != null && right.hasAnyAbsolutePath());
+ return hasAny && leftPure && rightPure;
+ case FUNCTION:
+ if (args != null) {
+ boolean anyAbsolute = false;
+ for (CompiledExpr arg : args) {
+ if (arg.hasAnyAbsolutePath()) {
+ anyAbsolute = true;
+ if (!arg.hasPureAbsolutePath()) {
+ return false; // Has descendant path
+ }
+ }
+ }
+ return anyAbsolute;
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if this expression contains any ABSOLUTE_PATH expressions.
+ */
+ private boolean hasAnyAbsolutePath() {
+ switch (type) {
+ case ABSOLUTE_PATH:
+ return true;
+ case COMPARISON:
+ case AND:
+ case OR:
+ return (left != null && left.hasAnyAbsolutePath()) ||
+ (right != null && right.hasAnyAbsolutePath());
+ case FUNCTION:
+ if (args != null) {
+ for (CompiledExpr arg : args) {
+ if (arg.hasAnyAbsolutePath()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ default:
+ return false;
+ }
+ }
+
+ public ExprType getType() {
+ return type;
+ }
+ }
+}
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
index 025625ac4d..49eeda46da 100644
--- a/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/XPathMatcher.java
@@ -17,56 +17,34 @@
import org.jspecify.annotations.Nullable;
import org.openrewrite.Cursor;
-import org.openrewrite.internal.StringUtils;
-import org.openrewrite.xml.search.FindTags;
-import org.openrewrite.xml.trait.Namespaced;
+import org.openrewrite.xml.XPathCompiler.*;
+import org.openrewrite.xml.tree.Content;
import org.openrewrite.xml.tree.Xml;
-import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
-import static java.util.Collections.reverse;
+import static org.openrewrite.xml.XPathCompiler.FLAG_ABSOLUTE_PATH;
/**
* Supports a limited set of XPath expressions, specifically those documented on this page.
- * Additionally, supports `local-name()` and `namespace-uri()` conditions, `and`/`or` operators, and chained conditions.
+ * Additionally, supports `local-name()` and `namespace-uri()` conditions, `and`/`or` operators, chained conditions,
+ * and abbreviated syntax (`.` for self, `..` for parent within a path).
*
* Used for checking whether a visitor's cursor meets a certain XPath expression.
- *
- * The "current node" for XPath evaluation is always the root node of the document. As a result, '.' and '..' are not
- * recognized.
*/
+@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public class XPathMatcher {
- private static final Pattern XPATH_ELEMENT_SPLITTER = Pattern.compile("((?<=/)(?=/)|[^/\\[]|\\[[^]]*])+");
- // Regular expression to support conditional tags like `plugin[artifactId='maven-compiler-plugin']` or foo[@bar='baz']
- private static final Pattern ELEMENT_WITH_CONDITION_PATTERN = Pattern.compile("(@)?([-:\\w]+|\\*)(\\[.+])");
- private static final Pattern CONDITION_PATTERN = Pattern.compile("(\\[.*?])+?");
- private static final Pattern CONDITION_CONJUNCTION_PATTERN = Pattern.compile("(((local-name|namespace-uri|text)\\(\\)|(@)?([-\\w:]+|\\*))\\h*=\\h*[\"'](.*?)[\"'](\\h?(or|and)\\h?)?)+?");
-
private final String expression;
- private final boolean startsWithSlash;
- private final boolean startsWithDoubleSlash;
- private final String[] parts;
- private final long tagMatchingParts;
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private volatile CompiledXPath compiled;
public XPathMatcher(String expression) {
this.expression = expression;
- startsWithSlash = expression.startsWith("/");
- startsWithDoubleSlash = expression.startsWith("//");
- parts = splitOnXPathSeparator(expression.substring(startsWithDoubleSlash ? 2 : startsWithSlash ? 1 : 0));
- tagMatchingParts = Arrays.stream(parts).filter(part -> !part.isEmpty() && !part.startsWith("@")).count();
- }
-
- private String[] splitOnXPathSeparator(String input) {
- List matches = new ArrayList<>();
- Matcher m = XPATH_ELEMENT_SPLITTER.matcher(input);
- while (m.find()) {
- matches.add(m.group());
- }
- return matches.toArray(new String[0]);
}
/**
@@ -76,269 +54,1393 @@ private String[] splitOnXPathSeparator(String input) {
* @return true if the expression matches the cursor, false otherwise
*/
public boolean matches(Cursor cursor) {
- List path = new ArrayList<>();
- for (Cursor c = cursor; c != null; c = c.getParent()) {
- if (c.getValue() instanceof Xml.Tag) {
- path.add(c.getValue());
- }
+ // Ensure expression is parsed and steps are compiled
+ CompiledXPath xpath = compile();
+
+ // Path expressions use bottom-up matching (consecutive parent steps
+ // are normalized to predicates at compile time)
+ if (xpath.isPathExpression() && xpath.steps.length > 0) {
+ return matchBottomUp(cursor);
}
- if (startsWithDoubleSlash || !startsWithSlash) {
- int pathIndex = 0;
- for (int i = parts.length - 1; i >= 0; i--, pathIndex++) {
- String part = parts[i];
+ // Boolean and filter expressions use specialized evaluation
+ return matchTopDown(cursor);
+ }
- String partWithCondition = null;
- Xml.Tag tagForCondition = null;
- boolean conditionIsBefore = false;
- if (part.endsWith("]") && i < path.size()) {
- int index = part.indexOf("[");
- if (index < 0) {
- return false;
- }
- partWithCondition = part;
- tagForCondition = path.get(pathIndex);
- } else if (i < path.size() && i > 0 && parts[i - 1].endsWith("]")) {
- String partBefore = parts[i - 1];
- int index = partBefore.indexOf("[");
- if (index < 0) {
- return false;
- }
- if (!partBefore.contains("@")) {
- conditionIsBefore = true;
- partWithCondition = partBefore;
- tagForCondition = path.get(parts.length - i);
- }
- } else if (part.endsWith(")")) { // is xpath method
- // TODO: implement other xpath methods
- throw new UnsupportedOperationException("XPath methods are not supported");
+ private CompiledXPath compile() {
+ CompiledXPath result = compiled;
+ //noinspection ConstantValue
+ if (result == null) {
+ synchronized (this) {
+ result = compiled;
+ if (result == null) {
+ compiled = result = XPathCompiler.compile(expression);
}
+ }
+ }
+ return result;
+ }
- String partName;
- boolean matchedCondition = false;
-
- Matcher matcher;
- if (tagForCondition != null && partWithCondition.endsWith("]") &&
- (matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(partWithCondition)).matches()) {
- String optionalPartName = matchesElementWithConditionFunction(matcher, tagForCondition, cursor);
- if (optionalPartName == null) {
- return false;
+ /**
+ * Bottom-up matching: work backwards through compiled steps from the cursor.
+ * This avoids building a path array and fails fast on mismatches.
+ */
+ private boolean matchBottomUp(Cursor cursor) {
+ CompiledStep[] steps = compiled.steps;
+ // Early reject: for absolute paths without any //, if cursor depth exceeds element steps, can't match
+ // e.g., /a/b has 2 element steps, so cursor at depth 3+ can never match
+ // But /a//b can match at any depth due to the // so we can't apply this optimization
+ if (compiled.hasAbsolutePath() && !compiled.hasDescendant() && compiled.elementStepCount > 0) {
+ int depth = 0;
+ for (Cursor c = cursor; c != null; c = c.getParent()) {
+ if (c.getValue() instanceof Xml.Tag) {
+ depth++;
+ if (depth > compiled.elementStepCount) {
+ return false; // Too deep
}
- partName = optionalPartName;
- matchedCondition = true;
- } else {
- partName = null;
}
+ }
+ }
- if (part.startsWith("@")) {
- if (!matchedCondition) {
- if (!(cursor.getValue() instanceof Xml.Attribute)) {
- return false;
- }
- Xml.Attribute attribute = cursor.getValue();
- if (!attribute.getKeyAsString().equals(part.substring(1)) && !"*".equals(part.substring(1))) {
- return false;
- }
- }
+ // Get current element
+ Object cursorValue = cursor.getValue();
- pathIndex--;
- continue;
+ // Match last step against current cursor position
+ CompiledStep lastStep = steps[steps.length - 1];
+
+ // Handle attribute matching
+ if (lastStep.getType() == StepType.ATTRIBUTE_STEP) {
+ if (!(cursorValue instanceof Xml.Attribute)) {
+ return false;
+ }
+ Xml.Attribute attr = (Xml.Attribute) cursorValue;
+ if (!matchesName(lastStep.getName(), attr.getKeyAsString())) {
+ return false;
+ }
+ // Check predicates on attribute
+ if (lastStep.getPredicates().length > 0) {
+ if (!evaluateAttributePredicatesBottomUp(lastStep.getPredicates(), attr, cursor)) {
+ return false;
}
+ }
+ // For attributes, continue matching from parent tag
+ Cursor parentCursor = cursor.getParent();
+ if (parentCursor == null || !(parentCursor.getValue() instanceof Xml.Tag)) {
+ return false;
+ }
+ return matchRemainingStepsBottomUp(parentCursor, steps.length - 2);
+ }
- boolean conditionNotFulfilled = tagForCondition == null ||
- (!part.equals(partName) && !tagForCondition.getName().equals(partName));
+ // Handle node type test (text(), comment(), etc.)
+ if (lastStep.getType() == StepType.NODE_TYPE_TEST) {
+ return matchNodeTypeTestBottomUp(lastStep, cursor, steps.length - 2);
+ }
- int idx = part.indexOf("[");
- if (idx > 0) {
- part = part.substring(0, idx);
- }
- if (path.size() < i + 1 ||
- (!(path.get(pathIndex).getName().equals(part)) && !"*".equals(part)) ||
- conditionIsBefore && conditionNotFulfilled) {
+ // Handle parent axis (parent::node()) or abbreviated parent (..) as last step
+ // This means the cursor should be at a parent position, and we need to verify
+ // the step before this exists as a child
+ if (lastStep.getType() == StepType.ABBREVIATED_DOTDOT ||
+ (lastStep.getType() == StepType.AXIS_STEP && lastStep.getAxisType() == AxisType.PARENT)) {
+ return matchParentStepAsLast(lastStep, cursor, steps.length - 2);
+ }
+
+ // Handle self axis (self::node() or .) as last step
+ if (lastStep.getType() == StepType.ABBREVIATED_DOT ||
+ (lastStep.getType() == StepType.AXIS_STEP && lastStep.getAxisType() == AxisType.SELF)) {
+ // . or self:: means current position - cursor must be a tag matching the name test (if any)
+ if (!(cursorValue instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag tag = (Xml.Tag) cursorValue;
+ if (lastStep.getType() == StepType.AXIS_STEP && !matchesElementName(lastStep.getName(), tag.getName())) {
+ return false;
+ }
+ // Continue matching from current position (don't go up)
+ return matchRemainingStepsBottomUp(cursor, steps.length - 2);
+ }
+
+ // For element matching, cursor must be a tag
+ if (!(cursorValue instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag currentTag = (Xml.Tag) cursorValue;
+
+ // Check last step matches current tag
+ if (!matchStepAgainstTag(lastStep, currentTag, cursor)) {
+ return false;
+ }
+
+ // Match remaining steps going up the cursor chain
+ Cursor parentCursor = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(parentCursor, steps.length - 2);
+ }
+
+ /**
+ * Handle parent step (.. or parent::) as the last step.
+ * This means we're at a parent position, and we need to verify the child exists.
+ */
+ private boolean matchParentStepAsLast(CompiledStep parentStep, Cursor cursor, int prevStepIdx) {
+ Object cursorValue = cursor.getValue();
+ if (!(cursorValue instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag currentTag = (Xml.Tag) cursorValue;
+
+ // Check node test name for parent:: axis
+ if (parentStep.type == StepType.AXIS_STEP && !matchesElementName(parentStep.name, currentTag.getName())) {
+ return false;
+ }
+
+ // If there's a previous step, verify it exists as a child
+ if (prevStepIdx >= 0) {
+ CompiledStep prevStep = compiled.steps[prevStepIdx];
+ // The previous step should exist as a child of current position
+ if (prevStep.type == StepType.NODE_TEST && prevStep.name != null) {
+ if (!hasChildWithName(currentTag, prevStep.name)) {
return false;
}
}
+ // Continue matching from current position, skipping the child verification step
+ return matchRemainingStepsBottomUp(cursor, prevStepIdx - 1);
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if a tag has a direct child with the given name.
+ */
+ private boolean hasChildWithName(Xml.Tag parent, String childName) {
+ return findChildTag(parent, childName) != null;
+ }
+
+ /**
+ * Handle .. (parent step) in the middle of a path.
+ * The step at prevStepIdx should be checked as a child existence test.
+ */
+ private boolean matchParentStepInMiddle(@Nullable Cursor cursor, int prevStepIdx) {
+ if (cursor == null || !(cursor.getValue() instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag currentTag = cursor.getValue();
- // we have matched the whole XPath, and it does not start with the root
+ if (prevStepIdx < 0) {
+ // No more steps - verify we're at root
+ if ((compiled.flags & FLAG_ABSOLUTE_PATH) != 0 && !compiled.steps[0].isDescendant) {
+ Cursor parentCursor = getParentTagCursor(cursor);
+ return parentCursor == null || !(parentCursor.getValue() instanceof Xml.Tag);
+ }
return true;
- } else {
- reverse(path);
-
- // Deal with the two forward slashes in the expression; works, but I'm not proud of it.
- if (expression.contains("//") && Arrays.stream(parts).anyMatch(StringUtils::isBlank)) {
- int blankPartIndex = Arrays.asList(parts).indexOf("");
- int doubleSlashIndex = expression.indexOf("//");
-
- if (path.size() > blankPartIndex && path.size() >= tagMatchingParts) {
- Xml.Tag blankPartTag = path.get(blankPartIndex);
- String part = parts[blankPartIndex + 1];
- Matcher matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(part);
- if (matcher.matches() ?
- matchesElementWithConditionFunction(matcher, blankPartTag, cursor) != null :
- Objects.equals(blankPartTag.getName(), part)) {
- if (matchesWithoutDoubleSlashesAt(cursor, doubleSlashIndex)) {
- return true;
- }
- // fall-through: maybe we can skip this element and match further down
+ }
+
+ CompiledStep prevStep = compiled.steps[prevStepIdx];
+
+ // The previous step should exist as a child (this is the "detour" we took before going up)
+ if (prevStep.type == StepType.NODE_TEST && prevStep.name != null) {
+ if (!hasChildWithName(currentTag, prevStep.name)) {
+ return false;
+ }
+ // Skip the child verification step and continue matching
+ return matchRemainingStepsBottomUp(cursor, prevStepIdx - 1);
+ }
+
+ // For other step types, just continue (shouldn't normally happen)
+ return matchRemainingStepsBottomUp(cursor, prevStepIdx - 1);
+ }
+
+ /**
+ * Evaluate attribute predicates in bottom-up context.
+ * Uses compiled expressions - no ANTLR tree traversal needed.
+ */
+ private boolean evaluateAttributePredicatesBottomUp(CompiledExpr[] predicates,
+ Xml.Attribute attr, Cursor cursor) {
+ for (CompiledExpr predicate : predicates) {
+ if (!evaluateExpr(predicate, null, attr, cursor, 1, 1)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Match node type test in bottom-up context.
+ */
+ private boolean matchNodeTypeTestBottomUp(CompiledStep step, Cursor cursor, int nextStepIdx) {
+ Object cursorValue = cursor.getValue();
+
+ switch (step.nodeTypeTestType) {
+ case TEXT:
+ // text() can match:
+ // 1. When cursor is on Xml.CharData - continue from parent tag
+ // 2. When cursor is on a tag that has text content - continue from same position
+ // (text() verifies content exists, doesn't change traversal level)
+ if (cursorValue instanceof Xml.CharData) {
+ Cursor parentCursor = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(parentCursor, nextStepIdx);
+ }
+ if (cursorValue instanceof Xml.Tag) {
+ Xml.Tag tag = (Xml.Tag) cursorValue;
+ if (tag.getValue().isPresent()) {
+ // Tag has text content - continue from SAME position
+ return matchRemainingStepsBottomUp(cursor, nextStepIdx);
}
- String newExpression = String.format(
- // the // here allows to skip several levels of nested elements
- "%s/%s//%s",
- expression.substring(0, doubleSlashIndex),
- blankPartTag.getName(),
- expression.substring(doubleSlashIndex + 2)
- );
- return new XPathMatcher(newExpression).matches(cursor);
- } else if (path.size() == tagMatchingParts) {
- return matchesWithoutDoubleSlashesAt(cursor, doubleSlashIndex);
}
- }
+ return false;
+
+ case COMMENT:
+ if (cursorValue instanceof Xml.Comment) {
+ Cursor parentCursor = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(parentCursor, nextStepIdx);
+ }
+ return false;
+
+ case NODE:
+ // node() matches any node
+ Cursor parentCursor = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(parentCursor, nextStepIdx);
+
+ case PROCESSING_INSTRUCTION:
+ if (cursorValue instanceof Xml.ProcessingInstruction) {
+ Cursor parentCursorPi = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(parentCursorPi, nextStepIdx);
+ }
+ return false;
- if (tagMatchingParts > path.size()) {
+ default:
return false;
+ }
+ }
+
+ /**
+ * Recursively match remaining steps going up the cursor chain.
+ *
+ * @param cursor Current position in the document (may be null if we've reached root)
+ * @param stepIdx Index of the step to match (going backwards from end)
+ * @return true if all remaining steps match
+ */
+ private boolean matchRemainingStepsBottomUp(@Nullable Cursor cursor, int stepIdx) {
+ // All steps matched - verify root condition
+ if (stepIdx < 0) {
+ if ((compiled.flags & FLAG_ABSOLUTE_PATH) != 0) {
+ // Absolute path (starts with /) - must have reached root (no more tag ancestors)
+ return cursor == null || !(cursor.getValue() instanceof Xml.Tag);
}
+ // Relative path or descendant path (starts with //) - any position is fine
+ return true;
+ }
+
+ CompiledStep step = compiled.steps[stepIdx];
+
+ // Handle special step types that don't consume parent levels normally
+ switch (step.type) {
+ case ABBREVIATED_DOT:
+ // . means "self" - don't move up, just continue matching from same position
+ return matchRemainingStepsBottomUp(cursor, stepIdx - 1);
- for (int i = 0; i < parts.length; i++) {
- String part = parts[i];
+ case ABBREVIATED_DOTDOT:
+ // .. means "parent" - in bottom-up, the step BEFORE .. should be checked
+ // as a child existence test (not a direct match)
+ return matchParentStepInMiddle(cursor, stepIdx - 1);
- int isAttr = part.startsWith("@") ? 1 : 0;
- Xml.Tag tag = i - isAttr < path.size() ? path.get(i - isAttr) : null;
- String partName;
- boolean matchedCondition = false;
+ case AXIS_STEP:
+ return matchAxisStepBottomUp(step, cursor, stepIdx);
- Matcher matcher;
- if (tag != null && part.endsWith("]") && (matcher = ELEMENT_WITH_CONDITION_PATTERN.matcher(part)).matches()) {
- String optionalPartName = matchesElementWithConditionFunction(matcher, tag, cursor);
- if (optionalPartName == null) {
+ case NODE_TYPE_TEST:
+ // Node type test in the middle of a path
+ return matchNodeTypeStepBottomUp(step, cursor, stepIdx);
+
+ default:
+ // Regular element step (NODE_TEST)
+ break;
+ }
+
+ // Check how this step relates to the step we just matched (stepIdx + 1)
+ // The step at stepIdx+1 tells us the relationship via isDescendant
+ CompiledStep nextStep = compiled.steps[stepIdx + 1];
+
+ if (nextStep.isDescendant) {
+ // The step we just matched can be a descendant of current step
+ // So current step can be ANY ancestor - scan upward with backtracking
+ Cursor pos = cursor;
+ while (pos != null && pos.getValue() instanceof Xml.Tag) {
+ Xml.Tag tag = pos.getValue();
+ if (matchStepAgainstTag(step, tag, pos)) {
+ // Found a candidate - try to match remaining prefix
+ Cursor nextParent = getParentTagCursor(pos);
+ if (matchRemainingStepsBottomUp(nextParent, stepIdx - 1)) {
+ return true; // This candidate worked
+ }
+ // Continue scanning for another candidate (backtracking)
+ }
+ pos = getParentTagCursor(pos);
+ }
+ return false; // No valid candidate found
+ } else {
+ // Direct parent relationship - current step must be at cursor exactly
+ if (cursor == null || !(cursor.getValue() instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag tag = cursor.getValue();
+ if (!matchStepAgainstTag(step, tag, cursor)) {
+ return false;
+ }
+ Cursor nextParent = getParentTagCursor(cursor);
+ return matchRemainingStepsBottomUp(nextParent, stepIdx - 1);
+ }
+ }
+
+ /**
+ * Match axis step in bottom-up context.
+ */
+ private boolean matchAxisStepBottomUp(CompiledStep step, @Nullable Cursor cursor, int stepIdx) {
+ switch (step.axisType) {
+ case PARENT:
+ // parent::node() or parent::element - similar to ..
+ // Check node test name first
+ if (cursor != null && cursor.getValue() instanceof Xml.Tag) {
+ Xml.Tag tag = cursor.getValue();
+ if (!matchesElementName(step.name, tag.getName())) {
return false;
}
- partName = optionalPartName;
- matchedCondition = true;
- } else {
- partName = part;
}
+ // The step before parent:: should be verified as a child
+ return matchParentStepInMiddle(cursor, stepIdx - 1);
- if (part.startsWith("@")) {
- if (matchedCondition) {
- return true;
+ case SELF:
+ // self::node() or self::element - matches current position
+ // In bottom-up, we need to check the next step's cursor position
+ // Actually, self:: doesn't consume a level - it's like . with a name test
+ if (cursor != null && cursor.getValue() instanceof Xml.Tag) {
+ Xml.Tag tag = cursor.getValue();
+ if (!matchesElementName(step.name, tag.getName())) {
+ return false;
}
- return cursor.getValue() instanceof Xml.Attribute &&
- (((Xml.Attribute) cursor.getValue()).getKeyAsString().equals(part.substring(1)) ||
- "*".equals(part.substring(1)));
}
+ return matchRemainingStepsBottomUp(cursor, stepIdx - 1);
- if (path.size() < i + 1 || (tag != null && !tag.getName().equals(partName) && !"*".equals(partName) && !"*".equals(part))) {
+ case CHILD:
+ // child:: is the default - same as normal element step
+ if (cursor == null || !(cursor.getValue() instanceof Xml.Tag)) {
return false;
}
+ Xml.Tag tag = cursor.getValue();
+ if (!matchStepAgainstTag(step, tag, cursor)) {
+ return false;
+ }
+ return matchRemainingStepsBottomUp(getParentTagCursor(cursor), stepIdx - 1);
+
+ default:
+ // Unsupported axis
+ return false;
+ }
+ }
+
+ /**
+ * Match node type test step in bottom-up context (not as last step).
+ */
+ private boolean matchNodeTypeStepBottomUp(CompiledStep step, @Nullable Cursor cursor, int stepIdx) {
+ // Node type tests in the middle of a path need special handling
+ switch (step.nodeTypeTestType) {
+ case NODE:
+ // node() matches anything - don't consume extra level
+ return matchRemainingStepsBottomUp(cursor, stepIdx - 1);
+
+ case TEXT:
+ case COMMENT:
+ case PROCESSING_INSTRUCTION:
+ // These typically don't appear in the middle of element paths
+ // Fall through to default handling
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Get the parent cursor that contains a tag, skipping non-tag nodes.
+ */
+ private @Nullable Cursor getParentTagCursor(@Nullable Cursor cursor) {
+ if (cursor == null) return null;
+ Cursor parent = cursor.getParent();
+ while (parent != null && !(parent.getValue() instanceof Xml.Tag)) {
+ if (parent.getValue() instanceof Xml.Document) {
+ return null;
}
+ parent = parent.getParent();
+ }
+ return parent;
+ }
- return cursor.getValue() instanceof Xml.Tag && path.size() == parts.length;
+ /**
+ * Check if a compiled step matches a tag.
+ */
+ private boolean matchStepAgainstTag(CompiledStep step, Xml.Tag tag, Cursor cursor) {
+ // Handle different step types
+ switch (step.type) {
+ case NODE_TEST:
+ // Element name or wildcard
+ if (!matchesName(step.name, tag.getName())) {
+ return false;
+ }
+ break;
+ case ABBREVIATED_DOT:
+ // . matches current - always true for tags
+ break;
+ case ABBREVIATED_DOTDOT:
+ // .. is handled differently - this shouldn't be called directly
+ return false;
+ case NODE_TYPE_TEST:
+ // text(), comment(), node() - these don't match tags
+ if (step.nodeTypeTestType != NodeTypeTestType.NODE) {
+ return false;
+ }
+ break;
+ default:
+ // Other step types not supported in bottom-up yet
+ return false;
+ }
+
+ // Check predicates if present
+ if (step.predicates.length > 0) {
+ return evaluatePredicates(step.predicates, tag, cursor);
}
+
+ return true;
}
- private boolean matchesWithoutDoubleSlashesAt(Cursor cursor, int doubleSlashIndex) {
- String newExpression = String.format(
- "%s/%s",
- expression.substring(0, doubleSlashIndex),
- expression.substring(doubleSlashIndex + 2)
- );
- return new XPathMatcher(newExpression).matches(cursor);
+ /**
+ * Evaluate predicates against a tag.
+ * Uses compiled expressions - no ANTLR tree traversal needed.
+ */
+ private boolean evaluatePredicates(CompiledExpr[] predicates, Xml.Tag tag, Cursor cursor) {
+ // Calculate position/size once by looking at parent's children
+ int position = 1;
+ int size = 1;
+ Cursor parentCursor = cursor.getParent();
+ if (parentCursor != null && parentCursor.getValue() instanceof Xml.Tag) {
+ Xml.Tag parent = parentCursor.getValue();
+ List extends Content> contents = parent.getContent();
+ if (contents != null) {
+ int count = 0;
+ for (Content c : contents) {
+ if (c instanceof Xml.Tag) {
+ Xml.Tag child = (Xml.Tag) c;
+ if (child.getName().equals(tag.getName())) {
+ count++;
+ if (child == tag) {
+ position = count;
+ }
+ }
+ }
+ }
+ size = count > 0 ? count : 1;
+ }
+ }
+
+ for (CompiledExpr predicate : predicates) {
+ if (!evaluateExpr(predicate, tag, null, cursor, position, size)) {
+ return false;
+ }
+ }
+ return true;
}
+ // ==================== Compiled Expression Evaluation ====================
+
/**
- * Checks that the given {@code tag} matches the XPath part represented by {@code matcher}.
+ * Unified expression evaluation for both tag and attribute contexts.
+ * Pass the relevant context (tag or attr), with the other as null.
+ * This is allocation-free - no context objects are created.
*
- * @param matcher an XPath part matcher for {@link #ELEMENT_WITH_CONDITION_PATTERN}
- * @param tag a tag to match
- * @param cursor the cursor we are trying to match
- * @return the element name specified before the condition of the part
- * (either {@code tag.getName()}, {@code "*"} or an attribute name) or {@code null} if the tag did not match
- */
- private @Nullable String matchesElementWithConditionFunction(Matcher matcher, Xml.Tag tag, Cursor cursor) {
- boolean isAttributeElement = matcher.group(1) != null;
- String element = matcher.group(2);
- String allConditions = matcher.group(3);
-
- // Fail quickly if element name doesn't match
- if (!isAttributeElement && !tag.getName().equals(element) && !"*".equals(element)) {
- return null;
+ * @param expr the compiled expression to evaluate
+ * @param tag the tag context (null for attribute-only evaluation)
+ * @param attr the attribute context (null for tag-only evaluation)
+ * @param cursor the cursor position
+ * @param position the 1-based position among siblings
+ * @param size the total number of siblings
+ * @return true if the expression evaluates to true
+ */
+ @SuppressWarnings("DataFlowIssue")
+ private boolean evaluateExpr(CompiledExpr expr, Xml.@Nullable Tag tag, Xml.@Nullable Attribute attr,
+ Cursor cursor, int position, int size) {
+ switch (expr.type) {
+ case NUMERIC:
+ // Positional predicate: [1], [2], etc.
+ return position == expr.numericValue;
+
+ case AND:
+ return evaluateExpr(expr.left, tag, attr, cursor, position, size) &&
+ evaluateExpr(expr.right, tag, attr, cursor, position, size);
+
+ case OR:
+ return evaluateExpr(expr.left, tag, attr, cursor, position, size) ||
+ evaluateExpr(expr.right, tag, attr, cursor, position, size);
+
+ case COMPARISON:
+ return evaluateComparison(expr, tag, attr, cursor, position, size);
+
+ case FUNCTION:
+ return evaluateFunction(expr, tag, attr, cursor, position, size);
+
+ case CHILD:
+ // Existence check: [childName] - tag context only
+ return tag != null && hasChildElement(tag, expr.name);
+
+ case PATH:
+ // Path existence check: [a/b/c] - tag context only
+ return tag != null && pathExists(tag, expr);
+
+ case ATTRIBUTE:
+ // Existence check: [@attr] - tag context only
+ return tag != null && hasAttribute(tag, expr.name);
+
+ case BOOLEAN:
+ return expr.booleanValue;
+
+ default:
+ return false;
}
+ }
- // check that all conditions match on current element
- Matcher conditions = CONDITION_PATTERN.matcher(allConditions);
- boolean stillMatchesConditions = true;
- while (conditions.find() && stillMatchesConditions) {
- String conditionGroup = conditions.group(1);
- Matcher condition = CONDITION_CONJUNCTION_PATTERN.matcher(conditionGroup);
- boolean orCondition = false;
-
- while (condition.find() && (stillMatchesConditions || orCondition)) {
- boolean matchCurrentCondition = false;
-
- boolean isAttributeCondition = condition.group(4) != null;
- String selector = isAttributeCondition ? condition.group(5) : condition.group(2);
- boolean isFunctionCondition = selector.endsWith("()");
- String value = condition.group(6);
- String conjunction = condition.group(8);
- orCondition = "or".equals(conjunction);
-
- // invalid conjunction if not 'or' or 'and'
- if (!orCondition && conjunction != null && !"and".equals(conjunction)) {
- // TODO: throw exception for invalid or unsupported XPath conjunction?
- stillMatchesConditions = false;
- break;
+ /**
+ * Unified comparison evaluation for both tag and attribute contexts.
+ */
+ @SuppressWarnings("DataFlowIssue")
+ private boolean evaluateComparison(CompiledExpr expr, Xml.@Nullable Tag tag, Xml.@Nullable Attribute attr,
+ Cursor cursor, int position, int size) {
+ String leftValue = resolveValue(expr.left, tag, attr, cursor, position, size);
+ String rightValue = resolveValue(expr.right, tag, attr, cursor, position, size);
+
+ if (leftValue == null || rightValue == null) {
+ return false;
+ }
+
+ return compareValues(leftValue, rightValue, expr.op);
+ }
+
+ /**
+ * Unified value resolution for both tag and attribute contexts.
+ */
+ private @Nullable String resolveValue(CompiledExpr expr, Xml.@Nullable Tag tag, Xml.@Nullable Attribute attr,
+ Cursor cursor, int position, int size) {
+ switch (expr.type) {
+ case STRING:
+ return expr.stringValue;
+
+ case NUMERIC:
+ return String.valueOf(expr.numericValue);
+
+ case CHILD:
+ // Tag context only
+ return tag != null ? getChildElementValue(tag, expr.name) : null;
+
+ case ATTRIBUTE:
+ // In tag context, get attribute from tag
+ // In attribute context, get value from current attribute or parent tag
+ if (tag != null) {
+ return getAttributeValue(tag, expr.name);
+ }
+ if (attr != null) {
+ if (expr.name == null || "*".equals(expr.name)) {
+ return attr.getValueAsString();
+ }
+ // For named attributes, check parent tag
+ Cursor parentCursor = cursor.getParent();
+ if (parentCursor != null && parentCursor.getValue() instanceof Xml.Tag) {
+ return getAttributeValue(parentCursor.getValue(), expr.name);
+ }
}
+ return null;
+
+ case PATH:
+ // Tag context only
+ return tag != null ? resolvePathValue(expr, tag) : null;
- if (isAttributeCondition) { // [@attr='value'] pattern
- for (Xml.Attribute a : tag.getAttributes()) {
- if ((a.getKeyAsString().equals(selector) || "*".equals(selector)) && a.getValueAsString().equals(value)) {
- matchCurrentCondition = true;
- break;
+ case ABSOLUTE_PATH:
+ // Resolve path from document root
+ if (expr.stringValue != null) {
+ Xml.Tag root = getRootTag(cursor);
+ if (root != null) {
+ Set pathMatches = findTagsByPath(root, expr.stringValue);
+ if (!pathMatches.isEmpty()) {
+ return pathMatches.iterator().next().getValue().orElse("");
}
}
- } else if (isFunctionCondition) { // [local-name()='name'] or [text()='value'] pattern
- if ("text()".equals(selector)) {
- matchCurrentCondition = tag.getValue().map(v -> v.equals(value)).orElse(false);
- } else if (isAttributeElement) {
- for (Xml.Attribute a : tag.getAttributes()) {
- if (matchesElementAndFunction(new Cursor(cursor, a), element, selector, value)) {
- matchCurrentCondition = true;
- break;
- }
+ }
+ return "";
+
+ case FUNCTION:
+ return resolveFunctionValue(expr, tag, attr, cursor, position, size);
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Unified function evaluation as boolean for both contexts.
+ */
+ private boolean evaluateFunction(CompiledExpr expr, Xml.@Nullable Tag tag, Xml.@Nullable Attribute attr,
+ Cursor cursor, int position, int size) {
+ if (expr.functionType == null) {
+ return false;
+ }
+
+ switch (expr.functionType) {
+ case POSITION:
+ return position > 0;
+
+ case LAST:
+ return position == size;
+
+ case NOT:
+ if (expr.args != null && expr.args.length > 0) {
+ return !evaluateExpr(expr.args[0], tag, attr, cursor, position, size);
+ }
+ return false;
+
+ case CONTAINS:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String substr = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return str != null && substr != null && str.contains(substr);
+ }
+ return false;
+
+ case STARTS_WITH:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String prefix = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return str != null && prefix != null && str.startsWith(prefix);
+ }
+ return false;
+
+ case ENDS_WITH:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String suffix = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return str != null && suffix != null && str.endsWith(suffix);
+ }
+ return false;
+
+ case STRING_LENGTH:
+ if (expr.args != null && expr.args.length > 0) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ return str != null && !str.isEmpty();
+ }
+ return false;
+
+ case COUNT:
+ if (expr.args != null && expr.args.length > 0) {
+ CompiledExpr pathArg = expr.args[0];
+ if (pathArg.type == ExprType.ABSOLUTE_PATH && pathArg.stringValue != null) {
+ Xml.Tag root = getRootTag(cursor);
+ if (root != null) {
+ Set matches = findTagsByPath(root, pathArg.stringValue);
+ return !matches.isEmpty();
}
- } else {
- matchCurrentCondition = matchesElementAndFunction(cursor, element, selector, value);
}
- } else { // other [] conditions
- for (Xml.Tag t : FindTags.find(tag, selector)) {
- if (t.getValue().map(v -> v.equals(value)).orElse(false)) {
- matchCurrentCondition = true;
- break;
+ }
+ return false;
+
+ case TEXT:
+ // text() as existence check - tag context only
+ return tag != null && tag.getValue().isPresent() && !tag.getValue().get().trim().isEmpty();
+
+ case LOCAL_NAME:
+ case NAMESPACE_URI:
+ // These return strings, not booleans - but as existence check they're truthy
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Unified function value resolution for both contexts.
+ */
+ private @Nullable String resolveFunctionValue(CompiledExpr expr, Xml.@Nullable Tag tag, Xml.@Nullable Attribute attr,
+ Cursor cursor, int position, int size) {
+ if (expr.functionType == null) {
+ return null;
+ }
+
+ switch (expr.functionType) {
+ case POSITION:
+ return String.valueOf(position);
+
+ case LAST:
+ return String.valueOf(size);
+
+ case LOCAL_NAME:
+ if (tag != null) {
+ return localName(tag.getName());
+ }
+ if (attr != null) {
+ return localName(attr.getKeyAsString());
+ }
+ return null;
+
+ case NAMESPACE_URI:
+ if (tag != null) {
+ return resolveNamespaceUri(tag, cursor);
+ }
+ if (attr != null) {
+ return resolveAttributeNamespaceUri(attr, cursor);
+ }
+ return null;
+
+ case TEXT:
+ // Tag context only
+ return tag != null ? tag.getValue().orElse("") : null;
+
+ case STRING_LENGTH:
+ if (expr.args != null && expr.args.length > 0) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ return str != null ? String.valueOf(str.length()) : "0";
+ }
+ return "0";
+
+ case SUBSTRING_BEFORE:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String delim = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ String result = substringBefore(str, delim);
+ return result != null ? result : "";
+ }
+ return "";
+
+ case SUBSTRING_AFTER:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String delim = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ String result = substringAfter(str, delim);
+ return result != null ? result : "";
+ }
+ return "";
+
+ case COUNT:
+ if (expr.args != null && expr.args.length > 0) {
+ CompiledExpr pathArg = expr.args[0];
+ if (pathArg.type == ExprType.ABSOLUTE_PATH && pathArg.stringValue != null) {
+ Xml.Tag root = getRootTag(cursor);
+ if (root != null) {
+ Set matches = findTagsByPath(root, pathArg.stringValue);
+ return String.valueOf(matches.size());
}
}
}
- // break condition early if first OR condition is fulfilled
- if (matchCurrentCondition && orCondition) {
- break;
+ return "0";
+
+ case CONTAINS:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String substr = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return String.valueOf(str != null && substr != null && str.contains(substr));
+ }
+ return "false";
+
+ case STARTS_WITH:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String prefix = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return String.valueOf(str != null && prefix != null && str.startsWith(prefix));
+ }
+ return "false";
+
+ case ENDS_WITH:
+ if (expr.args != null && expr.args.length >= 2) {
+ String str = resolveValue(expr.args[0], tag, attr, cursor, position, size);
+ String suffix = resolveValue(expr.args[1], tag, attr, cursor, position, size);
+ return String.valueOf(str != null && suffix != null && str.endsWith(suffix));
+ }
+ return "false";
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Check if a path exists starting from a tag.
+ * Used for predicates like [a/b/c] which check existence of a descendant path.
+ */
+ private boolean pathExists(Xml.Tag tag, CompiledExpr pathExpr) {
+ if (pathExpr.args == null || pathExpr.args.length == 0) {
+ return true; // Empty path always exists
+ }
+
+ // Navigate through each step in the path
+ Xml.Tag current = tag;
+ for (CompiledExpr step : pathExpr.args) {
+ if (step.type != ExprType.CHILD) {
+ return false; // Only CHILD steps supported in path existence check
+ }
+ String childName = step.name;
+ Xml.Tag child = findChildTag(current, childName);
+ if (child == null) {
+ return false;
+ }
+ current = child;
+ }
+ return true;
+ }
+
+ /**
+ * Find a child tag with the given name.
+ */
+ private Xml.@Nullable Tag findChildTag(Xml.Tag parent, @Nullable String name) {
+ List extends Content> contents = parent.getContent();
+ if (contents == null) {
+ return null;
+ }
+ for (Content c : contents) {
+ if (c instanceof Xml.Tag) {
+ Xml.Tag child = (Xml.Tag) c;
+ if (name == null || "*".equals(name) || child.getName().equals(name)) {
+ return child;
}
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resolve a PATH expression value by navigating child elements.
+ */
+ private @Nullable String resolvePathValue(CompiledExpr expr, Xml.Tag tag) {
+ if (expr.args == null || expr.args.length == 0) {
+ // No path steps - get text of current element
+ if (expr.functionType == FunctionType.TEXT) {
+ return tag.getValue().orElse("");
+ }
+ return null;
+ }
- stillMatchesConditions = matchCurrentCondition;
+ // Navigate through child elements following the path
+ Xml.Tag current = tag;
+ for (int i = 0; i < expr.args.length; i++) {
+ CompiledExpr step = expr.args[i];
+ if (step.type != ExprType.CHILD) {
+ return null;
+ }
+ Xml.Tag child = findChildTag(current, step.name);
+ if (child == null) {
+ return null;
}
+ current = child;
}
- return stillMatchesConditions ? element : null;
+ // Apply terminal function if present
+ if (expr.functionType == FunctionType.TEXT) {
+ return current.getValue().orElse("");
+ }
+
+ // Default: return text value of target element
+ return current.getValue().orElse("");
}
- private static boolean matchesElementAndFunction(Cursor cursor, String element, String selector, String value) {
- Namespaced namespaced = new Namespaced(cursor);
- if (!"*".equals(element) && !Objects.equals(namespaced.getName().orElse(null), element)) {
- return false;
- } else if ("local-name()".equals(selector)) {
- return Objects.equals(namespaced.getLocalName().orElse(null), value);
- } else if ("namespace-uri()".equals(selector)) {
- Optional nsUri = namespaced.getNamespaceUri();
- return nsUri.isPresent() && nsUri.get().equals(value);
+ /**
+ * Compare two values using the given comparison operator.
+ * Package-private so XPathMatcherVisitor can share this logic.
+ */
+ static boolean compareValues(String left, String right, @Nullable ComparisonOp op) {
+ if (op == null) {
+ return left.equals(right);
+ }
+
+ switch (op) {
+ case EQ:
+ return left.equals(right);
+ case NE:
+ return !left.equals(right);
+ case LT:
+ case LE:
+ case GT:
+ case GE:
+ // Try numeric comparison
+ try {
+ double leftNum = Double.parseDouble(left);
+ double rightNum = Double.parseDouble(right);
+ switch (op) {
+ case LT:
+ return leftNum < rightNum;
+ case LE:
+ return leftNum <= rightNum;
+ case GT:
+ return leftNum > rightNum;
+ case GE:
+ return leftNum >= rightNum;
+ default:
+ return false;
+ }
+ } catch (NumberFormatException e) {
+ // Fall back to string comparison
+ int cmp = left.compareTo(right);
+ switch (op) {
+ case LT:
+ return cmp < 0;
+ case LE:
+ return cmp <= 0;
+ case GT:
+ return cmp > 0;
+ case GE:
+ return cmp >= 0;
+ default:
+ return false;
+ }
+ }
+ default:
+ return false;
+ }
+ }
+
+ // ==================== Static String Function Helpers ====================
+
+ private static @Nullable String substringBefore(@Nullable String str, @Nullable String delim) {
+ if (str == null || delim == null) {
+ return null;
+ }
+ int idx = str.indexOf(delim);
+ return idx >= 0 ? str.substring(0, idx) : "";
+ }
+
+ private static @Nullable String substringAfter(@Nullable String str, @Nullable String delim) {
+ if (str == null || delim == null) {
+ return null;
+ }
+ int idx = str.indexOf(delim);
+ return idx >= 0 ? str.substring(idx + delim.length()) : "";
+ }
+
+ private static String localName(String name) {
+ int colonIdx = name.indexOf(':');
+ return colonIdx >= 0 ? name.substring(colonIdx + 1) : name;
+ }
+
+ // ==================== Instance Helper Methods ====================
+
+ /**
+ * Check if tag has a child element with the given name.
+ */
+ private boolean hasChildElement(Xml.Tag tag, @Nullable String name) {
+ return findChildTag(tag, name) != null;
+ }
+
+ /**
+ * Get the text value of a child element.
+ */
+ private @Nullable String getChildElementValue(Xml.Tag tag, @Nullable String name) {
+ Xml.Tag child = findChildTag(tag, name);
+ return child != null ? child.getValue().orElse("") : null;
+ }
+
+ /**
+ * Check if tag has an attribute with the given name.
+ */
+ private boolean hasAttribute(Xml.Tag tag, @Nullable String name) {
+ List attrs = tag.getAttributes();
+ for (Xml.Attribute attr : attrs) {
+ if (name == null || "*".equals(name) || attr.getKeyAsString().equals(name)) {
+ return true;
+ }
}
return false;
}
+
+ /**
+ * Get the value of an attribute.
+ */
+ private @Nullable String getAttributeValue(Xml.Tag tag, @Nullable String name) {
+ List attrs = tag.getAttributes();
+ for (Xml.Attribute attr : attrs) {
+ if (name == null || "*".equals(name) || attr.getKeyAsString().equals(name)) {
+ return attr.getValueAsString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Resolve namespace URI for a tag.
+ */
+ private @Nullable String resolveNamespaceUri(Xml.Tag tag, Cursor cursor) {
+ String tagName = tag.getName();
+ String prefix = "";
+ int colonIdx = tagName.indexOf(':');
+ if (colonIdx >= 0) {
+ prefix = tagName.substring(0, colonIdx);
+ }
+ return findNamespaceUri(prefix, cursor);
+ }
+
+ /**
+ * Resolve namespace URI for an attribute.
+ */
+ private @Nullable String resolveAttributeNamespaceUri(Xml.Attribute attr, Cursor cursor) {
+ String attrName = attr.getKeyAsString();
+ int colonIdx = attrName.indexOf(':');
+ if (colonIdx >= 0) {
+ String prefix = attrName.substring(0, colonIdx);
+ return findNamespaceUri(prefix, cursor);
+ }
+ return "";
+ }
+
+ /**
+ * Find namespace URI for a prefix by walking up the cursor.
+ */
+ private @Nullable String findNamespaceUri(String prefix, Cursor cursor) {
+ String nsAttr = prefix.isEmpty() ? "xmlns" : "xmlns:" + prefix;
+ for (Cursor c = cursor; c != null; c = c.getParent()) {
+ if (c.getValue() instanceof Xml.Tag) {
+ Xml.Tag t = c.getValue();
+ for (Xml.Attribute attr : t.getAttributes()) {
+ if (attr.getKeyAsString().equals(nsAttr)) {
+ return attr.getValueAsString();
+ }
+ }
+ }
+ }
+ return prefix.isEmpty() ? "" : null;
+ }
+
+ /**
+ * Handle non-path expressions (boolean expressions, filter expressions).
+ * Path expressions are handled by matchBottomUp.
+ */
+ private boolean matchTopDown(Cursor cursor) {
+ switch (compiled.exprType) {
+ case XPathCompiler.EXPR_BOOLEAN:
+ return matchBooleanExpr(cursor);
+ case XPathCompiler.EXPR_FILTER:
+ return matchFilterExpr(cursor);
+ default:
+ // Path expressions should go through matchBottomUp
+ return false;
+ }
+ }
+
+ // ==================== Boolean Expression Matching ====================
+
+ /**
+ * Match a boolean expression like contains(/root/element, 'value').
+ *
+ * For expressions with pure absolute paths (/foo/bar): cursor must be at the root element.
+ *
+ * For expressions with descendant paths (//foo) or relative paths (foo/bar):
+ * evaluated from cursor context, matches at any position where the expression is true.
+ */
+ @SuppressWarnings("DataFlowIssue")
+ private boolean matchBooleanExpr(Cursor cursor) {
+ if (compiled.booleanExpr == null) {
+ return false;
+ }
+
+ // Cursor must be at a tag
+ if (!(cursor.getValue() instanceof Xml.Tag)) {
+ return false;
+ }
+ Xml.Tag currentTag = cursor.getValue();
+
+ // Check if expression contains relative paths or descendant paths
+ // Relative paths (foo/bar) and descendant paths (//foo) can match at any cursor position
+ // Only pure absolute paths (/foo/bar) require cursor to be at root
+ if (compiled.booleanExpr.hasRelativePath() || !compiled.booleanExpr.hasPureAbsolutePath()) {
+ // Relative or descendant paths: evaluate from cursor context at any position
+ return evaluateExpr(compiled.booleanExpr, currentTag, null, cursor, 1, 1);
+ }
+
+ // Pure absolute paths only: only match at root element
+ // Check if cursor is at root (parent is Document or no parent tag)
+ Cursor parentCursor = cursor.getParent();
+ while (parentCursor != null && !(parentCursor.getValue() instanceof Xml.Tag) &&
+ !(parentCursor.getValue() instanceof Xml.Document)) {
+ parentCursor = parentCursor.getParent();
+ }
+ if (parentCursor == null || !(parentCursor.getValue() instanceof Xml.Document)) {
+ return false; // Not at root element
+ }
+
+ // Evaluate the expression from current (root) context
+ return evaluateExpr(compiled.booleanExpr, currentTag, null, cursor, 1, 1);
+ }
+
+ // ==================== Filter Expression Matching ====================
+
+ /**
+ * Match a filter expression like (/root/a)[1] or (/root/a)[last()]/child.
+ */
+ @SuppressWarnings("DataFlowIssue")
+ private boolean matchFilterExpr(Cursor cursor) {
+ if (compiled.filterExpr == null) {
+ return false;
+ }
+
+ Xml.Tag root = getRootTag(cursor);
+ if (root == null) {
+ return false;
+ }
+
+ // Find all matching nodes
+ Set allMatches = findTagsByPath(root, compiled.filterExpr.pathExpr);
+ if (allMatches.isEmpty()) {
+ return false;
+ }
+
+ // Convert to list for positional access
+ List matchList = new ArrayList<>(allMatches);
+ int size = matchList.size();
+
+ // Apply predicates to filter the result set
+ List filteredMatches = new ArrayList<>();
+ for (int i = 0; i < matchList.size(); i++) {
+ Xml.Tag tag = matchList.get(i);
+ int position = i + 1; // 1-based
+ boolean allPredicatesMatch = true;
+ for (CompiledExpr predicate : compiled.filterExpr.predicates) {
+ if (!evaluateFilterPredicate(predicate, tag, position, size)) {
+ allPredicatesMatch = false;
+ break;
+ }
+ }
+ if (allPredicatesMatch) {
+ filteredMatches.add(tag);
+ }
+ }
+
+ if (filteredMatches.isEmpty()) {
+ return false;
+ }
+
+ // Get current tag from cursor
+ Xml.Tag currentTag = null;
+ if (cursor.getValue() instanceof Xml.Tag) {
+ currentTag = cursor.getValue();
+ }
+ if (currentTag == null) {
+ return false;
+ }
+
+ // Check if there's a trailing path after the predicates
+ if (compiled.filterExpr.trailingPath != null) {
+ Set trailingMatches = new LinkedHashSet<>();
+ for (Xml.Tag filteredTag : filteredMatches) {
+ if (compiled.filterExpr.trailingIsDescendant) {
+ findDescendants(filteredTag, compiled.filterExpr.trailingPath, trailingMatches);
+ } else {
+ trailingMatches.addAll(findDirectChildren(filteredTag, compiled.filterExpr.trailingPath));
+ }
+ }
+ return trailingMatches.contains(currentTag);
+ } else {
+ return filteredMatches.contains(currentTag);
+ }
+ }
+
+ /**
+ * Evaluate a predicate for filter expressions.
+ */
+ @SuppressWarnings("DataFlowIssue")
+ private boolean evaluateFilterPredicate(CompiledExpr expr, Xml.Tag tag, int position, int size) {
+ switch (expr.type) {
+ case NUMERIC:
+ return position == expr.numericValue;
+
+ case FUNCTION:
+ if (expr.functionType == FunctionType.LAST) {
+ return position == size;
+ }
+ if (expr.functionType == FunctionType.POSITION) {
+ return position > 0;
+ }
+ return false;
+
+ case COMPARISON:
+ // For filter predicates, comparisons often involve position()/last()
+ String leftValue = resolveFilterValue(expr.left, tag, position, size);
+ String rightValue = resolveFilterValue(expr.right, tag, position, size);
+ if (leftValue == null || rightValue == null) {
+ return false;
+ }
+ return compareValues(leftValue, rightValue, expr.op);
+
+ default:
+ return true; // Be permissive for unsupported expressions
+ }
+ }
+
+ /**
+ * Resolve a value for filter predicate evaluation.
+ */
+ @SuppressWarnings("ConstantValue")
+ private @Nullable String resolveFilterValue(CompiledExpr expr, Xml.Tag tag, int position, int size) {
+ if (expr == null) return null;
+
+ switch (expr.type) {
+ case STRING:
+ return expr.stringValue;
+ case NUMERIC:
+ return String.valueOf(expr.numericValue);
+ case FUNCTION:
+ if (expr.functionType == FunctionType.POSITION) {
+ return String.valueOf(position);
+ }
+ if (expr.functionType == FunctionType.LAST) {
+ return String.valueOf(size);
+ }
+ if (expr.functionType == FunctionType.LOCAL_NAME) {
+ return localName(tag.getName());
+ }
+ return null;
+ default:
+ return null;
+ }
+ }
+
+ // ==================== Tree Traversal Helpers ====================
+
+ /**
+ * Get the cursor at the root tag by walking up to the document.
+ */
+ private @Nullable Cursor getRootCursor(Cursor cursor) {
+ Cursor c = cursor;
+ while (c.getParent() != null && !(c.getParent().getValue() instanceof Xml.Document)) {
+ c = c.getParent();
+ }
+ return c.getValue() instanceof Xml.Tag ? c : null;
+ }
+
+ /**
+ * Get the root tag from a cursor by walking up to the document.
+ */
+ private Xml.@Nullable Tag getRootTag(Cursor cursor) {
+ Cursor rootCursor = getRootCursor(cursor);
+ return rootCursor != null ? (Xml.Tag) rootCursor.getValue() : null;
+ }
+
+ /**
+ * Find tags matching a path expression starting from a root tag.
+ * Supports absolute paths (/a/b), descendant paths (//a), and relative paths (a/b).
+ */
+ private Set findTagsByPath(Xml.Tag startTag, String pathExpr) {
+ Set result = new LinkedHashSet<>();
+
+ // Handle descendant-or-self axis (//)
+ if (pathExpr.startsWith("//")) {
+ String elementName = pathExpr.substring(2);
+ if (elementName.contains("/")) {
+ elementName = elementName.substring(0, elementName.indexOf('/'));
+ }
+ findDescendants(startTag, elementName, result);
+ return result;
+ }
+
+ // Handle absolute path
+ boolean isAbsolute = pathExpr.startsWith("/");
+ if (isAbsolute) {
+ pathExpr = pathExpr.substring(1);
+ }
+
+ String[] steps = pathExpr.split("/");
+ if (steps.length == 0) {
+ return result;
+ }
+
+ Set currentMatches = new LinkedHashSet<>();
+
+ if (isAbsolute) {
+ // First step matches the root element itself
+ String firstStep = steps[0];
+ if ("*".equals(firstStep) || startTag.getName().equals(firstStep)) {
+ if (steps.length == 1) {
+ currentMatches.add(startTag);
+ } else {
+ currentMatches = findDirectChildren(startTag, steps[1]);
+ for (int i = 2; i < steps.length; i++) {
+ Set nextMatches = new LinkedHashSet<>();
+ for (Xml.Tag match : currentMatches) {
+ nextMatches.addAll(findDirectChildren(match, steps[i]));
+ }
+ currentMatches = nextMatches;
+ }
+ }
+ }
+ } else {
+ // Relative path - start with children
+ currentMatches = findDirectChildren(startTag, steps[0]);
+ for (int i = 1; i < steps.length; i++) {
+ Set nextMatches = new LinkedHashSet<>();
+ for (Xml.Tag match : currentMatches) {
+ nextMatches.addAll(findDirectChildren(match, steps[i]));
+ }
+ currentMatches = nextMatches;
+ }
+ }
+
+ return currentMatches;
+ }
+
+ /**
+ * Find all descendant tags matching the given element name.
+ */
+ private void findDescendants(Xml.Tag tag, String elementName, Set result) {
+ if ("*".equals(elementName) || tag.getName().equals(elementName)) {
+ result.add(tag);
+ }
+ List extends Content> contents = tag.getContent();
+ if (contents != null) {
+ for (Content content : contents) {
+ if (content instanceof Xml.Tag) {
+ findDescendants((Xml.Tag) content, elementName, result);
+ }
+ }
+ }
+ }
+
+ /**
+ * Find direct children matching the given element name or wildcard.
+ */
+ private Set findDirectChildren(Xml.Tag parent, String elementName) {
+ Set result = new LinkedHashSet<>();
+ List extends Content> contents = parent.getContent();
+ if (contents == null) {
+ return result;
+ }
+ for (Content content : contents) {
+ if (content instanceof Xml.Tag) {
+ Xml.Tag child = (Xml.Tag) content;
+ if ("*".equals(elementName) || child.getName().equals(elementName)) {
+ result.add(child);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Check if a name pattern matches an actual element name in an axis context.
+ * Handles wildcards (*), node() tests, and null patterns.
+ * Use this for element names where node() is a valid node type test.
+ *
+ * @param pattern the XPath name pattern (may be null, "*", "node", or a specific name)
+ * @param actualName the actual element name to match against
+ * @return true if the pattern matches the actual name
+ */
+ private static boolean matchesElementName(@Nullable String pattern, String actualName) {
+ return pattern == null || "*".equals(pattern) || "node".equals(pattern) || actualName.equals(pattern);
+ }
+
+ /**
+ * Check if a name pattern matches an actual name (without node() test).
+ * Handles wildcards (*) and null patterns.
+ * Use this for attribute names or simple element name tests.
+ *
+ * @param pattern the XPath name pattern (may be null, "*", or a specific name)
+ * @param actualName the actual name to match against
+ * @return true if the pattern matches the actual name
+ */
+ private static boolean matchesName(@Nullable String pattern, String actualName) {
+ return pattern == null || "*".equals(pattern) || actualName.equals(pattern);
+ }
}
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLLexer.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLLexer.java
index b7047021f4..24e95132b0 100644
--- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLLexer.java
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLLexer.java
@@ -15,12 +15,14 @@
*/
// Generated from ~/git/rewrite/rewrite-xml/src/main/antlr/XMLLexer.g4 by ANTLR 4.13.2
package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.Lexer;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.*;
-import org.antlr.v4.runtime.atn.ATN;
-import org.antlr.v4.runtime.atn.ATNDeserializer;
-import org.antlr.v4.runtime.atn.LexerATNSimulator;
-import org.antlr.v4.runtime.atn.PredictionContextCache;
+import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
+import org.antlr.v4.runtime.misc.*;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
public class XMLLexer extends Lexer {
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLParser.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLParser.java
index 9ffcc3360f..ec71d61207 100644
--- a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLParser.java
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XMLParser.java
@@ -15,16 +15,14 @@
*/
// Generated from ~/git/rewrite/rewrite-xml/src/main/antlr/XMLParser.g4 by ANTLR 4.13.2
package org.openrewrite.xml.internal.grammar;
-import org.antlr.v4.runtime.atn.ATN;
-import org.antlr.v4.runtime.atn.ATNDeserializer;
-import org.antlr.v4.runtime.atn.ParserATNSimulator;
-import org.antlr.v4.runtime.atn.PredictionContextCache;
+import org.antlr.v4.runtime.atn.*;
import org.antlr.v4.runtime.dfa.DFA;
import org.antlr.v4.runtime.*;
-import org.antlr.v4.runtime.tree.ParseTreeListener;
-import org.antlr.v4.runtime.tree.ParseTreeVisitor;
-import org.antlr.v4.runtime.tree.TerminalNode;
+import org.antlr.v4.runtime.misc.*;
+import org.antlr.v4.runtime.tree.*;
import java.util.List;
+import java.util.Iterator;
+import java.util.ArrayList;
@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
public class XMLParser extends Parser {
@@ -61,22 +59,22 @@ private static String[] makeRuleNames() {
private static String[] makeLiteralNames() {
return new String[] {
- null, null, null, null, null, null, null, null, null, "'?'", null, "'<'",
- null, null, null, null, null, null, null, null, null, null, null, null,
- null, null, null, null, null, null, null, null, null, null, "'/>'", null,
+ null, null, null, null, null, null, null, null, null, "'?'", null, "'<'",
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, "'/>'", null,
"'%@'", "'%'", "'/'", "'='"
};
}
private static final String[] _LITERAL_NAMES = makeLiteralNames();
private static String[] makeSymbolicNames() {
return new String[] {
- null, "WS", "COMMENT", "CDATA", "ParamEntityRef", "EntityRef", "CharRef",
- "SEA_WS", "UTF_ENCODING_BOM", "QUESTION_MARK", "SPECIAL_OPEN_XML", "OPEN",
- "SPECIAL_OPEN", "DTD_OPEN", "JSP_COMMENT", "JSP_DECLARATION", "JSP_EXPRESSION",
- "JSP_SCRIPTLET", "TEXT", "DTD_CLOSE", "DTD_SUBSET_OPEN", "DTD_S", "DOCTYPE",
- "DTD_SUBSET_CLOSE", "MARKUP_OPEN", "DTS_SUBSET_S", "MARK_UP_CLOSE", "MARKUP_S",
- "MARKUP_TEXT", "MARKUP_SUBSET", "PI_S", "PI_TEXT", "CLOSE", "SPECIAL_CLOSE",
- "SLASH_CLOSE", "S", "DIRECTIVE_OPEN", "DIRECTIVE_CLOSE", "SLASH", "EQUALS",
+ null, "WS", "COMMENT", "CDATA", "ParamEntityRef", "EntityRef", "CharRef",
+ "SEA_WS", "UTF_ENCODING_BOM", "QUESTION_MARK", "SPECIAL_OPEN_XML", "OPEN",
+ "SPECIAL_OPEN", "DTD_OPEN", "JSP_COMMENT", "JSP_DECLARATION", "JSP_EXPRESSION",
+ "JSP_SCRIPTLET", "TEXT", "DTD_CLOSE", "DTD_SUBSET_OPEN", "DTD_S", "DOCTYPE",
+ "DTD_SUBSET_CLOSE", "MARKUP_OPEN", "DTS_SUBSET_S", "MARK_UP_CLOSE", "MARKUP_S",
+ "MARKUP_TEXT", "MARKUP_SUBSET", "PI_S", "PI_TEXT", "CLOSE", "SPECIAL_CLOSE",
+ "SLASH_CLOSE", "S", "DIRECTIVE_OPEN", "DIRECTIVE_CLOSE", "SLASH", "EQUALS",
"STRING", "Name"
};
}
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.interp b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.interp
new file mode 100644
index 0000000000..03b70a6e56
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.interp
@@ -0,0 +1,101 @@
+token literal names:
+null
+null
+'/'
+'//'
+'::'
+'['
+']'
+'('
+')'
+'@'
+'..'
+'.'
+','
+'='
+'!='
+'<='
+'>='
+'<'
+'>'
+'*'
+null
+'and'
+'or'
+'local-name'
+'namespace-uri'
+null
+null
+null
+
+token symbolic names:
+null
+WS
+SLASH
+DOUBLE_SLASH
+AXIS_SEP
+LBRACKET
+RBRACKET
+LPAREN
+RPAREN
+AT
+DOTDOT
+DOT
+COMMA
+EQUALS
+NOT_EQUALS
+LTE
+GTE
+LT
+GT
+WILDCARD
+NUMBER
+AND
+OR
+LOCAL_NAME
+NAMESPACE_URI
+STRING_LITERAL
+QNAME
+NCNAME
+
+rule names:
+WS
+SLASH
+DOUBLE_SLASH
+AXIS_SEP
+LBRACKET
+RBRACKET
+LPAREN
+RPAREN
+AT
+DOTDOT
+DOT
+COMMA
+EQUALS
+NOT_EQUALS
+LTE
+GTE
+LT
+GT
+WILDCARD
+NUMBER
+AND
+OR
+LOCAL_NAME
+NAMESPACE_URI
+STRING_LITERAL
+QNAME
+NCNAME
+NCNAME_CHARS
+NAME_START_CHAR
+NAME_CHAR
+
+channel names:
+DEFAULT_TOKEN_CHANNEL
+HIDDEN
+
+mode names:
+DEFAULT_MODE
+
+atn:
+[4, 0, 27, 193, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 1, 0, 4, 0, 63, 8, 0, 11, 0, 12, 0, 64, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 18, 1, 18, 1, 19, 4, 19, 112, 8, 19, 11, 19, 12, 19, 113, 1, 19, 1, 19, 4, 19, 118, 8, 19, 11, 19, 12, 19, 119, 3, 19, 122, 8, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 5, 24, 158, 8, 24, 10, 24, 12, 24, 161, 9, 24, 1, 24, 1, 24, 1, 24, 5, 24, 166, 8, 24, 10, 24, 12, 24, 169, 9, 24, 1, 24, 3, 24, 172, 8, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 27, 1, 27, 5, 27, 182, 8, 27, 10, 27, 12, 27, 185, 9, 27, 1, 28, 3, 28, 188, 8, 28, 1, 29, 1, 29, 3, 29, 192, 8, 29, 0, 0, 30, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 0, 57, 0, 59, 0, 1, 0, 6, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 1, 0, 39, 39, 1, 0, 34, 34, 14, 0, 65, 90, 95, 95, 97, 122, 192, 214, 216, 246, 248, 767, 880, 893, 895, 8191, 8204, 8205, 8304, 8591, 11264, 12271, 12289, 55295, 63744, 64975, 65008, 65533, 5, 0, 45, 46, 48, 57, 183, 183, 768, 879, 8255, 8256, 198, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 1, 62, 1, 0, 0, 0, 3, 68, 1, 0, 0, 0, 5, 70, 1, 0, 0, 0, 7, 73, 1, 0, 0, 0, 9, 76, 1, 0, 0, 0, 11, 78, 1, 0, 0, 0, 13, 80, 1, 0, 0, 0, 15, 82, 1, 0, 0, 0, 17, 84, 1, 0, 0, 0, 19, 86, 1, 0, 0, 0, 21, 89, 1, 0, 0, 0, 23, 91, 1, 0, 0, 0, 25, 93, 1, 0, 0, 0, 27, 95, 1, 0, 0, 0, 29, 98, 1, 0, 0, 0, 31, 101, 1, 0, 0, 0, 33, 104, 1, 0, 0, 0, 35, 106, 1, 0, 0, 0, 37, 108, 1, 0, 0, 0, 39, 111, 1, 0, 0, 0, 41, 123, 1, 0, 0, 0, 43, 127, 1, 0, 0, 0, 45, 130, 1, 0, 0, 0, 47, 141, 1, 0, 0, 0, 49, 171, 1, 0, 0, 0, 51, 173, 1, 0, 0, 0, 53, 177, 1, 0, 0, 0, 55, 179, 1, 0, 0, 0, 57, 187, 1, 0, 0, 0, 59, 191, 1, 0, 0, 0, 61, 63, 7, 0, 0, 0, 62, 61, 1, 0, 0, 0, 63, 64, 1, 0, 0, 0, 64, 62, 1, 0, 0, 0, 64, 65, 1, 0, 0, 0, 65, 66, 1, 0, 0, 0, 66, 67, 6, 0, 0, 0, 67, 2, 1, 0, 0, 0, 68, 69, 5, 47, 0, 0, 69, 4, 1, 0, 0, 0, 70, 71, 5, 47, 0, 0, 71, 72, 5, 47, 0, 0, 72, 6, 1, 0, 0, 0, 73, 74, 5, 58, 0, 0, 74, 75, 5, 58, 0, 0, 75, 8, 1, 0, 0, 0, 76, 77, 5, 91, 0, 0, 77, 10, 1, 0, 0, 0, 78, 79, 5, 93, 0, 0, 79, 12, 1, 0, 0, 0, 80, 81, 5, 40, 0, 0, 81, 14, 1, 0, 0, 0, 82, 83, 5, 41, 0, 0, 83, 16, 1, 0, 0, 0, 84, 85, 5, 64, 0, 0, 85, 18, 1, 0, 0, 0, 86, 87, 5, 46, 0, 0, 87, 88, 5, 46, 0, 0, 88, 20, 1, 0, 0, 0, 89, 90, 5, 46, 0, 0, 90, 22, 1, 0, 0, 0, 91, 92, 5, 44, 0, 0, 92, 24, 1, 0, 0, 0, 93, 94, 5, 61, 0, 0, 94, 26, 1, 0, 0, 0, 95, 96, 5, 33, 0, 0, 96, 97, 5, 61, 0, 0, 97, 28, 1, 0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 100, 5, 61, 0, 0, 100, 30, 1, 0, 0, 0, 101, 102, 5, 62, 0, 0, 102, 103, 5, 61, 0, 0, 103, 32, 1, 0, 0, 0, 104, 105, 5, 60, 0, 0, 105, 34, 1, 0, 0, 0, 106, 107, 5, 62, 0, 0, 107, 36, 1, 0, 0, 0, 108, 109, 5, 42, 0, 0, 109, 38, 1, 0, 0, 0, 110, 112, 7, 1, 0, 0, 111, 110, 1, 0, 0, 0, 112, 113, 1, 0, 0, 0, 113, 111, 1, 0, 0, 0, 113, 114, 1, 0, 0, 0, 114, 121, 1, 0, 0, 0, 115, 117, 5, 46, 0, 0, 116, 118, 7, 1, 0, 0, 117, 116, 1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 119, 120, 1, 0, 0, 0, 120, 122, 1, 0, 0, 0, 121, 115, 1, 0, 0, 0, 121, 122, 1, 0, 0, 0, 122, 40, 1, 0, 0, 0, 123, 124, 5, 97, 0, 0, 124, 125, 5, 110, 0, 0, 125, 126, 5, 100, 0, 0, 126, 42, 1, 0, 0, 0, 127, 128, 5, 111, 0, 0, 128, 129, 5, 114, 0, 0, 129, 44, 1, 0, 0, 0, 130, 131, 5, 108, 0, 0, 131, 132, 5, 111, 0, 0, 132, 133, 5, 99, 0, 0, 133, 134, 5, 97, 0, 0, 134, 135, 5, 108, 0, 0, 135, 136, 5, 45, 0, 0, 136, 137, 5, 110, 0, 0, 137, 138, 5, 97, 0, 0, 138, 139, 5, 109, 0, 0, 139, 140, 5, 101, 0, 0, 140, 46, 1, 0, 0, 0, 141, 142, 5, 110, 0, 0, 142, 143, 5, 97, 0, 0, 143, 144, 5, 109, 0, 0, 144, 145, 5, 101, 0, 0, 145, 146, 5, 115, 0, 0, 146, 147, 5, 112, 0, 0, 147, 148, 5, 97, 0, 0, 148, 149, 5, 99, 0, 0, 149, 150, 5, 101, 0, 0, 150, 151, 5, 45, 0, 0, 151, 152, 5, 117, 0, 0, 152, 153, 5, 114, 0, 0, 153, 154, 5, 105, 0, 0, 154, 48, 1, 0, 0, 0, 155, 159, 5, 39, 0, 0, 156, 158, 8, 2, 0, 0, 157, 156, 1, 0, 0, 0, 158, 161, 1, 0, 0, 0, 159, 157, 1, 0, 0, 0, 159, 160, 1, 0, 0, 0, 160, 162, 1, 0, 0, 0, 161, 159, 1, 0, 0, 0, 162, 172, 5, 39, 0, 0, 163, 167, 5, 34, 0, 0, 164, 166, 8, 3, 0, 0, 165, 164, 1, 0, 0, 0, 166, 169, 1, 0, 0, 0, 167, 165, 1, 0, 0, 0, 167, 168, 1, 0, 0, 0, 168, 170, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 170, 172, 5, 34, 0, 0, 171, 155, 1, 0, 0, 0, 171, 163, 1, 0, 0, 0, 172, 50, 1, 0, 0, 0, 173, 174, 3, 55, 27, 0, 174, 175, 5, 58, 0, 0, 175, 176, 3, 55, 27, 0, 176, 52, 1, 0, 0, 0, 177, 178, 3, 55, 27, 0, 178, 54, 1, 0, 0, 0, 179, 183, 3, 57, 28, 0, 180, 182, 3, 59, 29, 0, 181, 180, 1, 0, 0, 0, 182, 185, 1, 0, 0, 0, 183, 181, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 56, 1, 0, 0, 0, 185, 183, 1, 0, 0, 0, 186, 188, 7, 4, 0, 0, 187, 186, 1, 0, 0, 0, 188, 58, 1, 0, 0, 0, 189, 192, 3, 57, 28, 0, 190, 192, 7, 5, 0, 0, 191, 189, 1, 0, 0, 0, 191, 190, 1, 0, 0, 0, 192, 60, 1, 0, 0, 0, 11, 0, 64, 113, 119, 121, 159, 167, 171, 183, 187, 191, 1, 6, 0, 0]
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.java
new file mode 100644
index 0000000000..ac46095e15
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathLexer.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.Lexer;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.TokenStream;
+import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.atn.*;
+import org.antlr.v4.runtime.dfa.DFA;
+import org.antlr.v4.runtime.misc.*;
+
+@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
+public class XPathLexer extends Lexer {
+ static { RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION); }
+
+ protected static final DFA[] _decisionToDFA;
+ protected static final PredictionContextCache _sharedContextCache =
+ new PredictionContextCache();
+ public static final int
+ WS=1, SLASH=2, DOUBLE_SLASH=3, AXIS_SEP=4, LBRACKET=5, RBRACKET=6, LPAREN=7,
+ RPAREN=8, AT=9, DOTDOT=10, DOT=11, COMMA=12, EQUALS=13, NOT_EQUALS=14,
+ LTE=15, GTE=16, LT=17, GT=18, WILDCARD=19, NUMBER=20, AND=21, OR=22, LOCAL_NAME=23,
+ NAMESPACE_URI=24, STRING_LITERAL=25, QNAME=26, NCNAME=27;
+ public static String[] channelNames = {
+ "DEFAULT_TOKEN_CHANNEL", "HIDDEN"
+ };
+
+ public static String[] modeNames = {
+ "DEFAULT_MODE"
+ };
+
+ private static String[] makeRuleNames() {
+ return new String[] {
+ "WS", "SLASH", "DOUBLE_SLASH", "AXIS_SEP", "LBRACKET", "RBRACKET", "LPAREN",
+ "RPAREN", "AT", "DOTDOT", "DOT", "COMMA", "EQUALS", "NOT_EQUALS", "LTE",
+ "GTE", "LT", "GT", "WILDCARD", "NUMBER", "AND", "OR", "LOCAL_NAME", "NAMESPACE_URI",
+ "STRING_LITERAL", "QNAME", "NCNAME", "NCNAME_CHARS", "NAME_START_CHAR",
+ "NAME_CHAR"
+ };
+ }
+ public static final String[] ruleNames = makeRuleNames();
+
+ private static String[] makeLiteralNames() {
+ return new String[] {
+ null, null, "'/'", "'//'", "'::'", "'['", "']'", "'('", "')'", "'@'",
+ "'..'", "'.'", "','", "'='", "'!='", "'<='", "'>='", "'<'", "'>'", "'*'",
+ null, "'and'", "'or'", "'local-name'", "'namespace-uri'"
+ };
+ }
+ private static final String[] _LITERAL_NAMES = makeLiteralNames();
+ private static String[] makeSymbolicNames() {
+ return new String[] {
+ null, "WS", "SLASH", "DOUBLE_SLASH", "AXIS_SEP", "LBRACKET", "RBRACKET",
+ "LPAREN", "RPAREN", "AT", "DOTDOT", "DOT", "COMMA", "EQUALS", "NOT_EQUALS",
+ "LTE", "GTE", "LT", "GT", "WILDCARD", "NUMBER", "AND", "OR", "LOCAL_NAME",
+ "NAMESPACE_URI", "STRING_LITERAL", "QNAME", "NCNAME"
+ };
+ }
+ private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
+ public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
+
+ /**
+ * @deprecated Use {@link #VOCABULARY} instead.
+ */
+ @Deprecated
+ public static final String[] tokenNames;
+ static {
+ tokenNames = new String[_SYMBOLIC_NAMES.length];
+ for (int i = 0; i < tokenNames.length; i++) {
+ tokenNames[i] = VOCABULARY.getLiteralName(i);
+ if (tokenNames[i] == null) {
+ tokenNames[i] = VOCABULARY.getSymbolicName(i);
+ }
+
+ if (tokenNames[i] == null) {
+ tokenNames[i] = "";
+ }
+ }
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTokenNames() {
+ return tokenNames;
+ }
+
+ @Override
+
+ public Vocabulary getVocabulary() {
+ return VOCABULARY;
+ }
+
+
+ public XPathLexer(CharStream input) {
+ super(input);
+ _interp = new LexerATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
+ }
+
+ @Override
+ public String getGrammarFileName() { return "XPathLexer.g4"; }
+
+ @Override
+ public String[] getRuleNames() { return ruleNames; }
+
+ @Override
+ public String getSerializedATN() { return _serializedATN; }
+
+ @Override
+ public String[] getChannelNames() { return channelNames; }
+
+ @Override
+ public String[] getModeNames() { return modeNames; }
+
+ @Override
+ public ATN getATN() { return _ATN; }
+
+ public static final String _serializedATN =
+ "\u0004\u0000\u001b\u00c1\u0006\uffff\uffff\u0002\u0000\u0007\u0000\u0002"+
+ "\u0001\u0007\u0001\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002"+
+ "\u0004\u0007\u0004\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002"+
+ "\u0007\u0007\u0007\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002"+
+ "\u000b\u0007\u000b\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e"+
+ "\u0002\u000f\u0007\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011"+
+ "\u0002\u0012\u0007\u0012\u0002\u0013\u0007\u0013\u0002\u0014\u0007\u0014"+
+ "\u0002\u0015\u0007\u0015\u0002\u0016\u0007\u0016\u0002\u0017\u0007\u0017"+
+ "\u0002\u0018\u0007\u0018\u0002\u0019\u0007\u0019\u0002\u001a\u0007\u001a"+
+ "\u0002\u001b\u0007\u001b\u0002\u001c\u0007\u001c\u0002\u001d\u0007\u001d"+
+ "\u0001\u0000\u0004\u0000?\b\u0000\u000b\u0000\f\u0000@\u0001\u0000\u0001"+
+ "\u0000\u0001\u0001\u0001\u0001\u0001\u0002\u0001\u0002\u0001\u0002\u0001"+
+ "\u0003\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0001\u0005\u0001"+
+ "\u0005\u0001\u0006\u0001\u0006\u0001\u0007\u0001\u0007\u0001\b\u0001\b"+
+ "\u0001\t\u0001\t\u0001\t\u0001\n\u0001\n\u0001\u000b\u0001\u000b\u0001"+
+ "\f\u0001\f\u0001\r\u0001\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000e"+
+ "\u0001\u000f\u0001\u000f\u0001\u000f\u0001\u0010\u0001\u0010\u0001\u0011"+
+ "\u0001\u0011\u0001\u0012\u0001\u0012\u0001\u0013\u0004\u0013p\b\u0013"+
+ "\u000b\u0013\f\u0013q\u0001\u0013\u0001\u0013\u0004\u0013v\b\u0013\u000b"+
+ "\u0013\f\u0013w\u0003\u0013z\b\u0013\u0001\u0014\u0001\u0014\u0001\u0014"+
+ "\u0001\u0014\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0016\u0001\u0016"+
+ "\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0016"+
+ "\u0001\u0016\u0001\u0016\u0001\u0016\u0001\u0017\u0001\u0017\u0001\u0017"+
+ "\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017"+
+ "\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0018"+
+ "\u0001\u0018\u0005\u0018\u009e\b\u0018\n\u0018\f\u0018\u00a1\t\u0018\u0001"+
+ "\u0018\u0001\u0018\u0001\u0018\u0005\u0018\u00a6\b\u0018\n\u0018\f\u0018"+
+ "\u00a9\t\u0018\u0001\u0018\u0003\u0018\u00ac\b\u0018\u0001\u0019\u0001"+
+ "\u0019\u0001\u0019\u0001\u0019\u0001\u001a\u0001\u001a\u0001\u001b\u0001"+
+ "\u001b\u0005\u001b\u00b6\b\u001b\n\u001b\f\u001b\u00b9\t\u001b\u0001\u001c"+
+ "\u0003\u001c\u00bc\b\u001c\u0001\u001d\u0001\u001d\u0003\u001d\u00c0\b"+
+ "\u001d\u0000\u0000\u001e\u0001\u0001\u0003\u0002\u0005\u0003\u0007\u0004"+
+ "\t\u0005\u000b\u0006\r\u0007\u000f\b\u0011\t\u0013\n\u0015\u000b\u0017"+
+ "\f\u0019\r\u001b\u000e\u001d\u000f\u001f\u0010!\u0011#\u0012%\u0013\'"+
+ "\u0014)\u0015+\u0016-\u0017/\u00181\u00193\u001a5\u001b7\u00009\u0000"+
+ ";\u0000\u0001\u0000\u0006\u0003\u0000\t\n\r\r \u0001\u000009\u0001\u0000"+
+ "\'\'\u0001\u0000\"\"\u000e\u0000AZ__az\u00c0\u00d6\u00d8\u00f6\u00f8\u02ff"+
+ "\u0370\u037d\u037f\u1fff\u200c\u200d\u2070\u218f\u2c00\u2fef\u3001\u8000"+
+ "\ud7ff\u8000\uf900\u8000\ufdcf\u8000\ufdf0\u8000\ufffd\u0005\u0000-.0"+
+ "9\u00b7\u00b7\u0300\u036f\u203f\u2040\u00c6\u0000\u0001\u0001\u0000\u0000"+
+ "\u0000\u0000\u0003\u0001\u0000\u0000\u0000\u0000\u0005\u0001\u0000\u0000"+
+ "\u0000\u0000\u0007\u0001\u0000\u0000\u0000\u0000\t\u0001\u0000\u0000\u0000"+
+ "\u0000\u000b\u0001\u0000\u0000\u0000\u0000\r\u0001\u0000\u0000\u0000\u0000"+
+ "\u000f\u0001\u0000\u0000\u0000\u0000\u0011\u0001\u0000\u0000\u0000\u0000"+
+ "\u0013\u0001\u0000\u0000\u0000\u0000\u0015\u0001\u0000\u0000\u0000\u0000"+
+ "\u0017\u0001\u0000\u0000\u0000\u0000\u0019\u0001\u0000\u0000\u0000\u0000"+
+ "\u001b\u0001\u0000\u0000\u0000\u0000\u001d\u0001\u0000\u0000\u0000\u0000"+
+ "\u001f\u0001\u0000\u0000\u0000\u0000!\u0001\u0000\u0000\u0000\u0000#\u0001"+
+ "\u0000\u0000\u0000\u0000%\u0001\u0000\u0000\u0000\u0000\'\u0001\u0000"+
+ "\u0000\u0000\u0000)\u0001\u0000\u0000\u0000\u0000+\u0001\u0000\u0000\u0000"+
+ "\u0000-\u0001\u0000\u0000\u0000\u0000/\u0001\u0000\u0000\u0000\u00001"+
+ "\u0001\u0000\u0000\u0000\u00003\u0001\u0000\u0000\u0000\u00005\u0001\u0000"+
+ "\u0000\u0000\u0001>\u0001\u0000\u0000\u0000\u0003D\u0001\u0000\u0000\u0000"+
+ "\u0005F\u0001\u0000\u0000\u0000\u0007I\u0001\u0000\u0000\u0000\tL\u0001"+
+ "\u0000\u0000\u0000\u000bN\u0001\u0000\u0000\u0000\rP\u0001\u0000\u0000"+
+ "\u0000\u000fR\u0001\u0000\u0000\u0000\u0011T\u0001\u0000\u0000\u0000\u0013"+
+ "V\u0001\u0000\u0000\u0000\u0015Y\u0001\u0000\u0000\u0000\u0017[\u0001"+
+ "\u0000\u0000\u0000\u0019]\u0001\u0000\u0000\u0000\u001b_\u0001\u0000\u0000"+
+ "\u0000\u001db\u0001\u0000\u0000\u0000\u001fe\u0001\u0000\u0000\u0000!"+
+ "h\u0001\u0000\u0000\u0000#j\u0001\u0000\u0000\u0000%l\u0001\u0000\u0000"+
+ "\u0000\'o\u0001\u0000\u0000\u0000){\u0001\u0000\u0000\u0000+\u007f\u0001"+
+ "\u0000\u0000\u0000-\u0082\u0001\u0000\u0000\u0000/\u008d\u0001\u0000\u0000"+
+ "\u00001\u00ab\u0001\u0000\u0000\u00003\u00ad\u0001\u0000\u0000\u00005"+
+ "\u00b1\u0001\u0000\u0000\u00007\u00b3\u0001\u0000\u0000\u00009\u00bb\u0001"+
+ "\u0000\u0000\u0000;\u00bf\u0001\u0000\u0000\u0000=?\u0007\u0000\u0000"+
+ "\u0000>=\u0001\u0000\u0000\u0000?@\u0001\u0000\u0000\u0000@>\u0001\u0000"+
+ "\u0000\u0000@A\u0001\u0000\u0000\u0000AB\u0001\u0000\u0000\u0000BC\u0006"+
+ "\u0000\u0000\u0000C\u0002\u0001\u0000\u0000\u0000DE\u0005/\u0000\u0000"+
+ "E\u0004\u0001\u0000\u0000\u0000FG\u0005/\u0000\u0000GH\u0005/\u0000\u0000"+
+ "H\u0006\u0001\u0000\u0000\u0000IJ\u0005:\u0000\u0000JK\u0005:\u0000\u0000"+
+ "K\b\u0001\u0000\u0000\u0000LM\u0005[\u0000\u0000M\n\u0001\u0000\u0000"+
+ "\u0000NO\u0005]\u0000\u0000O\f\u0001\u0000\u0000\u0000PQ\u0005(\u0000"+
+ "\u0000Q\u000e\u0001\u0000\u0000\u0000RS\u0005)\u0000\u0000S\u0010\u0001"+
+ "\u0000\u0000\u0000TU\u0005@\u0000\u0000U\u0012\u0001\u0000\u0000\u0000"+
+ "VW\u0005.\u0000\u0000WX\u0005.\u0000\u0000X\u0014\u0001\u0000\u0000\u0000"+
+ "YZ\u0005.\u0000\u0000Z\u0016\u0001\u0000\u0000\u0000[\\\u0005,\u0000\u0000"+
+ "\\\u0018\u0001\u0000\u0000\u0000]^\u0005=\u0000\u0000^\u001a\u0001\u0000"+
+ "\u0000\u0000_`\u0005!\u0000\u0000`a\u0005=\u0000\u0000a\u001c\u0001\u0000"+
+ "\u0000\u0000bc\u0005<\u0000\u0000cd\u0005=\u0000\u0000d\u001e\u0001\u0000"+
+ "\u0000\u0000ef\u0005>\u0000\u0000fg\u0005=\u0000\u0000g \u0001\u0000\u0000"+
+ "\u0000hi\u0005<\u0000\u0000i\"\u0001\u0000\u0000\u0000jk\u0005>\u0000"+
+ "\u0000k$\u0001\u0000\u0000\u0000lm\u0005*\u0000\u0000m&\u0001\u0000\u0000"+
+ "\u0000np\u0007\u0001\u0000\u0000on\u0001\u0000\u0000\u0000pq\u0001\u0000"+
+ "\u0000\u0000qo\u0001\u0000\u0000\u0000qr\u0001\u0000\u0000\u0000ry\u0001"+
+ "\u0000\u0000\u0000su\u0005.\u0000\u0000tv\u0007\u0001\u0000\u0000ut\u0001"+
+ "\u0000\u0000\u0000vw\u0001\u0000\u0000\u0000wu\u0001\u0000\u0000\u0000"+
+ "wx\u0001\u0000\u0000\u0000xz\u0001\u0000\u0000\u0000ys\u0001\u0000\u0000"+
+ "\u0000yz\u0001\u0000\u0000\u0000z(\u0001\u0000\u0000\u0000{|\u0005a\u0000"+
+ "\u0000|}\u0005n\u0000\u0000}~\u0005d\u0000\u0000~*\u0001\u0000\u0000\u0000"+
+ "\u007f\u0080\u0005o\u0000\u0000\u0080\u0081\u0005r\u0000\u0000\u0081,"+
+ "\u0001\u0000\u0000\u0000\u0082\u0083\u0005l\u0000\u0000\u0083\u0084\u0005"+
+ "o\u0000\u0000\u0084\u0085\u0005c\u0000\u0000\u0085\u0086\u0005a\u0000"+
+ "\u0000\u0086\u0087\u0005l\u0000\u0000\u0087\u0088\u0005-\u0000\u0000\u0088"+
+ "\u0089\u0005n\u0000\u0000\u0089\u008a\u0005a\u0000\u0000\u008a\u008b\u0005"+
+ "m\u0000\u0000\u008b\u008c\u0005e\u0000\u0000\u008c.\u0001\u0000\u0000"+
+ "\u0000\u008d\u008e\u0005n\u0000\u0000\u008e\u008f\u0005a\u0000\u0000\u008f"+
+ "\u0090\u0005m\u0000\u0000\u0090\u0091\u0005e\u0000\u0000\u0091\u0092\u0005"+
+ "s\u0000\u0000\u0092\u0093\u0005p\u0000\u0000\u0093\u0094\u0005a\u0000"+
+ "\u0000\u0094\u0095\u0005c\u0000\u0000\u0095\u0096\u0005e\u0000\u0000\u0096"+
+ "\u0097\u0005-\u0000\u0000\u0097\u0098\u0005u\u0000\u0000\u0098\u0099\u0005"+
+ "r\u0000\u0000\u0099\u009a\u0005i\u0000\u0000\u009a0\u0001\u0000\u0000"+
+ "\u0000\u009b\u009f\u0005\'\u0000\u0000\u009c\u009e\b\u0002\u0000\u0000"+
+ "\u009d\u009c\u0001\u0000\u0000\u0000\u009e\u00a1\u0001\u0000\u0000\u0000"+
+ "\u009f\u009d\u0001\u0000\u0000\u0000\u009f\u00a0\u0001\u0000\u0000\u0000"+
+ "\u00a0\u00a2\u0001\u0000\u0000\u0000\u00a1\u009f\u0001\u0000\u0000\u0000"+
+ "\u00a2\u00ac\u0005\'\u0000\u0000\u00a3\u00a7\u0005\"\u0000\u0000\u00a4"+
+ "\u00a6\b\u0003\u0000\u0000\u00a5\u00a4\u0001\u0000\u0000\u0000\u00a6\u00a9"+
+ "\u0001\u0000\u0000\u0000\u00a7\u00a5\u0001\u0000\u0000\u0000\u00a7\u00a8"+
+ "\u0001\u0000\u0000\u0000\u00a8\u00aa\u0001\u0000\u0000\u0000\u00a9\u00a7"+
+ "\u0001\u0000\u0000\u0000\u00aa\u00ac\u0005\"\u0000\u0000\u00ab\u009b\u0001"+
+ "\u0000\u0000\u0000\u00ab\u00a3\u0001\u0000\u0000\u0000\u00ac2\u0001\u0000"+
+ "\u0000\u0000\u00ad\u00ae\u00037\u001b\u0000\u00ae\u00af\u0005:\u0000\u0000"+
+ "\u00af\u00b0\u00037\u001b\u0000\u00b04\u0001\u0000\u0000\u0000\u00b1\u00b2"+
+ "\u00037\u001b\u0000\u00b26\u0001\u0000\u0000\u0000\u00b3\u00b7\u00039"+
+ "\u001c\u0000\u00b4\u00b6\u0003;\u001d\u0000\u00b5\u00b4\u0001\u0000\u0000"+
+ "\u0000\u00b6\u00b9\u0001\u0000\u0000\u0000\u00b7\u00b5\u0001\u0000\u0000"+
+ "\u0000\u00b7\u00b8\u0001\u0000\u0000\u0000\u00b88\u0001\u0000\u0000\u0000"+
+ "\u00b9\u00b7\u0001\u0000\u0000\u0000\u00ba\u00bc\u0007\u0004\u0000\u0000"+
+ "\u00bb\u00ba\u0001\u0000\u0000\u0000\u00bc:\u0001\u0000\u0000\u0000\u00bd"+
+ "\u00c0\u00039\u001c\u0000\u00be\u00c0\u0007\u0005\u0000\u0000\u00bf\u00bd"+
+ "\u0001\u0000\u0000\u0000\u00bf\u00be\u0001\u0000\u0000\u0000\u00c0<\u0001"+
+ "\u0000\u0000\u0000\u000b\u0000@qwy\u009f\u00a7\u00ab\u00b7\u00bb\u00bf"+
+ "\u0001\u0006\u0000\u0000";
+ public static final ATN _ATN =
+ new ATNDeserializer().deserialize(_serializedATN.toCharArray());
+ static {
+ _decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
+ for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
+ _decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
+ }
+ }
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.tokens b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.tokens
new file mode 100644
index 0000000000..68cbcd6cc2
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathLexer.tokens
@@ -0,0 +1,49 @@
+WS=1
+SLASH=2
+DOUBLE_SLASH=3
+AXIS_SEP=4
+LBRACKET=5
+RBRACKET=6
+LPAREN=7
+RPAREN=8
+AT=9
+DOTDOT=10
+DOT=11
+COMMA=12
+EQUALS=13
+NOT_EQUALS=14
+LTE=15
+GTE=16
+LT=17
+GT=18
+WILDCARD=19
+NUMBER=20
+AND=21
+OR=22
+LOCAL_NAME=23
+NAMESPACE_URI=24
+STRING_LITERAL=25
+QNAME=26
+NCNAME=27
+'/'=2
+'//'=3
+'::'=4
+'['=5
+']'=6
+'('=7
+')'=8
+'@'=9
+'..'=10
+'.'=11
+','=12
+'='=13
+'!='=14
+'<='=15
+'>='=16
+'<'=17
+'>'=18
+'*'=19
+'and'=21
+'or'=22
+'local-name'=23
+'namespace-uri'=24
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.interp b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.interp
new file mode 100644
index 0000000000..2097a48dc8
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.interp
@@ -0,0 +1,91 @@
+token literal names:
+null
+null
+'/'
+'//'
+'::'
+'['
+']'
+'('
+')'
+'@'
+'..'
+'.'
+','
+'='
+'!='
+'<='
+'>='
+'<'
+'>'
+'*'
+null
+'and'
+'or'
+'local-name'
+'namespace-uri'
+null
+null
+null
+
+token symbolic names:
+null
+WS
+SLASH
+DOUBLE_SLASH
+AXIS_SEP
+LBRACKET
+RBRACKET
+LPAREN
+RPAREN
+AT
+DOTDOT
+DOT
+COMMA
+EQUALS
+NOT_EQUALS
+LTE
+GTE
+LT
+GT
+WILDCARD
+NUMBER
+AND
+OR
+LOCAL_NAME
+NAMESPACE_URI
+STRING_LITERAL
+QNAME
+NCNAME
+
+rule names:
+xpathExpression
+filterExpr
+booleanExpr
+comparisonOp
+comparand
+absoluteLocationPath
+relativeLocationPath
+pathSeparator
+step
+axisStep
+axisName
+abbreviatedStep
+nodeTypeTest
+attributeStep
+nodeTest
+predicate
+predicateExpr
+orExpr
+andExpr
+primaryExpr
+predicateValue
+functionCall
+functionArgs
+functionArg
+childElementTest
+stringLiteral
+
+
+atn:
+[4, 1, 27, 218, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 1, 0, 1, 0, 1, 0, 1, 0, 3, 0, 57, 8, 0, 1, 1, 1, 1, 1, 1, 3, 1, 62, 8, 1, 1, 1, 1, 1, 4, 1, 66, 8, 1, 11, 1, 12, 1, 67, 1, 1, 1, 1, 1, 1, 3, 1, 73, 8, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 80, 8, 2, 1, 3, 1, 3, 1, 4, 1, 4, 3, 4, 86, 8, 4, 1, 5, 1, 5, 3, 5, 90, 8, 5, 1, 5, 1, 5, 3, 5, 94, 8, 5, 1, 6, 1, 6, 1, 6, 1, 6, 5, 6, 100, 8, 6, 10, 6, 12, 6, 103, 9, 6, 1, 7, 1, 7, 1, 8, 1, 8, 5, 8, 109, 8, 8, 10, 8, 12, 8, 112, 9, 8, 1, 8, 1, 8, 5, 8, 116, 8, 8, 10, 8, 12, 8, 119, 9, 8, 1, 8, 1, 8, 5, 8, 123, 8, 8, 10, 8, 12, 8, 126, 9, 8, 1, 8, 1, 8, 3, 8, 130, 8, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 5, 17, 158, 8, 17, 10, 17, 12, 17, 161, 9, 17, 1, 18, 1, 18, 1, 18, 5, 18, 166, 8, 18, 10, 18, 12, 18, 169, 9, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 176, 8, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 3, 20, 183, 8, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 194, 8, 21, 1, 21, 3, 21, 197, 8, 21, 1, 22, 1, 22, 1, 22, 5, 22, 202, 8, 22, 10, 22, 12, 22, 205, 9, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 3, 23, 212, 8, 23, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 0, 0, 26, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 0, 4, 1, 0, 13, 18, 1, 0, 2, 3, 1, 0, 10, 11, 2, 0, 19, 19, 26, 27, 224, 0, 56, 1, 0, 0, 0, 2, 58, 1, 0, 0, 0, 4, 79, 1, 0, 0, 0, 6, 81, 1, 0, 0, 0, 8, 85, 1, 0, 0, 0, 10, 93, 1, 0, 0, 0, 12, 95, 1, 0, 0, 0, 14, 104, 1, 0, 0, 0, 16, 129, 1, 0, 0, 0, 18, 131, 1, 0, 0, 0, 20, 135, 1, 0, 0, 0, 22, 137, 1, 0, 0, 0, 24, 139, 1, 0, 0, 0, 26, 143, 1, 0, 0, 0, 28, 146, 1, 0, 0, 0, 30, 148, 1, 0, 0, 0, 32, 152, 1, 0, 0, 0, 34, 154, 1, 0, 0, 0, 36, 162, 1, 0, 0, 0, 38, 175, 1, 0, 0, 0, 40, 182, 1, 0, 0, 0, 42, 196, 1, 0, 0, 0, 44, 198, 1, 0, 0, 0, 46, 211, 1, 0, 0, 0, 48, 213, 1, 0, 0, 0, 50, 215, 1, 0, 0, 0, 52, 57, 3, 4, 2, 0, 53, 57, 3, 2, 1, 0, 54, 57, 3, 10, 5, 0, 55, 57, 3, 12, 6, 0, 56, 52, 1, 0, 0, 0, 56, 53, 1, 0, 0, 0, 56, 54, 1, 0, 0, 0, 56, 55, 1, 0, 0, 0, 57, 1, 1, 0, 0, 0, 58, 61, 5, 7, 0, 0, 59, 62, 3, 10, 5, 0, 60, 62, 3, 12, 6, 0, 61, 59, 1, 0, 0, 0, 61, 60, 1, 0, 0, 0, 62, 63, 1, 0, 0, 0, 63, 65, 5, 8, 0, 0, 64, 66, 3, 30, 15, 0, 65, 64, 1, 0, 0, 0, 66, 67, 1, 0, 0, 0, 67, 65, 1, 0, 0, 0, 67, 68, 1, 0, 0, 0, 68, 72, 1, 0, 0, 0, 69, 70, 3, 14, 7, 0, 70, 71, 3, 12, 6, 0, 71, 73, 1, 0, 0, 0, 72, 69, 1, 0, 0, 0, 72, 73, 1, 0, 0, 0, 73, 3, 1, 0, 0, 0, 74, 75, 3, 42, 21, 0, 75, 76, 3, 6, 3, 0, 76, 77, 3, 8, 4, 0, 77, 80, 1, 0, 0, 0, 78, 80, 3, 42, 21, 0, 79, 74, 1, 0, 0, 0, 79, 78, 1, 0, 0, 0, 80, 5, 1, 0, 0, 0, 81, 82, 7, 0, 0, 0, 82, 7, 1, 0, 0, 0, 83, 86, 3, 50, 25, 0, 84, 86, 5, 20, 0, 0, 85, 83, 1, 0, 0, 0, 85, 84, 1, 0, 0, 0, 86, 9, 1, 0, 0, 0, 87, 89, 5, 2, 0, 0, 88, 90, 3, 12, 6, 0, 89, 88, 1, 0, 0, 0, 89, 90, 1, 0, 0, 0, 90, 94, 1, 0, 0, 0, 91, 92, 5, 3, 0, 0, 92, 94, 3, 12, 6, 0, 93, 87, 1, 0, 0, 0, 93, 91, 1, 0, 0, 0, 94, 11, 1, 0, 0, 0, 95, 101, 3, 16, 8, 0, 96, 97, 3, 14, 7, 0, 97, 98, 3, 16, 8, 0, 98, 100, 1, 0, 0, 0, 99, 96, 1, 0, 0, 0, 100, 103, 1, 0, 0, 0, 101, 99, 1, 0, 0, 0, 101, 102, 1, 0, 0, 0, 102, 13, 1, 0, 0, 0, 103, 101, 1, 0, 0, 0, 104, 105, 7, 1, 0, 0, 105, 15, 1, 0, 0, 0, 106, 110, 3, 18, 9, 0, 107, 109, 3, 30, 15, 0, 108, 107, 1, 0, 0, 0, 109, 112, 1, 0, 0, 0, 110, 108, 1, 0, 0, 0, 110, 111, 1, 0, 0, 0, 111, 130, 1, 0, 0, 0, 112, 110, 1, 0, 0, 0, 113, 117, 3, 28, 14, 0, 114, 116, 3, 30, 15, 0, 115, 114, 1, 0, 0, 0, 116, 119, 1, 0, 0, 0, 117, 115, 1, 0, 0, 0, 117, 118, 1, 0, 0, 0, 118, 130, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 120, 124, 3, 26, 13, 0, 121, 123, 3, 30, 15, 0, 122, 121, 1, 0, 0, 0, 123, 126, 1, 0, 0, 0, 124, 122, 1, 0, 0, 0, 124, 125, 1, 0, 0, 0, 125, 130, 1, 0, 0, 0, 126, 124, 1, 0, 0, 0, 127, 130, 3, 24, 12, 0, 128, 130, 3, 22, 11, 0, 129, 106, 1, 0, 0, 0, 129, 113, 1, 0, 0, 0, 129, 120, 1, 0, 0, 0, 129, 127, 1, 0, 0, 0, 129, 128, 1, 0, 0, 0, 130, 17, 1, 0, 0, 0, 131, 132, 3, 20, 10, 0, 132, 133, 5, 4, 0, 0, 133, 134, 3, 28, 14, 0, 134, 19, 1, 0, 0, 0, 135, 136, 5, 27, 0, 0, 136, 21, 1, 0, 0, 0, 137, 138, 7, 2, 0, 0, 138, 23, 1, 0, 0, 0, 139, 140, 5, 27, 0, 0, 140, 141, 5, 7, 0, 0, 141, 142, 5, 8, 0, 0, 142, 25, 1, 0, 0, 0, 143, 144, 5, 9, 0, 0, 144, 145, 7, 3, 0, 0, 145, 27, 1, 0, 0, 0, 146, 147, 7, 3, 0, 0, 147, 29, 1, 0, 0, 0, 148, 149, 5, 5, 0, 0, 149, 150, 3, 32, 16, 0, 150, 151, 5, 6, 0, 0, 151, 31, 1, 0, 0, 0, 152, 153, 3, 34, 17, 0, 153, 33, 1, 0, 0, 0, 154, 159, 3, 36, 18, 0, 155, 156, 5, 22, 0, 0, 156, 158, 3, 36, 18, 0, 157, 155, 1, 0, 0, 0, 158, 161, 1, 0, 0, 0, 159, 157, 1, 0, 0, 0, 159, 160, 1, 0, 0, 0, 160, 35, 1, 0, 0, 0, 161, 159, 1, 0, 0, 0, 162, 167, 3, 38, 19, 0, 163, 164, 5, 21, 0, 0, 164, 166, 3, 38, 19, 0, 165, 163, 1, 0, 0, 0, 166, 169, 1, 0, 0, 0, 167, 165, 1, 0, 0, 0, 167, 168, 1, 0, 0, 0, 168, 37, 1, 0, 0, 0, 169, 167, 1, 0, 0, 0, 170, 171, 3, 40, 20, 0, 171, 172, 3, 6, 3, 0, 172, 173, 3, 8, 4, 0, 173, 176, 1, 0, 0, 0, 174, 176, 3, 40, 20, 0, 175, 170, 1, 0, 0, 0, 175, 174, 1, 0, 0, 0, 176, 39, 1, 0, 0, 0, 177, 183, 3, 42, 21, 0, 178, 183, 3, 26, 13, 0, 179, 183, 3, 12, 6, 0, 180, 183, 3, 48, 24, 0, 181, 183, 5, 20, 0, 0, 182, 177, 1, 0, 0, 0, 182, 178, 1, 0, 0, 0, 182, 179, 1, 0, 0, 0, 182, 180, 1, 0, 0, 0, 182, 181, 1, 0, 0, 0, 183, 41, 1, 0, 0, 0, 184, 185, 5, 23, 0, 0, 185, 186, 5, 7, 0, 0, 186, 197, 5, 8, 0, 0, 187, 188, 5, 24, 0, 0, 188, 189, 5, 7, 0, 0, 189, 197, 5, 8, 0, 0, 190, 191, 5, 27, 0, 0, 191, 193, 5, 7, 0, 0, 192, 194, 3, 44, 22, 0, 193, 192, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 195, 1, 0, 0, 0, 195, 197, 5, 8, 0, 0, 196, 184, 1, 0, 0, 0, 196, 187, 1, 0, 0, 0, 196, 190, 1, 0, 0, 0, 197, 43, 1, 0, 0, 0, 198, 203, 3, 46, 23, 0, 199, 200, 5, 12, 0, 0, 200, 202, 3, 46, 23, 0, 201, 199, 1, 0, 0, 0, 202, 205, 1, 0, 0, 0, 203, 201, 1, 0, 0, 0, 203, 204, 1, 0, 0, 0, 204, 45, 1, 0, 0, 0, 205, 203, 1, 0, 0, 0, 206, 212, 3, 10, 5, 0, 207, 212, 3, 42, 21, 0, 208, 212, 3, 12, 6, 0, 209, 212, 3, 50, 25, 0, 210, 212, 5, 20, 0, 0, 211, 206, 1, 0, 0, 0, 211, 207, 1, 0, 0, 0, 211, 208, 1, 0, 0, 0, 211, 209, 1, 0, 0, 0, 211, 210, 1, 0, 0, 0, 212, 47, 1, 0, 0, 0, 213, 214, 7, 3, 0, 0, 214, 49, 1, 0, 0, 0, 215, 216, 5, 25, 0, 0, 216, 51, 1, 0, 0, 0, 21, 56, 61, 67, 72, 79, 85, 89, 93, 101, 110, 117, 124, 129, 159, 167, 175, 182, 193, 196, 203, 211]
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.java
new file mode 100644
index 0000000000..6e3c5cc3c1
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.java
@@ -0,0 +1,2040 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathParser.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.atn.*;
+import org.antlr.v4.runtime.dfa.DFA;
+import org.antlr.v4.runtime.*;
+import org.antlr.v4.runtime.misc.*;
+import org.antlr.v4.runtime.tree.*;
+import java.util.List;
+import java.util.Iterator;
+import java.util.ArrayList;
+
+@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast", "CheckReturnValue", "this-escape"})
+public class XPathParser extends Parser {
+ static { RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION); }
+
+ protected static final DFA[] _decisionToDFA;
+ protected static final PredictionContextCache _sharedContextCache =
+ new PredictionContextCache();
+ public static final int
+ WS=1, SLASH=2, DOUBLE_SLASH=3, AXIS_SEP=4, LBRACKET=5, RBRACKET=6, LPAREN=7,
+ RPAREN=8, AT=9, DOTDOT=10, DOT=11, COMMA=12, EQUALS=13, NOT_EQUALS=14,
+ LTE=15, GTE=16, LT=17, GT=18, WILDCARD=19, NUMBER=20, AND=21, OR=22, LOCAL_NAME=23,
+ NAMESPACE_URI=24, STRING_LITERAL=25, QNAME=26, NCNAME=27;
+ public static final int
+ RULE_xpathExpression = 0, RULE_filterExpr = 1, RULE_booleanExpr = 2, RULE_comparisonOp = 3,
+ RULE_comparand = 4, RULE_absoluteLocationPath = 5, RULE_relativeLocationPath = 6,
+ RULE_pathSeparator = 7, RULE_step = 8, RULE_axisStep = 9, RULE_axisName = 10,
+ RULE_abbreviatedStep = 11, RULE_nodeTypeTest = 12, RULE_attributeStep = 13,
+ RULE_nodeTest = 14, RULE_predicate = 15, RULE_predicateExpr = 16, RULE_orExpr = 17,
+ RULE_andExpr = 18, RULE_primaryExpr = 19, RULE_predicateValue = 20, RULE_functionCall = 21,
+ RULE_functionArgs = 22, RULE_functionArg = 23, RULE_childElementTest = 24,
+ RULE_stringLiteral = 25;
+ private static String[] makeRuleNames() {
+ return new String[] {
+ "xpathExpression", "filterExpr", "booleanExpr", "comparisonOp", "comparand",
+ "absoluteLocationPath", "relativeLocationPath", "pathSeparator", "step",
+ "axisStep", "axisName", "abbreviatedStep", "nodeTypeTest", "attributeStep",
+ "nodeTest", "predicate", "predicateExpr", "orExpr", "andExpr", "primaryExpr",
+ "predicateValue", "functionCall", "functionArgs", "functionArg", "childElementTest",
+ "stringLiteral"
+ };
+ }
+ public static final String[] ruleNames = makeRuleNames();
+
+ private static String[] makeLiteralNames() {
+ return new String[] {
+ null, null, "'/'", "'//'", "'::'", "'['", "']'", "'('", "')'", "'@'",
+ "'..'", "'.'", "','", "'='", "'!='", "'<='", "'>='", "'<'", "'>'", "'*'",
+ null, "'and'", "'or'", "'local-name'", "'namespace-uri'"
+ };
+ }
+ private static final String[] _LITERAL_NAMES = makeLiteralNames();
+ private static String[] makeSymbolicNames() {
+ return new String[] {
+ null, "WS", "SLASH", "DOUBLE_SLASH", "AXIS_SEP", "LBRACKET", "RBRACKET",
+ "LPAREN", "RPAREN", "AT", "DOTDOT", "DOT", "COMMA", "EQUALS", "NOT_EQUALS",
+ "LTE", "GTE", "LT", "GT", "WILDCARD", "NUMBER", "AND", "OR", "LOCAL_NAME",
+ "NAMESPACE_URI", "STRING_LITERAL", "QNAME", "NCNAME"
+ };
+ }
+ private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames();
+ public static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES);
+
+ /**
+ * @deprecated Use {@link #VOCABULARY} instead.
+ */
+ @Deprecated
+ public static final String[] tokenNames;
+ static {
+ tokenNames = new String[_SYMBOLIC_NAMES.length];
+ for (int i = 0; i < tokenNames.length; i++) {
+ tokenNames[i] = VOCABULARY.getLiteralName(i);
+ if (tokenNames[i] == null) {
+ tokenNames[i] = VOCABULARY.getSymbolicName(i);
+ }
+
+ if (tokenNames[i] == null) {
+ tokenNames[i] = "";
+ }
+ }
+ }
+
+ @Override
+ @Deprecated
+ public String[] getTokenNames() {
+ return tokenNames;
+ }
+
+ @Override
+
+ public Vocabulary getVocabulary() {
+ return VOCABULARY;
+ }
+
+ @Override
+ public String getGrammarFileName() { return "XPathParser.g4"; }
+
+ @Override
+ public String[] getRuleNames() { return ruleNames; }
+
+ @Override
+ public String getSerializedATN() { return _serializedATN; }
+
+ @Override
+ public ATN getATN() { return _ATN; }
+
+ public XPathParser(TokenStream input) {
+ super(input);
+ _interp = new ParserATNSimulator(this,_ATN,_decisionToDFA,_sharedContextCache);
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class XpathExpressionContext extends ParserRuleContext {
+ public BooleanExprContext booleanExpr() {
+ return getRuleContext(BooleanExprContext.class,0);
+ }
+ public FilterExprContext filterExpr() {
+ return getRuleContext(FilterExprContext.class,0);
+ }
+ public AbsoluteLocationPathContext absoluteLocationPath() {
+ return getRuleContext(AbsoluteLocationPathContext.class,0);
+ }
+ public RelativeLocationPathContext relativeLocationPath() {
+ return getRuleContext(RelativeLocationPathContext.class,0);
+ }
+ public XpathExpressionContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_xpathExpression; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterXpathExpression(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitXpathExpression(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitXpathExpression(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final XpathExpressionContext xpathExpression() throws RecognitionException {
+ XpathExpressionContext _localctx = new XpathExpressionContext(_ctx, getState());
+ enterRule(_localctx, 0, RULE_xpathExpression);
+ try {
+ setState(56);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,0,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(52);
+ booleanExpr();
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(53);
+ filterExpr();
+ }
+ break;
+ case 3:
+ enterOuterAlt(_localctx, 3);
+ {
+ setState(54);
+ absoluteLocationPath();
+ }
+ break;
+ case 4:
+ enterOuterAlt(_localctx, 4);
+ {
+ setState(55);
+ relativeLocationPath();
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class FilterExprContext extends ParserRuleContext {
+ public TerminalNode LPAREN() { return getToken(XPathParser.LPAREN, 0); }
+ public TerminalNode RPAREN() { return getToken(XPathParser.RPAREN, 0); }
+ public AbsoluteLocationPathContext absoluteLocationPath() {
+ return getRuleContext(AbsoluteLocationPathContext.class,0);
+ }
+ public List relativeLocationPath() {
+ return getRuleContexts(RelativeLocationPathContext.class);
+ }
+ public RelativeLocationPathContext relativeLocationPath(int i) {
+ return getRuleContext(RelativeLocationPathContext.class,i);
+ }
+ public List predicate() {
+ return getRuleContexts(PredicateContext.class);
+ }
+ public PredicateContext predicate(int i) {
+ return getRuleContext(PredicateContext.class,i);
+ }
+ public PathSeparatorContext pathSeparator() {
+ return getRuleContext(PathSeparatorContext.class,0);
+ }
+ public FilterExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_filterExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterFilterExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitFilterExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitFilterExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final FilterExprContext filterExpr() throws RecognitionException {
+ FilterExprContext _localctx = new FilterExprContext(_ctx, getState());
+ enterRule(_localctx, 2, RULE_filterExpr);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(58);
+ match(LPAREN);
+ setState(61);
+ _errHandler.sync(this);
+ switch (_input.LA(1)) {
+ case SLASH:
+ case DOUBLE_SLASH:
+ {
+ setState(59);
+ absoluteLocationPath();
+ }
+ break;
+ case AT:
+ case DOTDOT:
+ case DOT:
+ case WILDCARD:
+ case QNAME:
+ case NCNAME:
+ {
+ setState(60);
+ relativeLocationPath();
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ setState(63);
+ match(RPAREN);
+ setState(65);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ do {
+ {
+ {
+ setState(64);
+ predicate();
+ }
+ }
+ setState(67);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ } while ( _la==LBRACKET );
+ setState(72);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ if (_la==SLASH || _la==DOUBLE_SLASH) {
+ {
+ setState(69);
+ pathSeparator();
+ setState(70);
+ relativeLocationPath();
+ }
+ }
+
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class BooleanExprContext extends ParserRuleContext {
+ public FunctionCallContext functionCall() {
+ return getRuleContext(FunctionCallContext.class,0);
+ }
+ public ComparisonOpContext comparisonOp() {
+ return getRuleContext(ComparisonOpContext.class,0);
+ }
+ public ComparandContext comparand() {
+ return getRuleContext(ComparandContext.class,0);
+ }
+ public BooleanExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_booleanExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterBooleanExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitBooleanExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitBooleanExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final BooleanExprContext booleanExpr() throws RecognitionException {
+ BooleanExprContext _localctx = new BooleanExprContext(_ctx, getState());
+ enterRule(_localctx, 4, RULE_booleanExpr);
+ try {
+ setState(79);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,4,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(74);
+ functionCall();
+ setState(75);
+ comparisonOp();
+ setState(76);
+ comparand();
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(78);
+ functionCall();
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class ComparisonOpContext extends ParserRuleContext {
+ public TerminalNode EQUALS() { return getToken(XPathParser.EQUALS, 0); }
+ public TerminalNode NOT_EQUALS() { return getToken(XPathParser.NOT_EQUALS, 0); }
+ public TerminalNode LT() { return getToken(XPathParser.LT, 0); }
+ public TerminalNode GT() { return getToken(XPathParser.GT, 0); }
+ public TerminalNode LTE() { return getToken(XPathParser.LTE, 0); }
+ public TerminalNode GTE() { return getToken(XPathParser.GTE, 0); }
+ public ComparisonOpContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_comparisonOp; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterComparisonOp(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitComparisonOp(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitComparisonOp(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final ComparisonOpContext comparisonOp() throws RecognitionException {
+ ComparisonOpContext _localctx = new ComparisonOpContext(_ctx, getState());
+ enterRule(_localctx, 6, RULE_comparisonOp);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(81);
+ _la = _input.LA(1);
+ if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 516096L) != 0)) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class ComparandContext extends ParserRuleContext {
+ public StringLiteralContext stringLiteral() {
+ return getRuleContext(StringLiteralContext.class,0);
+ }
+ public TerminalNode NUMBER() { return getToken(XPathParser.NUMBER, 0); }
+ public ComparandContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_comparand; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterComparand(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitComparand(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitComparand(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final ComparandContext comparand() throws RecognitionException {
+ ComparandContext _localctx = new ComparandContext(_ctx, getState());
+ enterRule(_localctx, 8, RULE_comparand);
+ try {
+ setState(85);
+ _errHandler.sync(this);
+ switch (_input.LA(1)) {
+ case STRING_LITERAL:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(83);
+ stringLiteral();
+ }
+ break;
+ case NUMBER:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(84);
+ match(NUMBER);
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AbsoluteLocationPathContext extends ParserRuleContext {
+ public TerminalNode SLASH() { return getToken(XPathParser.SLASH, 0); }
+ public RelativeLocationPathContext relativeLocationPath() {
+ return getRuleContext(RelativeLocationPathContext.class,0);
+ }
+ public TerminalNode DOUBLE_SLASH() { return getToken(XPathParser.DOUBLE_SLASH, 0); }
+ public AbsoluteLocationPathContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_absoluteLocationPath; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAbsoluteLocationPath(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAbsoluteLocationPath(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAbsoluteLocationPath(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AbsoluteLocationPathContext absoluteLocationPath() throws RecognitionException {
+ AbsoluteLocationPathContext _localctx = new AbsoluteLocationPathContext(_ctx, getState());
+ enterRule(_localctx, 10, RULE_absoluteLocationPath);
+ int _la;
+ try {
+ setState(93);
+ _errHandler.sync(this);
+ switch (_input.LA(1)) {
+ case SLASH:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(87);
+ match(SLASH);
+ setState(89);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 201854464L) != 0)) {
+ {
+ setState(88);
+ relativeLocationPath();
+ }
+ }
+
+ }
+ break;
+ case DOUBLE_SLASH:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(91);
+ match(DOUBLE_SLASH);
+ setState(92);
+ relativeLocationPath();
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class RelativeLocationPathContext extends ParserRuleContext {
+ public List step() {
+ return getRuleContexts(StepContext.class);
+ }
+ public StepContext step(int i) {
+ return getRuleContext(StepContext.class,i);
+ }
+ public List pathSeparator() {
+ return getRuleContexts(PathSeparatorContext.class);
+ }
+ public PathSeparatorContext pathSeparator(int i) {
+ return getRuleContext(PathSeparatorContext.class,i);
+ }
+ public RelativeLocationPathContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_relativeLocationPath; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterRelativeLocationPath(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitRelativeLocationPath(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitRelativeLocationPath(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final RelativeLocationPathContext relativeLocationPath() throws RecognitionException {
+ RelativeLocationPathContext _localctx = new RelativeLocationPathContext(_ctx, getState());
+ enterRule(_localctx, 12, RULE_relativeLocationPath);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(95);
+ step();
+ setState(101);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==SLASH || _la==DOUBLE_SLASH) {
+ {
+ {
+ setState(96);
+ pathSeparator();
+ setState(97);
+ step();
+ }
+ }
+ setState(103);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class PathSeparatorContext extends ParserRuleContext {
+ public TerminalNode SLASH() { return getToken(XPathParser.SLASH, 0); }
+ public TerminalNode DOUBLE_SLASH() { return getToken(XPathParser.DOUBLE_SLASH, 0); }
+ public PathSeparatorContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_pathSeparator; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterPathSeparator(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitPathSeparator(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitPathSeparator(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final PathSeparatorContext pathSeparator() throws RecognitionException {
+ PathSeparatorContext _localctx = new PathSeparatorContext(_ctx, getState());
+ enterRule(_localctx, 14, RULE_pathSeparator);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(104);
+ _la = _input.LA(1);
+ if ( !(_la==SLASH || _la==DOUBLE_SLASH) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class StepContext extends ParserRuleContext {
+ public AxisStepContext axisStep() {
+ return getRuleContext(AxisStepContext.class,0);
+ }
+ public List predicate() {
+ return getRuleContexts(PredicateContext.class);
+ }
+ public PredicateContext predicate(int i) {
+ return getRuleContext(PredicateContext.class,i);
+ }
+ public NodeTestContext nodeTest() {
+ return getRuleContext(NodeTestContext.class,0);
+ }
+ public AttributeStepContext attributeStep() {
+ return getRuleContext(AttributeStepContext.class,0);
+ }
+ public NodeTypeTestContext nodeTypeTest() {
+ return getRuleContext(NodeTypeTestContext.class,0);
+ }
+ public AbbreviatedStepContext abbreviatedStep() {
+ return getRuleContext(AbbreviatedStepContext.class,0);
+ }
+ public StepContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_step; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterStep(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitStep(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitStep(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final StepContext step() throws RecognitionException {
+ StepContext _localctx = new StepContext(_ctx, getState());
+ enterRule(_localctx, 16, RULE_step);
+ int _la;
+ try {
+ setState(129);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,12,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(106);
+ axisStep();
+ setState(110);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==LBRACKET) {
+ {
+ {
+ setState(107);
+ predicate();
+ }
+ }
+ setState(112);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(113);
+ nodeTest();
+ setState(117);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==LBRACKET) {
+ {
+ {
+ setState(114);
+ predicate();
+ }
+ }
+ setState(119);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ break;
+ case 3:
+ enterOuterAlt(_localctx, 3);
+ {
+ setState(120);
+ attributeStep();
+ setState(124);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==LBRACKET) {
+ {
+ {
+ setState(121);
+ predicate();
+ }
+ }
+ setState(126);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ break;
+ case 4:
+ enterOuterAlt(_localctx, 4);
+ {
+ setState(127);
+ nodeTypeTest();
+ }
+ break;
+ case 5:
+ enterOuterAlt(_localctx, 5);
+ {
+ setState(128);
+ abbreviatedStep();
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AxisStepContext extends ParserRuleContext {
+ public AxisNameContext axisName() {
+ return getRuleContext(AxisNameContext.class,0);
+ }
+ public TerminalNode AXIS_SEP() { return getToken(XPathParser.AXIS_SEP, 0); }
+ public NodeTestContext nodeTest() {
+ return getRuleContext(NodeTestContext.class,0);
+ }
+ public AxisStepContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_axisStep; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAxisStep(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAxisStep(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAxisStep(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AxisStepContext axisStep() throws RecognitionException {
+ AxisStepContext _localctx = new AxisStepContext(_ctx, getState());
+ enterRule(_localctx, 18, RULE_axisStep);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(131);
+ axisName();
+ setState(132);
+ match(AXIS_SEP);
+ setState(133);
+ nodeTest();
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AxisNameContext extends ParserRuleContext {
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public AxisNameContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_axisName; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAxisName(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAxisName(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAxisName(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AxisNameContext axisName() throws RecognitionException {
+ AxisNameContext _localctx = new AxisNameContext(_ctx, getState());
+ enterRule(_localctx, 20, RULE_axisName);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(135);
+ match(NCNAME);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AbbreviatedStepContext extends ParserRuleContext {
+ public TerminalNode DOTDOT() { return getToken(XPathParser.DOTDOT, 0); }
+ public TerminalNode DOT() { return getToken(XPathParser.DOT, 0); }
+ public AbbreviatedStepContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_abbreviatedStep; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAbbreviatedStep(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAbbreviatedStep(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAbbreviatedStep(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AbbreviatedStepContext abbreviatedStep() throws RecognitionException {
+ AbbreviatedStepContext _localctx = new AbbreviatedStepContext(_ctx, getState());
+ enterRule(_localctx, 22, RULE_abbreviatedStep);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(137);
+ _la = _input.LA(1);
+ if ( !(_la==DOTDOT || _la==DOT) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class NodeTypeTestContext extends ParserRuleContext {
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public TerminalNode LPAREN() { return getToken(XPathParser.LPAREN, 0); }
+ public TerminalNode RPAREN() { return getToken(XPathParser.RPAREN, 0); }
+ public NodeTypeTestContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_nodeTypeTest; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterNodeTypeTest(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitNodeTypeTest(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitNodeTypeTest(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final NodeTypeTestContext nodeTypeTest() throws RecognitionException {
+ NodeTypeTestContext _localctx = new NodeTypeTestContext(_ctx, getState());
+ enterRule(_localctx, 24, RULE_nodeTypeTest);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(139);
+ match(NCNAME);
+ setState(140);
+ match(LPAREN);
+ setState(141);
+ match(RPAREN);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AttributeStepContext extends ParserRuleContext {
+ public TerminalNode AT() { return getToken(XPathParser.AT, 0); }
+ public TerminalNode QNAME() { return getToken(XPathParser.QNAME, 0); }
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public TerminalNode WILDCARD() { return getToken(XPathParser.WILDCARD, 0); }
+ public AttributeStepContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_attributeStep; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAttributeStep(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAttributeStep(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAttributeStep(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AttributeStepContext attributeStep() throws RecognitionException {
+ AttributeStepContext _localctx = new AttributeStepContext(_ctx, getState());
+ enterRule(_localctx, 26, RULE_attributeStep);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(143);
+ match(AT);
+ setState(144);
+ _la = _input.LA(1);
+ if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 201850880L) != 0)) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class NodeTestContext extends ParserRuleContext {
+ public TerminalNode QNAME() { return getToken(XPathParser.QNAME, 0); }
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public TerminalNode WILDCARD() { return getToken(XPathParser.WILDCARD, 0); }
+ public NodeTestContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_nodeTest; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterNodeTest(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitNodeTest(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitNodeTest(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final NodeTestContext nodeTest() throws RecognitionException {
+ NodeTestContext _localctx = new NodeTestContext(_ctx, getState());
+ enterRule(_localctx, 28, RULE_nodeTest);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(146);
+ _la = _input.LA(1);
+ if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 201850880L) != 0)) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class PredicateContext extends ParserRuleContext {
+ public TerminalNode LBRACKET() { return getToken(XPathParser.LBRACKET, 0); }
+ public PredicateExprContext predicateExpr() {
+ return getRuleContext(PredicateExprContext.class,0);
+ }
+ public TerminalNode RBRACKET() { return getToken(XPathParser.RBRACKET, 0); }
+ public PredicateContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_predicate; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterPredicate(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitPredicate(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitPredicate(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final PredicateContext predicate() throws RecognitionException {
+ PredicateContext _localctx = new PredicateContext(_ctx, getState());
+ enterRule(_localctx, 30, RULE_predicate);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(148);
+ match(LBRACKET);
+ setState(149);
+ predicateExpr();
+ setState(150);
+ match(RBRACKET);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class PredicateExprContext extends ParserRuleContext {
+ public OrExprContext orExpr() {
+ return getRuleContext(OrExprContext.class,0);
+ }
+ public PredicateExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_predicateExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterPredicateExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitPredicateExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitPredicateExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final PredicateExprContext predicateExpr() throws RecognitionException {
+ PredicateExprContext _localctx = new PredicateExprContext(_ctx, getState());
+ enterRule(_localctx, 32, RULE_predicateExpr);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(152);
+ orExpr();
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class OrExprContext extends ParserRuleContext {
+ public List andExpr() {
+ return getRuleContexts(AndExprContext.class);
+ }
+ public AndExprContext andExpr(int i) {
+ return getRuleContext(AndExprContext.class,i);
+ }
+ public List OR() { return getTokens(XPathParser.OR); }
+ public TerminalNode OR(int i) {
+ return getToken(XPathParser.OR, i);
+ }
+ public OrExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_orExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterOrExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitOrExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitOrExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final OrExprContext orExpr() throws RecognitionException {
+ OrExprContext _localctx = new OrExprContext(_ctx, getState());
+ enterRule(_localctx, 34, RULE_orExpr);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(154);
+ andExpr();
+ setState(159);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==OR) {
+ {
+ {
+ setState(155);
+ match(OR);
+ setState(156);
+ andExpr();
+ }
+ }
+ setState(161);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class AndExprContext extends ParserRuleContext {
+ public List primaryExpr() {
+ return getRuleContexts(PrimaryExprContext.class);
+ }
+ public PrimaryExprContext primaryExpr(int i) {
+ return getRuleContext(PrimaryExprContext.class,i);
+ }
+ public List AND() { return getTokens(XPathParser.AND); }
+ public TerminalNode AND(int i) {
+ return getToken(XPathParser.AND, i);
+ }
+ public AndExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_andExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterAndExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitAndExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitAndExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final AndExprContext andExpr() throws RecognitionException {
+ AndExprContext _localctx = new AndExprContext(_ctx, getState());
+ enterRule(_localctx, 36, RULE_andExpr);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(162);
+ primaryExpr();
+ setState(167);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==AND) {
+ {
+ {
+ setState(163);
+ match(AND);
+ setState(164);
+ primaryExpr();
+ }
+ }
+ setState(169);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class PrimaryExprContext extends ParserRuleContext {
+ public PredicateValueContext predicateValue() {
+ return getRuleContext(PredicateValueContext.class,0);
+ }
+ public ComparisonOpContext comparisonOp() {
+ return getRuleContext(ComparisonOpContext.class,0);
+ }
+ public ComparandContext comparand() {
+ return getRuleContext(ComparandContext.class,0);
+ }
+ public PrimaryExprContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_primaryExpr; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterPrimaryExpr(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitPrimaryExpr(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitPrimaryExpr(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final PrimaryExprContext primaryExpr() throws RecognitionException {
+ PrimaryExprContext _localctx = new PrimaryExprContext(_ctx, getState());
+ enterRule(_localctx, 38, RULE_primaryExpr);
+ try {
+ setState(175);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,15,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(170);
+ predicateValue();
+ setState(171);
+ comparisonOp();
+ setState(172);
+ comparand();
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(174);
+ predicateValue();
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class PredicateValueContext extends ParserRuleContext {
+ public FunctionCallContext functionCall() {
+ return getRuleContext(FunctionCallContext.class,0);
+ }
+ public AttributeStepContext attributeStep() {
+ return getRuleContext(AttributeStepContext.class,0);
+ }
+ public RelativeLocationPathContext relativeLocationPath() {
+ return getRuleContext(RelativeLocationPathContext.class,0);
+ }
+ public ChildElementTestContext childElementTest() {
+ return getRuleContext(ChildElementTestContext.class,0);
+ }
+ public TerminalNode NUMBER() { return getToken(XPathParser.NUMBER, 0); }
+ public PredicateValueContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_predicateValue; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterPredicateValue(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitPredicateValue(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitPredicateValue(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final PredicateValueContext predicateValue() throws RecognitionException {
+ PredicateValueContext _localctx = new PredicateValueContext(_ctx, getState());
+ enterRule(_localctx, 40, RULE_predicateValue);
+ try {
+ setState(182);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,16,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(177);
+ functionCall();
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(178);
+ attributeStep();
+ }
+ break;
+ case 3:
+ enterOuterAlt(_localctx, 3);
+ {
+ setState(179);
+ relativeLocationPath();
+ }
+ break;
+ case 4:
+ enterOuterAlt(_localctx, 4);
+ {
+ setState(180);
+ childElementTest();
+ }
+ break;
+ case 5:
+ enterOuterAlt(_localctx, 5);
+ {
+ setState(181);
+ match(NUMBER);
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class FunctionCallContext extends ParserRuleContext {
+ public TerminalNode LOCAL_NAME() { return getToken(XPathParser.LOCAL_NAME, 0); }
+ public TerminalNode LPAREN() { return getToken(XPathParser.LPAREN, 0); }
+ public TerminalNode RPAREN() { return getToken(XPathParser.RPAREN, 0); }
+ public TerminalNode NAMESPACE_URI() { return getToken(XPathParser.NAMESPACE_URI, 0); }
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public FunctionArgsContext functionArgs() {
+ return getRuleContext(FunctionArgsContext.class,0);
+ }
+ public FunctionCallContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_functionCall; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterFunctionCall(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitFunctionCall(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitFunctionCall(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final FunctionCallContext functionCall() throws RecognitionException {
+ FunctionCallContext _localctx = new FunctionCallContext(_ctx, getState());
+ enterRule(_localctx, 42, RULE_functionCall);
+ int _la;
+ try {
+ setState(196);
+ _errHandler.sync(this);
+ switch (_input.LA(1)) {
+ case LOCAL_NAME:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(184);
+ match(LOCAL_NAME);
+ setState(185);
+ match(LPAREN);
+ setState(186);
+ match(RPAREN);
+ }
+ break;
+ case NAMESPACE_URI:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(187);
+ match(NAMESPACE_URI);
+ setState(188);
+ match(LPAREN);
+ setState(189);
+ match(RPAREN);
+ }
+ break;
+ case NCNAME:
+ enterOuterAlt(_localctx, 3);
+ {
+ setState(190);
+ match(NCNAME);
+ setState(191);
+ match(LPAREN);
+ setState(193);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 261623308L) != 0)) {
+ {
+ setState(192);
+ functionArgs();
+ }
+ }
+
+ setState(195);
+ match(RPAREN);
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class FunctionArgsContext extends ParserRuleContext {
+ public List functionArg() {
+ return getRuleContexts(FunctionArgContext.class);
+ }
+ public FunctionArgContext functionArg(int i) {
+ return getRuleContext(FunctionArgContext.class,i);
+ }
+ public List COMMA() { return getTokens(XPathParser.COMMA); }
+ public TerminalNode COMMA(int i) {
+ return getToken(XPathParser.COMMA, i);
+ }
+ public FunctionArgsContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_functionArgs; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterFunctionArgs(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitFunctionArgs(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitFunctionArgs(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final FunctionArgsContext functionArgs() throws RecognitionException {
+ FunctionArgsContext _localctx = new FunctionArgsContext(_ctx, getState());
+ enterRule(_localctx, 44, RULE_functionArgs);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(198);
+ functionArg();
+ setState(203);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ while (_la==COMMA) {
+ {
+ {
+ setState(199);
+ match(COMMA);
+ setState(200);
+ functionArg();
+ }
+ }
+ setState(205);
+ _errHandler.sync(this);
+ _la = _input.LA(1);
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class FunctionArgContext extends ParserRuleContext {
+ public AbsoluteLocationPathContext absoluteLocationPath() {
+ return getRuleContext(AbsoluteLocationPathContext.class,0);
+ }
+ public FunctionCallContext functionCall() {
+ return getRuleContext(FunctionCallContext.class,0);
+ }
+ public RelativeLocationPathContext relativeLocationPath() {
+ return getRuleContext(RelativeLocationPathContext.class,0);
+ }
+ public StringLiteralContext stringLiteral() {
+ return getRuleContext(StringLiteralContext.class,0);
+ }
+ public TerminalNode NUMBER() { return getToken(XPathParser.NUMBER, 0); }
+ public FunctionArgContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_functionArg; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterFunctionArg(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitFunctionArg(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitFunctionArg(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final FunctionArgContext functionArg() throws RecognitionException {
+ FunctionArgContext _localctx = new FunctionArgContext(_ctx, getState());
+ enterRule(_localctx, 46, RULE_functionArg);
+ try {
+ setState(211);
+ _errHandler.sync(this);
+ switch ( getInterpreter().adaptivePredict(_input,20,_ctx) ) {
+ case 1:
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(206);
+ absoluteLocationPath();
+ }
+ break;
+ case 2:
+ enterOuterAlt(_localctx, 2);
+ {
+ setState(207);
+ functionCall();
+ }
+ break;
+ case 3:
+ enterOuterAlt(_localctx, 3);
+ {
+ setState(208);
+ relativeLocationPath();
+ }
+ break;
+ case 4:
+ enterOuterAlt(_localctx, 4);
+ {
+ setState(209);
+ stringLiteral();
+ }
+ break;
+ case 5:
+ enterOuterAlt(_localctx, 5);
+ {
+ setState(210);
+ match(NUMBER);
+ }
+ break;
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class ChildElementTestContext extends ParserRuleContext {
+ public TerminalNode QNAME() { return getToken(XPathParser.QNAME, 0); }
+ public TerminalNode NCNAME() { return getToken(XPathParser.NCNAME, 0); }
+ public TerminalNode WILDCARD() { return getToken(XPathParser.WILDCARD, 0); }
+ public ChildElementTestContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_childElementTest; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterChildElementTest(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitChildElementTest(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitChildElementTest(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final ChildElementTestContext childElementTest() throws RecognitionException {
+ ChildElementTestContext _localctx = new ChildElementTestContext(_ctx, getState());
+ enterRule(_localctx, 48, RULE_childElementTest);
+ int _la;
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(213);
+ _la = _input.LA(1);
+ if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 201850880L) != 0)) ) {
+ _errHandler.recoverInline(this);
+ }
+ else {
+ if ( _input.LA(1)==Token.EOF ) matchedEOF = true;
+ _errHandler.reportMatch(this);
+ consume();
+ }
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ @SuppressWarnings("CheckReturnValue")
+ public static class StringLiteralContext extends ParserRuleContext {
+ public TerminalNode STRING_LITERAL() { return getToken(XPathParser.STRING_LITERAL, 0); }
+ public StringLiteralContext(ParserRuleContext parent, int invokingState) {
+ super(parent, invokingState);
+ }
+ @Override public int getRuleIndex() { return RULE_stringLiteral; }
+ @Override
+ public void enterRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).enterStringLiteral(this);
+ }
+ @Override
+ public void exitRule(ParseTreeListener listener) {
+ if ( listener instanceof XPathParserListener ) ((XPathParserListener)listener).exitStringLiteral(this);
+ }
+ @Override
+ public T accept(ParseTreeVisitor extends T> visitor) {
+ if ( visitor instanceof XPathParserVisitor ) return ((XPathParserVisitor extends T>)visitor).visitStringLiteral(this);
+ else return visitor.visitChildren(this);
+ }
+ }
+
+ public final StringLiteralContext stringLiteral() throws RecognitionException {
+ StringLiteralContext _localctx = new StringLiteralContext(_ctx, getState());
+ enterRule(_localctx, 50, RULE_stringLiteral);
+ try {
+ enterOuterAlt(_localctx, 1);
+ {
+ setState(215);
+ match(STRING_LITERAL);
+ }
+ }
+ catch (RecognitionException re) {
+ _localctx.exception = re;
+ _errHandler.reportError(this, re);
+ _errHandler.recover(this, re);
+ }
+ finally {
+ exitRule();
+ }
+ return _localctx;
+ }
+
+ public static final String _serializedATN =
+ "\u0004\u0001\u001b\u00da\u0002\u0000\u0007\u0000\u0002\u0001\u0007\u0001"+
+ "\u0002\u0002\u0007\u0002\u0002\u0003\u0007\u0003\u0002\u0004\u0007\u0004"+
+ "\u0002\u0005\u0007\u0005\u0002\u0006\u0007\u0006\u0002\u0007\u0007\u0007"+
+ "\u0002\b\u0007\b\u0002\t\u0007\t\u0002\n\u0007\n\u0002\u000b\u0007\u000b"+
+ "\u0002\f\u0007\f\u0002\r\u0007\r\u0002\u000e\u0007\u000e\u0002\u000f\u0007"+
+ "\u000f\u0002\u0010\u0007\u0010\u0002\u0011\u0007\u0011\u0002\u0012\u0007"+
+ "\u0012\u0002\u0013\u0007\u0013\u0002\u0014\u0007\u0014\u0002\u0015\u0007"+
+ "\u0015\u0002\u0016\u0007\u0016\u0002\u0017\u0007\u0017\u0002\u0018\u0007"+
+ "\u0018\u0002\u0019\u0007\u0019\u0001\u0000\u0001\u0000\u0001\u0000\u0001"+
+ "\u0000\u0003\u00009\b\u0000\u0001\u0001\u0001\u0001\u0001\u0001\u0003"+
+ "\u0001>\b\u0001\u0001\u0001\u0001\u0001\u0004\u0001B\b\u0001\u000b\u0001"+
+ "\f\u0001C\u0001\u0001\u0001\u0001\u0001\u0001\u0003\u0001I\b\u0001\u0001"+
+ "\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0001\u0002\u0003\u0002P\b"+
+ "\u0002\u0001\u0003\u0001\u0003\u0001\u0004\u0001\u0004\u0003\u0004V\b"+
+ "\u0004\u0001\u0005\u0001\u0005\u0003\u0005Z\b\u0005\u0001\u0005\u0001"+
+ "\u0005\u0003\u0005^\b\u0005\u0001\u0006\u0001\u0006\u0001\u0006\u0001"+
+ "\u0006\u0005\u0006d\b\u0006\n\u0006\f\u0006g\t\u0006\u0001\u0007\u0001"+
+ "\u0007\u0001\b\u0001\b\u0005\bm\b\b\n\b\f\bp\t\b\u0001\b\u0001\b\u0005"+
+ "\bt\b\b\n\b\f\bw\t\b\u0001\b\u0001\b\u0005\b{\b\b\n\b\f\b~\t\b\u0001\b"+
+ "\u0001\b\u0003\b\u0082\b\b\u0001\t\u0001\t\u0001\t\u0001\t\u0001\n\u0001"+
+ "\n\u0001\u000b\u0001\u000b\u0001\f\u0001\f\u0001\f\u0001\f\u0001\r\u0001"+
+ "\r\u0001\r\u0001\u000e\u0001\u000e\u0001\u000f\u0001\u000f\u0001\u000f"+
+ "\u0001\u000f\u0001\u0010\u0001\u0010\u0001\u0011\u0001\u0011\u0001\u0011"+
+ "\u0005\u0011\u009e\b\u0011\n\u0011\f\u0011\u00a1\t\u0011\u0001\u0012\u0001"+
+ "\u0012\u0001\u0012\u0005\u0012\u00a6\b\u0012\n\u0012\f\u0012\u00a9\t\u0012"+
+ "\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0001\u0013\u0003\u0013"+
+ "\u00b0\b\u0013\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014\u0001\u0014"+
+ "\u0003\u0014\u00b7\b\u0014\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0015"+
+ "\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0001\u0015\u0003\u0015"+
+ "\u00c2\b\u0015\u0001\u0015\u0003\u0015\u00c5\b\u0015\u0001\u0016\u0001"+
+ "\u0016\u0001\u0016\u0005\u0016\u00ca\b\u0016\n\u0016\f\u0016\u00cd\t\u0016"+
+ "\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0001\u0017\u0003\u0017"+
+ "\u00d4\b\u0017\u0001\u0018\u0001\u0018\u0001\u0019\u0001\u0019\u0001\u0019"+
+ "\u0000\u0000\u001a\u0000\u0002\u0004\u0006\b\n\f\u000e\u0010\u0012\u0014"+
+ "\u0016\u0018\u001a\u001c\u001e \"$&(*,.02\u0000\u0004\u0001\u0000\r\u0012"+
+ "\u0001\u0000\u0002\u0003\u0001\u0000\n\u000b\u0002\u0000\u0013\u0013\u001a"+
+ "\u001b\u00e0\u00008\u0001\u0000\u0000\u0000\u0002:\u0001\u0000\u0000\u0000"+
+ "\u0004O\u0001\u0000\u0000\u0000\u0006Q\u0001\u0000\u0000\u0000\bU\u0001"+
+ "\u0000\u0000\u0000\n]\u0001\u0000\u0000\u0000\f_\u0001\u0000\u0000\u0000"+
+ "\u000eh\u0001\u0000\u0000\u0000\u0010\u0081\u0001\u0000\u0000\u0000\u0012"+
+ "\u0083\u0001\u0000\u0000\u0000\u0014\u0087\u0001\u0000\u0000\u0000\u0016"+
+ "\u0089\u0001\u0000\u0000\u0000\u0018\u008b\u0001\u0000\u0000\u0000\u001a"+
+ "\u008f\u0001\u0000\u0000\u0000\u001c\u0092\u0001\u0000\u0000\u0000\u001e"+
+ "\u0094\u0001\u0000\u0000\u0000 \u0098\u0001\u0000\u0000\u0000\"\u009a"+
+ "\u0001\u0000\u0000\u0000$\u00a2\u0001\u0000\u0000\u0000&\u00af\u0001\u0000"+
+ "\u0000\u0000(\u00b6\u0001\u0000\u0000\u0000*\u00c4\u0001\u0000\u0000\u0000"+
+ ",\u00c6\u0001\u0000\u0000\u0000.\u00d3\u0001\u0000\u0000\u00000\u00d5"+
+ "\u0001\u0000\u0000\u00002\u00d7\u0001\u0000\u0000\u000049\u0003\u0004"+
+ "\u0002\u000059\u0003\u0002\u0001\u000069\u0003\n\u0005\u000079\u0003\f"+
+ "\u0006\u000084\u0001\u0000\u0000\u000085\u0001\u0000\u0000\u000086\u0001"+
+ "\u0000\u0000\u000087\u0001\u0000\u0000\u00009\u0001\u0001\u0000\u0000"+
+ "\u0000:=\u0005\u0007\u0000\u0000;>\u0003\n\u0005\u0000<>\u0003\f\u0006"+
+ "\u0000=;\u0001\u0000\u0000\u0000=<\u0001\u0000\u0000\u0000>?\u0001\u0000"+
+ "\u0000\u0000?A\u0005\b\u0000\u0000@B\u0003\u001e\u000f\u0000A@\u0001\u0000"+
+ "\u0000\u0000BC\u0001\u0000\u0000\u0000CA\u0001\u0000\u0000\u0000CD\u0001"+
+ "\u0000\u0000\u0000DH\u0001\u0000\u0000\u0000EF\u0003\u000e\u0007\u0000"+
+ "FG\u0003\f\u0006\u0000GI\u0001\u0000\u0000\u0000HE\u0001\u0000\u0000\u0000"+
+ "HI\u0001\u0000\u0000\u0000I\u0003\u0001\u0000\u0000\u0000JK\u0003*\u0015"+
+ "\u0000KL\u0003\u0006\u0003\u0000LM\u0003\b\u0004\u0000MP\u0001\u0000\u0000"+
+ "\u0000NP\u0003*\u0015\u0000OJ\u0001\u0000\u0000\u0000ON\u0001\u0000\u0000"+
+ "\u0000P\u0005\u0001\u0000\u0000\u0000QR\u0007\u0000\u0000\u0000R\u0007"+
+ "\u0001\u0000\u0000\u0000SV\u00032\u0019\u0000TV\u0005\u0014\u0000\u0000"+
+ "US\u0001\u0000\u0000\u0000UT\u0001\u0000\u0000\u0000V\t\u0001\u0000\u0000"+
+ "\u0000WY\u0005\u0002\u0000\u0000XZ\u0003\f\u0006\u0000YX\u0001\u0000\u0000"+
+ "\u0000YZ\u0001\u0000\u0000\u0000Z^\u0001\u0000\u0000\u0000[\\\u0005\u0003"+
+ "\u0000\u0000\\^\u0003\f\u0006\u0000]W\u0001\u0000\u0000\u0000][\u0001"+
+ "\u0000\u0000\u0000^\u000b\u0001\u0000\u0000\u0000_e\u0003\u0010\b\u0000"+
+ "`a\u0003\u000e\u0007\u0000ab\u0003\u0010\b\u0000bd\u0001\u0000\u0000\u0000"+
+ "c`\u0001\u0000\u0000\u0000dg\u0001\u0000\u0000\u0000ec\u0001\u0000\u0000"+
+ "\u0000ef\u0001\u0000\u0000\u0000f\r\u0001\u0000\u0000\u0000ge\u0001\u0000"+
+ "\u0000\u0000hi\u0007\u0001\u0000\u0000i\u000f\u0001\u0000\u0000\u0000"+
+ "jn\u0003\u0012\t\u0000km\u0003\u001e\u000f\u0000lk\u0001\u0000\u0000\u0000"+
+ "mp\u0001\u0000\u0000\u0000nl\u0001\u0000\u0000\u0000no\u0001\u0000\u0000"+
+ "\u0000o\u0082\u0001\u0000\u0000\u0000pn\u0001\u0000\u0000\u0000qu\u0003"+
+ "\u001c\u000e\u0000rt\u0003\u001e\u000f\u0000sr\u0001\u0000\u0000\u0000"+
+ "tw\u0001\u0000\u0000\u0000us\u0001\u0000\u0000\u0000uv\u0001\u0000\u0000"+
+ "\u0000v\u0082\u0001\u0000\u0000\u0000wu\u0001\u0000\u0000\u0000x|\u0003"+
+ "\u001a\r\u0000y{\u0003\u001e\u000f\u0000zy\u0001\u0000\u0000\u0000{~\u0001"+
+ "\u0000\u0000\u0000|z\u0001\u0000\u0000\u0000|}\u0001\u0000\u0000\u0000"+
+ "}\u0082\u0001\u0000\u0000\u0000~|\u0001\u0000\u0000\u0000\u007f\u0082"+
+ "\u0003\u0018\f\u0000\u0080\u0082\u0003\u0016\u000b\u0000\u0081j\u0001"+
+ "\u0000\u0000\u0000\u0081q\u0001\u0000\u0000\u0000\u0081x\u0001\u0000\u0000"+
+ "\u0000\u0081\u007f\u0001\u0000\u0000\u0000\u0081\u0080\u0001\u0000\u0000"+
+ "\u0000\u0082\u0011\u0001\u0000\u0000\u0000\u0083\u0084\u0003\u0014\n\u0000"+
+ "\u0084\u0085\u0005\u0004\u0000\u0000\u0085\u0086\u0003\u001c\u000e\u0000"+
+ "\u0086\u0013\u0001\u0000\u0000\u0000\u0087\u0088\u0005\u001b\u0000\u0000"+
+ "\u0088\u0015\u0001\u0000\u0000\u0000\u0089\u008a\u0007\u0002\u0000\u0000"+
+ "\u008a\u0017\u0001\u0000\u0000\u0000\u008b\u008c\u0005\u001b\u0000\u0000"+
+ "\u008c\u008d\u0005\u0007\u0000\u0000\u008d\u008e\u0005\b\u0000\u0000\u008e"+
+ "\u0019\u0001\u0000\u0000\u0000\u008f\u0090\u0005\t\u0000\u0000\u0090\u0091"+
+ "\u0007\u0003\u0000\u0000\u0091\u001b\u0001\u0000\u0000\u0000\u0092\u0093"+
+ "\u0007\u0003\u0000\u0000\u0093\u001d\u0001\u0000\u0000\u0000\u0094\u0095"+
+ "\u0005\u0005\u0000\u0000\u0095\u0096\u0003 \u0010\u0000\u0096\u0097\u0005"+
+ "\u0006\u0000\u0000\u0097\u001f\u0001\u0000\u0000\u0000\u0098\u0099\u0003"+
+ "\"\u0011\u0000\u0099!\u0001\u0000\u0000\u0000\u009a\u009f\u0003$\u0012"+
+ "\u0000\u009b\u009c\u0005\u0016\u0000\u0000\u009c\u009e\u0003$\u0012\u0000"+
+ "\u009d\u009b\u0001\u0000\u0000\u0000\u009e\u00a1\u0001\u0000\u0000\u0000"+
+ "\u009f\u009d\u0001\u0000\u0000\u0000\u009f\u00a0\u0001\u0000\u0000\u0000"+
+ "\u00a0#\u0001\u0000\u0000\u0000\u00a1\u009f\u0001\u0000\u0000\u0000\u00a2"+
+ "\u00a7\u0003&\u0013\u0000\u00a3\u00a4\u0005\u0015\u0000\u0000\u00a4\u00a6"+
+ "\u0003&\u0013\u0000\u00a5\u00a3\u0001\u0000\u0000\u0000\u00a6\u00a9\u0001"+
+ "\u0000\u0000\u0000\u00a7\u00a5\u0001\u0000\u0000\u0000\u00a7\u00a8\u0001"+
+ "\u0000\u0000\u0000\u00a8%\u0001\u0000\u0000\u0000\u00a9\u00a7\u0001\u0000"+
+ "\u0000\u0000\u00aa\u00ab\u0003(\u0014\u0000\u00ab\u00ac\u0003\u0006\u0003"+
+ "\u0000\u00ac\u00ad\u0003\b\u0004\u0000\u00ad\u00b0\u0001\u0000\u0000\u0000"+
+ "\u00ae\u00b0\u0003(\u0014\u0000\u00af\u00aa\u0001\u0000\u0000\u0000\u00af"+
+ "\u00ae\u0001\u0000\u0000\u0000\u00b0\'\u0001\u0000\u0000\u0000\u00b1\u00b7"+
+ "\u0003*\u0015\u0000\u00b2\u00b7\u0003\u001a\r\u0000\u00b3\u00b7\u0003"+
+ "\f\u0006\u0000\u00b4\u00b7\u00030\u0018\u0000\u00b5\u00b7\u0005\u0014"+
+ "\u0000\u0000\u00b6\u00b1\u0001\u0000\u0000\u0000\u00b6\u00b2\u0001\u0000"+
+ "\u0000\u0000\u00b6\u00b3\u0001\u0000\u0000\u0000\u00b6\u00b4\u0001\u0000"+
+ "\u0000\u0000\u00b6\u00b5\u0001\u0000\u0000\u0000\u00b7)\u0001\u0000\u0000"+
+ "\u0000\u00b8\u00b9\u0005\u0017\u0000\u0000\u00b9\u00ba\u0005\u0007\u0000"+
+ "\u0000\u00ba\u00c5\u0005\b\u0000\u0000\u00bb\u00bc\u0005\u0018\u0000\u0000"+
+ "\u00bc\u00bd\u0005\u0007\u0000\u0000\u00bd\u00c5\u0005\b\u0000\u0000\u00be"+
+ "\u00bf\u0005\u001b\u0000\u0000\u00bf\u00c1\u0005\u0007\u0000\u0000\u00c0"+
+ "\u00c2\u0003,\u0016\u0000\u00c1\u00c0\u0001\u0000\u0000\u0000\u00c1\u00c2"+
+ "\u0001\u0000\u0000\u0000\u00c2\u00c3\u0001\u0000\u0000\u0000\u00c3\u00c5"+
+ "\u0005\b\u0000\u0000\u00c4\u00b8\u0001\u0000\u0000\u0000\u00c4\u00bb\u0001"+
+ "\u0000\u0000\u0000\u00c4\u00be\u0001\u0000\u0000\u0000\u00c5+\u0001\u0000"+
+ "\u0000\u0000\u00c6\u00cb\u0003.\u0017\u0000\u00c7\u00c8\u0005\f\u0000"+
+ "\u0000\u00c8\u00ca\u0003.\u0017\u0000\u00c9\u00c7\u0001\u0000\u0000\u0000"+
+ "\u00ca\u00cd\u0001\u0000\u0000\u0000\u00cb\u00c9\u0001\u0000\u0000\u0000"+
+ "\u00cb\u00cc\u0001\u0000\u0000\u0000\u00cc-\u0001\u0000\u0000\u0000\u00cd"+
+ "\u00cb\u0001\u0000\u0000\u0000\u00ce\u00d4\u0003\n\u0005\u0000\u00cf\u00d4"+
+ "\u0003*\u0015\u0000\u00d0\u00d4\u0003\f\u0006\u0000\u00d1\u00d4\u0003"+
+ "2\u0019\u0000\u00d2\u00d4\u0005\u0014\u0000\u0000\u00d3\u00ce\u0001\u0000"+
+ "\u0000\u0000\u00d3\u00cf\u0001\u0000\u0000\u0000\u00d3\u00d0\u0001\u0000"+
+ "\u0000\u0000\u00d3\u00d1\u0001\u0000\u0000\u0000\u00d3\u00d2\u0001\u0000"+
+ "\u0000\u0000\u00d4/\u0001\u0000\u0000\u0000\u00d5\u00d6\u0007\u0003\u0000"+
+ "\u0000\u00d61\u0001\u0000\u0000\u0000\u00d7\u00d8\u0005\u0019\u0000\u0000"+
+ "\u00d83\u0001\u0000\u0000\u0000\u00158=CHOUY]enu|\u0081\u009f\u00a7\u00af"+
+ "\u00b6\u00c1\u00c4\u00cb\u00d3";
+ public static final ATN _ATN =
+ new ATNDeserializer().deserialize(_serializedATN.toCharArray());
+ static {
+ _decisionToDFA = new DFA[_ATN.getNumberOfDecisions()];
+ for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) {
+ _decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i);
+ }
+ }
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.tokens b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.tokens
new file mode 100644
index 0000000000..68cbcd6cc2
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParser.tokens
@@ -0,0 +1,49 @@
+WS=1
+SLASH=2
+DOUBLE_SLASH=3
+AXIS_SEP=4
+LBRACKET=5
+RBRACKET=6
+LPAREN=7
+RPAREN=8
+AT=9
+DOTDOT=10
+DOT=11
+COMMA=12
+EQUALS=13
+NOT_EQUALS=14
+LTE=15
+GTE=16
+LT=17
+GT=18
+WILDCARD=19
+NUMBER=20
+AND=21
+OR=22
+LOCAL_NAME=23
+NAMESPACE_URI=24
+STRING_LITERAL=25
+QNAME=26
+NCNAME=27
+'/'=2
+'//'=3
+'::'=4
+'['=5
+']'=6
+'('=7
+')'=8
+'@'=9
+'..'=10
+'.'=11
+','=12
+'='=13
+'!='=14
+'<='=15
+'>='=16
+'<'=17
+'>'=18
+'*'=19
+'and'=21
+'or'=22
+'local-name'=23
+'namespace-uri'=24
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseListener.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseListener.java
new file mode 100644
index 0000000000..3082575b03
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseListener.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathParser.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+
+import org.antlr.v4.runtime.ParserRuleContext;
+import org.antlr.v4.runtime.tree.ErrorNode;
+import org.antlr.v4.runtime.tree.TerminalNode;
+
+/**
+ * This class provides an empty implementation of {@link XPathParserListener},
+ * which can be extended to create a listener which only needs to handle a subset
+ * of the available methods.
+ */
+@SuppressWarnings("CheckReturnValue")
+public class XPathParserBaseListener implements XPathParserListener {
+ /**
+ * {@inheritDoc}
+ *
+ *
The default implementation does nothing.
+ */
+ @Override public void enterXpathExpression(XPathParser.XpathExpressionContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitXpathExpression(XPathParser.XpathExpressionContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterFilterExpr(XPathParser.FilterExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitFilterExpr(XPathParser.FilterExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterBooleanExpr(XPathParser.BooleanExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitBooleanExpr(XPathParser.BooleanExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterComparisonOp(XPathParser.ComparisonOpContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitComparisonOp(XPathParser.ComparisonOpContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterComparand(XPathParser.ComparandContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitComparand(XPathParser.ComparandContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterPathSeparator(XPathParser.PathSeparatorContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitPathSeparator(XPathParser.PathSeparatorContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterStep(XPathParser.StepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitStep(XPathParser.StepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAxisStep(XPathParser.AxisStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAxisStep(XPathParser.AxisStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAxisName(XPathParser.AxisNameContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAxisName(XPathParser.AxisNameContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterNodeTypeTest(XPathParser.NodeTypeTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitNodeTypeTest(XPathParser.NodeTypeTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAttributeStep(XPathParser.AttributeStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAttributeStep(XPathParser.AttributeStepContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterNodeTest(XPathParser.NodeTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitNodeTest(XPathParser.NodeTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterPredicate(XPathParser.PredicateContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitPredicate(XPathParser.PredicateContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterPredicateExpr(XPathParser.PredicateExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitPredicateExpr(XPathParser.PredicateExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterOrExpr(XPathParser.OrExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitOrExpr(XPathParser.OrExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterAndExpr(XPathParser.AndExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitAndExpr(XPathParser.AndExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterPrimaryExpr(XPathParser.PrimaryExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitPrimaryExpr(XPathParser.PrimaryExprContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterPredicateValue(XPathParser.PredicateValueContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitPredicateValue(XPathParser.PredicateValueContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterFunctionCall(XPathParser.FunctionCallContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitFunctionCall(XPathParser.FunctionCallContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterFunctionArgs(XPathParser.FunctionArgsContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitFunctionArgs(XPathParser.FunctionArgsContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterFunctionArg(XPathParser.FunctionArgContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitFunctionArg(XPathParser.FunctionArgContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterChildElementTest(XPathParser.ChildElementTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitChildElementTest(XPathParser.ChildElementTestContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterStringLiteral(XPathParser.StringLiteralContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitStringLiteral(XPathParser.StringLiteralContext ctx) { }
+
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void enterEveryRule(ParserRuleContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void exitEveryRule(ParserRuleContext ctx) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void visitTerminal(TerminalNode node) { }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation does nothing.
+ */
+ @Override public void visitErrorNode(ErrorNode node) { }
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseVisitor.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseVisitor.java
new file mode 100644
index 0000000000..06243552d8
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserBaseVisitor.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathParser.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.tree.AbstractParseTreeVisitor;
+
+/**
+ * This class provides an empty implementation of {@link XPathParserVisitor},
+ * which can be extended to create a visitor which only needs to handle a subset
+ * of the available methods.
+ *
+ * @param The return type of the visit operation. Use {@link Void} for
+ * operations with no return type.
+ */
+@SuppressWarnings("CheckReturnValue")
+public class XPathParserBaseVisitor extends AbstractParseTreeVisitor implements XPathParserVisitor {
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitXpathExpression(XPathParser.XpathExpressionContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitFilterExpr(XPathParser.FilterExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitBooleanExpr(XPathParser.BooleanExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitComparisonOp(XPathParser.ComparisonOpContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitComparand(XPathParser.ComparandContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitPathSeparator(XPathParser.PathSeparatorContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitStep(XPathParser.StepContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAxisStep(XPathParser.AxisStepContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAxisName(XPathParser.AxisNameContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitNodeTypeTest(XPathParser.NodeTypeTestContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAttributeStep(XPathParser.AttributeStepContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitNodeTest(XPathParser.NodeTestContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitPredicate(XPathParser.PredicateContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitPredicateExpr(XPathParser.PredicateExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitOrExpr(XPathParser.OrExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitAndExpr(XPathParser.AndExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitPrimaryExpr(XPathParser.PrimaryExprContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitPredicateValue(XPathParser.PredicateValueContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitFunctionCall(XPathParser.FunctionCallContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitFunctionArgs(XPathParser.FunctionArgsContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitFunctionArg(XPathParser.FunctionArgContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitChildElementTest(XPathParser.ChildElementTestContext ctx) { return visitChildren(ctx); }
+ /**
+ * {@inheritDoc}
+ *
+ * The default implementation returns the result of calling
+ * {@link #visitChildren} on {@code ctx}.
+ */
+ @Override public T visitStringLiteral(XPathParser.StringLiteralContext ctx) { return visitChildren(ctx); }
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserListener.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserListener.java
new file mode 100644
index 0000000000..d7b5e6b45e
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserListener.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathParser.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.tree.ParseTreeListener;
+
+/**
+ * This interface defines a complete listener for a parse tree produced by
+ * {@link XPathParser}.
+ */
+public interface XPathParserListener extends ParseTreeListener {
+ /**
+ * Enter a parse tree produced by {@link XPathParser#xpathExpression}.
+ * @param ctx the parse tree
+ */
+ void enterXpathExpression(XPathParser.XpathExpressionContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#xpathExpression}.
+ * @param ctx the parse tree
+ */
+ void exitXpathExpression(XPathParser.XpathExpressionContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#filterExpr}.
+ * @param ctx the parse tree
+ */
+ void enterFilterExpr(XPathParser.FilterExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#filterExpr}.
+ * @param ctx the parse tree
+ */
+ void exitFilterExpr(XPathParser.FilterExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#booleanExpr}.
+ * @param ctx the parse tree
+ */
+ void enterBooleanExpr(XPathParser.BooleanExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#booleanExpr}.
+ * @param ctx the parse tree
+ */
+ void exitBooleanExpr(XPathParser.BooleanExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#comparisonOp}.
+ * @param ctx the parse tree
+ */
+ void enterComparisonOp(XPathParser.ComparisonOpContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#comparisonOp}.
+ * @param ctx the parse tree
+ */
+ void exitComparisonOp(XPathParser.ComparisonOpContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#comparand}.
+ * @param ctx the parse tree
+ */
+ void enterComparand(XPathParser.ComparandContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#comparand}.
+ * @param ctx the parse tree
+ */
+ void exitComparand(XPathParser.ComparandContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#absoluteLocationPath}.
+ * @param ctx the parse tree
+ */
+ void enterAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#absoluteLocationPath}.
+ * @param ctx the parse tree
+ */
+ void exitAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#relativeLocationPath}.
+ * @param ctx the parse tree
+ */
+ void enterRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#relativeLocationPath}.
+ * @param ctx the parse tree
+ */
+ void exitRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#pathSeparator}.
+ * @param ctx the parse tree
+ */
+ void enterPathSeparator(XPathParser.PathSeparatorContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#pathSeparator}.
+ * @param ctx the parse tree
+ */
+ void exitPathSeparator(XPathParser.PathSeparatorContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#step}.
+ * @param ctx the parse tree
+ */
+ void enterStep(XPathParser.StepContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#step}.
+ * @param ctx the parse tree
+ */
+ void exitStep(XPathParser.StepContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#axisStep}.
+ * @param ctx the parse tree
+ */
+ void enterAxisStep(XPathParser.AxisStepContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#axisStep}.
+ * @param ctx the parse tree
+ */
+ void exitAxisStep(XPathParser.AxisStepContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#axisName}.
+ * @param ctx the parse tree
+ */
+ void enterAxisName(XPathParser.AxisNameContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#axisName}.
+ * @param ctx the parse tree
+ */
+ void exitAxisName(XPathParser.AxisNameContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#abbreviatedStep}.
+ * @param ctx the parse tree
+ */
+ void enterAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#abbreviatedStep}.
+ * @param ctx the parse tree
+ */
+ void exitAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#nodeTypeTest}.
+ * @param ctx the parse tree
+ */
+ void enterNodeTypeTest(XPathParser.NodeTypeTestContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#nodeTypeTest}.
+ * @param ctx the parse tree
+ */
+ void exitNodeTypeTest(XPathParser.NodeTypeTestContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#attributeStep}.
+ * @param ctx the parse tree
+ */
+ void enterAttributeStep(XPathParser.AttributeStepContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#attributeStep}.
+ * @param ctx the parse tree
+ */
+ void exitAttributeStep(XPathParser.AttributeStepContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#nodeTest}.
+ * @param ctx the parse tree
+ */
+ void enterNodeTest(XPathParser.NodeTestContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#nodeTest}.
+ * @param ctx the parse tree
+ */
+ void exitNodeTest(XPathParser.NodeTestContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#predicate}.
+ * @param ctx the parse tree
+ */
+ void enterPredicate(XPathParser.PredicateContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#predicate}.
+ * @param ctx the parse tree
+ */
+ void exitPredicate(XPathParser.PredicateContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#predicateExpr}.
+ * @param ctx the parse tree
+ */
+ void enterPredicateExpr(XPathParser.PredicateExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#predicateExpr}.
+ * @param ctx the parse tree
+ */
+ void exitPredicateExpr(XPathParser.PredicateExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#orExpr}.
+ * @param ctx the parse tree
+ */
+ void enterOrExpr(XPathParser.OrExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#orExpr}.
+ * @param ctx the parse tree
+ */
+ void exitOrExpr(XPathParser.OrExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#andExpr}.
+ * @param ctx the parse tree
+ */
+ void enterAndExpr(XPathParser.AndExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#andExpr}.
+ * @param ctx the parse tree
+ */
+ void exitAndExpr(XPathParser.AndExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#primaryExpr}.
+ * @param ctx the parse tree
+ */
+ void enterPrimaryExpr(XPathParser.PrimaryExprContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#primaryExpr}.
+ * @param ctx the parse tree
+ */
+ void exitPrimaryExpr(XPathParser.PrimaryExprContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#predicateValue}.
+ * @param ctx the parse tree
+ */
+ void enterPredicateValue(XPathParser.PredicateValueContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#predicateValue}.
+ * @param ctx the parse tree
+ */
+ void exitPredicateValue(XPathParser.PredicateValueContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#functionCall}.
+ * @param ctx the parse tree
+ */
+ void enterFunctionCall(XPathParser.FunctionCallContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#functionCall}.
+ * @param ctx the parse tree
+ */
+ void exitFunctionCall(XPathParser.FunctionCallContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#functionArgs}.
+ * @param ctx the parse tree
+ */
+ void enterFunctionArgs(XPathParser.FunctionArgsContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#functionArgs}.
+ * @param ctx the parse tree
+ */
+ void exitFunctionArgs(XPathParser.FunctionArgsContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#functionArg}.
+ * @param ctx the parse tree
+ */
+ void enterFunctionArg(XPathParser.FunctionArgContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#functionArg}.
+ * @param ctx the parse tree
+ */
+ void exitFunctionArg(XPathParser.FunctionArgContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#childElementTest}.
+ * @param ctx the parse tree
+ */
+ void enterChildElementTest(XPathParser.ChildElementTestContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#childElementTest}.
+ * @param ctx the parse tree
+ */
+ void exitChildElementTest(XPathParser.ChildElementTestContext ctx);
+ /**
+ * Enter a parse tree produced by {@link XPathParser#stringLiteral}.
+ * @param ctx the parse tree
+ */
+ void enterStringLiteral(XPathParser.StringLiteralContext ctx);
+ /**
+ * Exit a parse tree produced by {@link XPathParser#stringLiteral}.
+ * @param ctx the parse tree
+ */
+ void exitStringLiteral(XPathParser.StringLiteralContext ctx);
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserVisitor.java b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserVisitor.java
new file mode 100644
index 0000000000..f130256329
--- /dev/null
+++ b/rewrite-xml/src/main/java/org/openrewrite/xml/internal/grammar/XPathParserVisitor.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Generated from /Users/knut/git/openrewrite/rewrite/rewrite-xml/src/main/antlr/XPathParser.g4 by ANTLR 4.13.2
+package org.openrewrite.xml.internal.grammar;
+import org.antlr.v4.runtime.tree.ParseTreeVisitor;
+
+/**
+ * This interface defines a complete generic visitor for a parse tree produced
+ * by {@link XPathParser}.
+ *
+ * @param The return type of the visit operation. Use {@link Void} for
+ * operations with no return type.
+ */
+public interface XPathParserVisitor extends ParseTreeVisitor {
+ /**
+ * Visit a parse tree produced by {@link XPathParser#xpathExpression}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitXpathExpression(XPathParser.XpathExpressionContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#filterExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitFilterExpr(XPathParser.FilterExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#booleanExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitBooleanExpr(XPathParser.BooleanExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#comparisonOp}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitComparisonOp(XPathParser.ComparisonOpContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#comparand}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitComparand(XPathParser.ComparandContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#absoluteLocationPath}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAbsoluteLocationPath(XPathParser.AbsoluteLocationPathContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#relativeLocationPath}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitRelativeLocationPath(XPathParser.RelativeLocationPathContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#pathSeparator}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitPathSeparator(XPathParser.PathSeparatorContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#step}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitStep(XPathParser.StepContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#axisStep}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAxisStep(XPathParser.AxisStepContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#axisName}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAxisName(XPathParser.AxisNameContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#abbreviatedStep}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAbbreviatedStep(XPathParser.AbbreviatedStepContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#nodeTypeTest}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitNodeTypeTest(XPathParser.NodeTypeTestContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#attributeStep}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAttributeStep(XPathParser.AttributeStepContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#nodeTest}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitNodeTest(XPathParser.NodeTestContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#predicate}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitPredicate(XPathParser.PredicateContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#predicateExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitPredicateExpr(XPathParser.PredicateExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#orExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitOrExpr(XPathParser.OrExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#andExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitAndExpr(XPathParser.AndExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#primaryExpr}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitPrimaryExpr(XPathParser.PrimaryExprContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#predicateValue}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitPredicateValue(XPathParser.PredicateValueContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#functionCall}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitFunctionCall(XPathParser.FunctionCallContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#functionArgs}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitFunctionArgs(XPathParser.FunctionArgsContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#functionArg}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitFunctionArg(XPathParser.FunctionArgContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#childElementTest}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitChildElementTest(XPathParser.ChildElementTestContext ctx);
+ /**
+ * Visit a parse tree produced by {@link XPathParser#stringLiteral}.
+ * @param ctx the parse tree
+ * @return the visitor result
+ */
+ T visitStringLiteral(XPathParser.StringLiteralContext ctx);
+}
\ No newline at end of file
diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/AddCommentToXmlTagTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/AddCommentToXmlTagTest.java
index 9d3622c3dd..8dce9772b8 100755
--- a/rewrite-xml/src/test/java/org/openrewrite/xml/AddCommentToXmlTagTest.java
+++ b/rewrite-xml/src/test/java/org/openrewrite/xml/AddCommentToXmlTagTest.java
@@ -30,7 +30,7 @@ void addCommentToDependencyBlock() {
rewriteRun(
spec -> spec.recipe(
new AddCommentToXmlTag(
- "/project/dependencies/",
+ "/project/dependencies",
" Comment text "
)
),
diff --git a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
index 006668c71c..74d6512aab 100755
--- a/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
+++ b/rewrite-xml/src/test/java/org/openrewrite/xml/XPathMatcherTest.java
@@ -15,16 +15,13 @@
*/
package org.openrewrite.xml;
-import org.junit.jupiter.api.Disabled;
+import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.Test;
-import org.openrewrite.ExecutionContext;
import org.openrewrite.Issue;
import org.openrewrite.SourceFile;
-import org.openrewrite.TreeVisitor;
-import org.openrewrite.marker.SearchResult;
import org.openrewrite.xml.tree.Xml;
-import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
@@ -115,51 +112,48 @@ class XPathMatcherTest {
@Test
void matchAbsolute() {
- assertThat(match("/dependencies/dependency", xmlDoc)).isTrue();
- assertThat(match("/dependencies/*/artifactId", xmlDoc)).isTrue();
- assertThat(match("/dependencies/*", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency//groupId", xmlDoc)).isTrue();
+ assertThat(matchCount("/dependencies/dependency", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/*/artifactId", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/*", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies//dependency", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies//dependency//groupId", xmlDoc)).isEqualTo(1);
// negative matches
- assertThat(match("/dependencies/dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dependency//dne", xmlDoc)).isFalse();
+ assertThat(matchCount("/dependencies/dne", xmlDoc)).isEqualTo(0);
+ assertThat(matchCount("/dependencies//dne", xmlDoc)).isEqualTo(0);
+ assertThat(matchCount("/dependencies//dependency//dne", xmlDoc)).isEqualTo(0);
}
@Test
void matchAbsoluteAttribute() {
- assertThat(match("/dependencies/dependency/artifactId/@scope", xmlDoc)).isTrue();
- assertThat(match("/dependencies/dependency/artifactId/@scope", xmlDoc)).isTrue();
- assertThat(match("/dependencies/dependency/artifactId/@*", xmlDoc)).isTrue();
- assertThat(match("/dependencies/dependency/groupId/@*", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dependency//@scope", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency//artifactId//@scope", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency//@*", xmlDoc)).isTrue();
- assertThat(match("/dependencies//dependency//artifactId//@*", xmlDoc)).isTrue();
+ assertThat(matchCount("/dependencies/dependency/artifactId/@scope", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/dependency/artifactId/@*", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/dependency/groupId/@*", xmlDoc)).isEqualTo(0);
+ assertThat(matchCount("/dependencies//dependency//@scope", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies//dependency//artifactId//@scope", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies//dependency//@*", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies//dependency//artifactId//@*", xmlDoc)).isEqualTo(2);
// negative matches
- assertThat(match("/dependencies/dependency/artifactId/@dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies/dependency/artifactId/@dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dependency//@dne", xmlDoc)).isFalse();
- assertThat(match("/dependencies//dependency//artifactId//@dne", xmlDoc)).isFalse();
-
+ assertThat(matchCount("/dependencies/dependency/artifactId/@dne", xmlDoc)).isEqualTo(0);
+ assertThat(matchCount("/dependencies//dependency//@dne", xmlDoc)).isEqualTo(0);
+ assertThat(matchCount("/dependencies//dependency//artifactId//@dne", xmlDoc)).isEqualTo(0);
}
@Test
void matchRelative() {
- assertThat(match("dependencies", xmlDoc)).isTrue();
- assertThat(match("dependency", xmlDoc)).isTrue();
- assertThat(match("//dependency", xmlDoc)).isTrue();
- assertThat(match("dependency/*", xmlDoc)).isTrue();
- assertThat(match("dne", xmlDoc)).isFalse();
+ assertThat(matchCount("dependencies", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("dependency", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("//dependency", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("dependency/*", xmlDoc)).isEqualTo(3); // 1 groupId + 2 artifactId
+ assertThat(matchCount("dne", xmlDoc)).isEqualTo(0);
}
@Test
void matchRelativeAttribute() {
- assertThat(match("dependency/artifactId/@scope", xmlDoc)).isTrue();
- assertThat(match("dependency/artifactId/@*", xmlDoc)).isTrue();
- assertThat(match("//dependency/artifactId/@scope", xmlDoc)).isTrue();
+ assertThat(matchCount("dependency/artifactId/@scope", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("dependency/artifactId/@*", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("//dependency/artifactId/@scope", xmlDoc)).isEqualTo(2);
}
@Test
@@ -308,18 +302,176 @@ void namespaceMatchFunctions() {
}
@Test
- @Disabled
- void otherUncoveredXpathFunctions() {
- // Other common XPath functions
- assertThat(match("contains(/root/element1, 'content1')", namespacedXml)).isTrue();
- assertThat(match("not(contains(/root/element1, 'content1'))", namespacedXml)).isFalse();
- assertThat(match("string-length(/root/element1) > 2", namespacedXml)).isTrue();
- assertThat(match("starts-with(/root/element1, 'content1')", namespacedXml)).isTrue();
- assertThat(match("ends-with(/root/element1, 'content1')", namespacedXml)).isTrue();
- assertThat(match("substring-before(/root/element1, '1') = 'content'", namespacedXml)).isTrue();
- assertThat(match("substring-after(/root/element1, 'content') = '1'", namespacedXml)).isTrue();
- assertThat(match("/root/element1/text()", namespacedXml)).isTrue();
- assertThat(match("count(/root/*)", namespacedXml)).isTrue();
+ void matchTextNodeTypeTest() {
+ // text() node type test works in path steps
+ assertThat(match("/root/element1/text()", namespacedXml)).isTrue();
+ assertThat(match("//element1/text()", namespacedXml)).isTrue();
+ assertThat(match("/root/ns2:element2/text()", namespacedXml)).isTrue();
+ // parent has child elements, not direct text content
+ assertThat(match("/root/parent/text()", namespacedXml)).isFalse();
+ }
+
+ @Test
+ void matchContainsFunction() {
+ // Basic contains - positive cases
+ assertThat(match("contains(/root/element1, 'content1')", namespacedXml)).isTrue();
+ assertThat(match("contains(/root/element1, 'content')", namespacedXml)).isTrue();
+ assertThat(match("contains(/root/element1, '1')", namespacedXml)).isTrue();
+ assertThat(match("contains(/root/element1, 'ent')", namespacedXml)).isTrue();
+
+ // Basic contains - negative cases
+ assertThat(match("contains(/root/element1, 'notfound')", namespacedXml)).isFalse();
+ assertThat(match("contains(/root/element1, 'CONTENT1')", namespacedXml)).isFalse(); // case sensitive
+ assertThat(match("contains(/root/element1, '')", namespacedXml)).isTrue(); // empty string is always contained
+
+ // Contains with different elements
+ assertThat(match("contains(/root/ns2:element2, 'content2')", namespacedXml)).isTrue();
+ assertThat(match("contains(/root/parent/element3, 'content3')", namespacedXml)).isTrue();
+
+ // Contains with non-existent path
+ assertThat(match("contains(/root/nonexistent, 'anything')", namespacedXml)).isFalse();
+ }
+
+ @Test
+ void matchContainsInPredicate() {
+ // contains() in predicate with child element - matches dependency with groupId containing 'openrewrite'
+ assertThat(match("/dependencies/dependency[contains(groupId, 'openrewrite')]", xmlDoc)).isTrue();
+ assertThat(match("/dependencies/dependency[contains(groupId, 'rewrite')]", xmlDoc)).isTrue();
+ assertThat(match("/dependencies/dependency[contains(artifactId, 'rewrite')]", xmlDoc)).isTrue();
+
+ // negative cases
+ assertThat(match("/dependencies/dependency[contains(groupId, 'notfound')]", xmlDoc)).isFalse();
+ assertThat(match("/dependencies/dependency[contains(artifactId, 'notfound')]", xmlDoc)).isFalse();
+
+ // with descendant axis
+ assertThat(match("//dependency[contains(groupId, 'openrewrite')]", xmlDoc)).isTrue();
+ assertThat(match("//dependency[contains(artifactId, 'xml')]", xmlDoc)).isTrue();
+ }
+
+ @Test
+ void matchNotFunction() {
+ // not() with contains
+ assertThat(match("not(contains(/root/element1, 'content1'))", namespacedXml)).isFalse();
+ assertThat(match("not(contains(/root/element1, 'notfound'))", namespacedXml)).isTrue();
+
+ // not() with starts-with
+ assertThat(match("not(starts-with(/root/element1, 'content'))", namespacedXml)).isFalse();
+ assertThat(match("not(starts-with(/root/element1, 'xyz'))", namespacedXml)).isTrue();
+
+ // Double negation
+ assertThat(match("not(not(contains(/root/element1, 'content1')))", namespacedXml)).isTrue();
+ }
+
+ @Test
+ void matchStringLengthFunction() {
+ // string-length with comparisons
+ assertThat(match("string-length(/root/element1) > 0", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) > 2", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) > 100", namespacedXml)).isFalse();
+
+ assertThat(match("string-length(/root/element1) < 100", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) < 5", namespacedXml)).isFalse();
+
+ assertThat(match("string-length(/root/element1) = 8", namespacedXml)).isTrue(); // "content1" = 8 chars
+ assertThat(match("string-length(/root/element1) = 7", namespacedXml)).isFalse();
+
+ assertThat(match("string-length(/root/element1) >= 8", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) >= 9", namespacedXml)).isFalse();
+
+ assertThat(match("string-length(/root/element1) <= 8", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) <= 7", namespacedXml)).isFalse();
+
+ assertThat(match("string-length(/root/element1) != 7", namespacedXml)).isTrue();
+ assertThat(match("string-length(/root/element1) != 8", namespacedXml)).isFalse();
+
+ // string-length of non-existent path
+ assertThat(match("string-length(/root/nonexistent) = 0", namespacedXml)).isTrue();
+ }
+
+ @Test
+ void matchStartsWithFunction() {
+ assertThat(match("starts-with(/root/element1, 'content')", namespacedXml)).isTrue();
+ assertThat(match("starts-with(/root/element1, 'con')", namespacedXml)).isTrue();
+ assertThat(match("starts-with(/root/element1, 'c')", namespacedXml)).isTrue();
+ assertThat(match("starts-with(/root/element1, '')", namespacedXml)).isTrue();
+ assertThat(match("starts-with(/root/element1, 'content1')", namespacedXml)).isTrue();
+
+ assertThat(match("starts-with(/root/element1, 'ontent')", namespacedXml)).isFalse();
+ assertThat(match("starts-with(/root/element1, '1')", namespacedXml)).isFalse();
+ assertThat(match("starts-with(/root/element1, 'Content')", namespacedXml)).isFalse(); // case sensitive
+ }
+
+ @Test
+ void matchEndsWithFunction() {
+ assertThat(match("ends-with(/root/element1, '1')", namespacedXml)).isTrue();
+ assertThat(match("ends-with(/root/element1, 'ent1')", namespacedXml)).isTrue();
+ assertThat(match("ends-with(/root/element1, 'content1')", namespacedXml)).isTrue();
+ assertThat(match("ends-with(/root/element1, '')", namespacedXml)).isTrue();
+
+ assertThat(match("ends-with(/root/element1, 'content')", namespacedXml)).isFalse();
+ assertThat(match("ends-with(/root/element1, '2')", namespacedXml)).isFalse();
+ assertThat(match("ends-with(/root/element1, 'Content1')", namespacedXml)).isFalse(); // case sensitive
+ }
+
+ @Test
+ void matchSubstringFunctions() {
+ // substring-before
+ assertThat(match("substring-before(/root/element1, '1') = 'content'", namespacedXml)).isTrue();
+ assertThat(match("substring-before(/root/element1, 'tent') = 'con'", namespacedXml)).isTrue();
+ assertThat(match("substring-before(/root/element1, 'c') = ''", namespacedXml)).isTrue(); // nothing before first char
+ assertThat(match("substring-before(/root/element1, 'notfound') = ''", namespacedXml)).isTrue(); // delimiter not found
+
+ // substring-after
+ assertThat(match("substring-after(/root/element1, 'content') = '1'", namespacedXml)).isTrue();
+ assertThat(match("substring-after(/root/element1, 'con') = 'tent1'", namespacedXml)).isTrue();
+ assertThat(match("substring-after(/root/element1, '1') = ''", namespacedXml)).isTrue(); // nothing after last char
+ assertThat(match("substring-after(/root/element1, 'notfound') = ''", namespacedXml)).isTrue(); // delimiter not found
+ }
+
+ @Test
+ void matchCountFunction() {
+ // TODO: count() with wildcard paths like count(/root/*) requires special handling
+ // to count all matching nodes rather than evaluating path to text content.
+ // For now, count() with a path that has text content returns 1 (truthy)
+ assertThat(match("count(/root/element1)", namespacedXml)).isTrue(); // has content, so count >= 1
+ assertThat(match("count(/root/element1) > 0", namespacedXml)).isTrue();
+
+ // count of non-existent returns 0
+ assertThat(match("count(/root/nonexistent) = 0", namespacedXml)).isTrue();
+ }
+
+ @Test
+ void matchNestedFunctionCalls() {
+ // Nested function calls
+ assertThat(match("not(not(contains(/root/element1, 'content1')))", namespacedXml)).isTrue();
+ assertThat(match("not(not(not(contains(/root/element1, 'content1'))))", namespacedXml)).isFalse();
+
+ // contains with substring result
+ assertThat(match("contains(substring-after(/root/element1, 'con'), 'tent')", namespacedXml)).isTrue();
+ }
+
+ @Test
+ void matchFunctionWithDescendantPath() {
+ // Using // descendant axis in function arguments
+ assertThat(match("contains(//element1, 'content1')", namespacedXml)).isTrue();
+ assertThat(match("contains(//element3, 'content3')", namespacedXml)).isTrue();
+ assertThat(match("string-length(//element1) = 8", namespacedXml)).isTrue();
+ }
+
+ @Test
+ void matchBooleanExpressionWithRelativePath() {
+ // Relative paths in boolean expressions are evaluated from the cursor's context element
+ // contains(element1, 'content1') matches at because it has a child containing 'content1'
+ assertThat(matchCount("contains(element1, 'content1')", namespacedXml)).isEqualTo(1); // matches at
+
+ // xmlDoc: org.openrewrite......
+ // contains(groupId, 'openrewrite') matches at elements that have child containing 'openrewrite'
+ assertThat(matchCount("contains(groupId, 'openrewrite')", xmlDoc)).isEqualTo(1); // matches at first
+ assertThat(matchCount("contains(groupId, 'notfound')", xmlDoc)).isEqualTo(0); // no matches
+
+ // Absolute paths: only match at root element
+ // contains(/dependencies/dependency/groupId, 'openrewrite') - matches only at (root)
+ assertThat(matchCount("contains(/dependencies/dependency/groupId, 'openrewrite')", xmlDoc)).isEqualTo(1);
}
@Test
@@ -421,12 +573,24 @@ void matchTextFunctionCondition() {
bar
notBar
+
+ two
+
+
+
+ three
+
+
"""
).toList().getFirst();
// text() predicate should only match element with specific text content
+ assertThat(match("/test/one[two/three/text()='three']", xml)).isTrue();
+ assertThat(match("/test/one[two/three/text()='notthree']", xml)).isFalse();
+ assertThat(match("/test/one[two/text()='two']", xml)).isTrue();
+ assertThat(match("/test/one[two/text()='nottwo']", xml)).isFalse();
assertThat(match("/test/foo[text()='bar']", xml)).isTrue();
assertThat(match("/test/foo[text()='notBar']", xml)).isTrue();
assertThat(match("/test/foo[text()='nonexistent']", xml)).isFalse();
@@ -503,24 +667,209 @@ void matchConditionsWithConjunctions() {
assertThat(match("//*[local-name()='element4' or local-name()='dne' and namespace-uri()='http://www.example.com/namespaceX']", namespacedXml)).isTrue();
}
- private boolean match(String xpath, SourceFile x) {
+ private boolean match(@Language("xpath") String xpath, SourceFile x) {
+ return matchCount(xpath, x) > 0;
+ }
+
+ private int matchCount(String xpath, SourceFile x) {
XPathMatcher matcher = new XPathMatcher(xpath);
- return !TreeVisitor.collect(new XmlVisitor<>() {
+ return (new XmlVisitor() {
@Override
- public Xml visitTag(Xml.Tag tag, ExecutionContext ctx) {
+ public Xml visitTag(Xml.Tag tag, AtomicInteger ctx) {
if (matcher.matches(getCursor())) {
- return SearchResult.found(tag);
+ ctx.incrementAndGet();
}
return super.visitTag(tag, ctx);
}
@Override
- public Xml visitAttribute(Xml.Attribute attribute, ExecutionContext ctx) {
+ public Xml visitAttribute(Xml.Attribute attribute, AtomicInteger ctx) {
if (matcher.matches(getCursor())) {
- return SearchResult.found(attribute);
+ ctx.incrementAndGet();
}
return super.visitAttribute(attribute, ctx);
}
- }, x, new ArrayList<>()).isEmpty();
+ }).reduce(x, new AtomicInteger()).get();
+ }
+
+ @Test
+ void matchNodeTypeTests() {
+ // text() node type test - matches elements with text content
+ assertThat(match("/root/element1/text()", namespacedXml)).isTrue();
+ assertThat(match("//element1/text()", namespacedXml)).isTrue();
+ assertThat(match("/root/parent/text()", namespacedXml)).isFalse(); // parent has child elements, not direct text
+ }
+
+ @Test
+ void matchPositionalPredicates() {
+ // xmlDoc has two elements under
+ // [1] selects the first dependency
+ assertThat(matchCount("/dependencies/dependency[1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[2]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[3]", xmlDoc)).isEqualTo(0); // only 2 dependencies
+
+ // [last()] selects the last element
+ assertThat(matchCount("/dependencies/dependency[last()]", xmlDoc)).isEqualTo(1);
+
+ // position() function
+ assertThat(matchCount("/dependencies/dependency[position()=1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[position()=2]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[position()=3]", xmlDoc)).isEqualTo(0);
+
+ // Combining positional with other predicates
+ // The first dependency has groupId, the second doesn't
+ assertThat(matchCount("/dependencies/dependency[1]/groupId", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[2]/groupId", xmlDoc)).isEqualTo(0);
+
+ // position() with comparison operators
+ assertThat(matchCount("/dependencies/dependency[position()>1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[position()<2]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[position()>=1]", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/dependency[position()<=2]", xmlDoc)).isEqualTo(2);
+ }
+
+ @Test
+ void matchPositionalWithOtherConditions() {
+ // Combining positional predicates with attribute/element conditions
+ // First dependency's artifactId has scope="compile", second has scope="test"
+ assertThat(matchCount("/dependencies/dependency[1]/artifactId[@scope='compile']", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[2]/artifactId[@scope='test']", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("/dependencies/dependency[1]/artifactId[@scope='test']", xmlDoc)).isEqualTo(0);
+ }
+
+ @Test
+ void matchParenthesizedPathExpressions() {
+ // Parenthesized path expressions apply predicates to the entire result set
+ // (/dependencies/dependency)[1] - first dependency from the entire document
+ assertThat(matchCount("(/dependencies/dependency)[1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("(/dependencies/dependency)[2]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("(/dependencies/dependency)[3]", xmlDoc)).isEqualTo(0);
+
+ // (/dependencies/dependency)[last()] - last dependency
+ assertThat(matchCount("(/dependencies/dependency)[last()]", xmlDoc)).isEqualTo(1);
+
+ // With position() function
+ assertThat(matchCount("(/dependencies/dependency)[position()=1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("(/dependencies/dependency)[position()=2]", xmlDoc)).isEqualTo(1);
+
+ // Descendant axis in parenthesized expression
+ assertThat(matchCount("(//dependency)[1]", xmlDoc)).isEqualTo(1);
+ assertThat(matchCount("(//dependency)[last()]", xmlDoc)).isEqualTo(1);
+ }
+
+ @Test
+ void matchAdvancedFilterExpressions() {
+ // (/path/expr)[predicate]/trailing - filter expression with trailing path
+ assertThat(matchCount("(/project/build/plugins/plugin)[1]/groupId", pomXml1)).isEqualTo(1);
+ assertThat(matchCount("(/project/build/plugins/plugin)[1]/artifactId", pomXml1)).isEqualTo(1);
+ assertThat(matchCount("(/project/build/plugins/plugin)[1]/configuration", pomXml1)).isEqualTo(1);
+ assertThat(matchCount("(/project/build/plugins/plugin)[1]/configuration/source", pomXml1)).isEqualTo(1);
+
+ // Test with attribute access in trailing path (not yet supported)
+ // assertThat(matchCount("(/dependencies/dependency)[1]/artifactId/@scope", xmlDoc)).isEqualTo(1);
+ }
+
+ @Test
+ void matchAbbreviatedSyntax() {
+ // . means self (current node)
+ assertThat(matchCount("/dependencies/./dependency", xmlDoc)).isEqualTo(2);
+ assertThat(matchCount("/dependencies/dependency/.", xmlDoc)).isEqualTo(2);
+
+ // .. means parent
+ assertThat(matchCount("/dependencies/dependency/groupId/..", xmlDoc)).isEqualTo(1); // only first dependency has groupId
+ assertThat(matchCount("/dependencies/dependency/groupId/../artifactId", xmlDoc)).isEqualTo(1);
+
+ // Multiple parent references
+ assertThat(matchCount("/dependencies/dependency/groupId/../../dependency", xmlDoc)).isEqualTo(2);
+ }
+
+ @Test
+ void matchParentAxis() {
+ // parent::node() - explicit parent axis with node() test
+ assertThat(match("/dependencies/dependency/groupId/parent::node()", xmlDoc)).isTrue();
+ assertThat(match("/dependencies/dependency/groupId/parent::dependency", xmlDoc)).isTrue();
+
+ // parent with specific element name
+ assertThat(match("/dependencies/dependency/parent::dependencies", xmlDoc)).isTrue();
+
+ // self::node() - explicit self axis
+ assertThat(match("/dependencies/self::node()", xmlDoc)).isTrue();
+ assertThat(match("/dependencies/self::dependencies", xmlDoc)).isTrue();
+
+ // Combined with other path steps
+ assertThat(match("/dependencies/dependency/groupId/parent::dependency/artifactId", xmlDoc)).isTrue();
+ }
+
+ @Test
+ void matchDescendantWithChildPath() {
+ // //plugins/plugin - descendant axis followed by child axis
+ // This is used by MavenPlugin matcher
+ assertThat(match("//plugins/plugin", pomXml1)).isTrue();
+ assertThat(match("//plugins/plugin/groupId", pomXml1)).isTrue();
+ assertThat(match("//plugins/plugin/artifactId", pomXml1)).isTrue();
+ assertThat(match("//plugins/plugin/configuration", pomXml1)).isTrue();
+ assertThat(match("//plugins/plugin/configuration/source", pomXml1)).isTrue();
+
+ // With pluginManagement
+ assertThat(match("//plugins/plugin", pomXml2)).isTrue();
+ assertThat(match("//pluginManagement/plugins/plugin", pomXml2)).isTrue();
+
+ // Negative cases
+ assertThat(match("//plugins/nonexistent", pomXml1)).isFalse();
+ assertThat(match("//nonexistent/plugin", pomXml1)).isFalse();
+ }
+
+ @Test
+ void matchDescendantStartingWithRootElement() {
+ // //project/build/... - descendant axis where first step matches root element
+ // This pattern is used by RemoveXmlTag and should match when root is
+ assertThat(match("//project/build", pomXml1)).isTrue();
+ assertThat(match("//project/build/plugins", pomXml1)).isTrue();
+ assertThat(match("//project/build/plugins/plugin", pomXml1)).isTrue();
+ assertThat(match("//project/build/pluginManagement/plugins/plugin", pomXml2)).isTrue();
+
+ // Should not match if the path doesn't exist
+ assertThat(match("//project/build/pluginManagement", pomXml1)).isFalse();
+ assertThat(match("//project/nonexistent", pomXml1)).isFalse();
}
+
+ @Test
+ void matchRootElement() {
+ // Single-step absolute path should match the root element
+ assertThat(matchCount("/project", pomXml1)).isEqualTo(1);
+ assertThat(matchCount("/dependencies", xmlDoc)).isEqualTo(1);
+
+ // /project/parent should match the parent element
+ SourceFile pomWithParent = new XmlParser().parse(
+ """
+
+
+ com.example
+
+ 1.0
+
+ """
+ ).toList().getFirst();
+ assertThat(matchCount("/project/parent", pomWithParent)).isEqualTo(1);
+ assertThat(matchCount("/project", pomWithParent)).isEqualTo(1);
+ }
+
+ @Test
+ void matchRelativePathFromContext() {
+ // Relative paths should match based on suffix, allowing match anywhere in document
+ // This is important for ChangeTagValue which uses paths like "version"
+ assertThat(match("version", pomXml1)).isTrue();
+ assertThat(match("groupId", pomXml1)).isTrue();
+ assertThat(match("artifactId", pomXml1)).isTrue();
+
+ // Multi-step relative paths
+ assertThat(match("configuration/source", pomXml1)).isTrue();
+ assertThat(match("plugin/configuration", pomXml1)).isTrue();
+ assertThat(match("plugins/plugin", pomXml1)).isTrue();
+
+ // Negative cases - element names that don't exist
+ assertThat(match("nonexistent", pomXml1)).isFalse();
+ assertThat(match("configuration/nonexistent", pomXml1)).isFalse();
+ }
+
}