From 62773357dd6781fd9fdfca8f8e785172401f5711 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Sat, 12 Sep 2026 07:43:52 +0000 Subject: [PATCH] fix: span a binary condition's node from its left operand Seven productions take their left operand as a parameter, so the caller has already consumed those tokens when JJTree opens the node and the range starts at the operator. A node for `a IN (1, 2)` covered only `IN (1, 2)`, and one for `a = 1` only `= 1`, leaving anyone reading `jjtGetFirstToken()` a range that excludes the operand the condition is about. Comparisons are a regression from 1b51f05d, which moved the left operand out of RegularCondition and into the caller as part of the Pratt refactor. IN, LIKE, SIMILAR TO and IS DISTINCT have taken their operand as a parameter for far longer and were already reporting the narrow range, so before that refactor the two halves of the family disagreed with each other. Link those nodes through an overload that starts the range at the left operand, which fixes the regression and makes the family consistent. testDetectInExpressions asserted the narrow range and now expects the wider one. Between, IsNullExpression, IsBooleanExpression, IsUnknownExpression, MemberOfExpression and OverlapsCondition also take a left operand but build no node at all, so they keep returning null and are left alone here. --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 29 ++++++++++++++----- .../statement/select/SelectASTTest.java | 25 +++++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 52afbaddf..37c28ae23 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -150,6 +150,21 @@ public class CCJSqlParser extends AbstractJSqlParser { node.jjtSetValue(access); } + /** + * Links a node whose production takes its left operand as a parameter, starting the node's + * token range at that operand rather than at the operator. The range is left as JJTree set it + * when the operand carries no node of its own. + */ + private void linkAST(ASTNodeAccess access, Node node, Expression leftExpression) { + linkAST(access, node); + if (leftExpression instanceof ASTNodeAccess) { + Node leftNode = ((ASTNodeAccess) leftExpression).getASTNode(); + if (leftNode != null && leftNode.jjtGetFirstToken() != null) { + node.jjtSetFirstToken(leftNode.jjtGetFirstToken()); + } + } + } + public Node getASTRoot() { return jjtree.rootNode(); } @@ -8665,7 +8680,7 @@ Expression RegularConditionRHS(Expression leftExpression, int oracleJoinRight) # } { - linkAST(result, jjtThis); + linkAST(result, jjtThis, leftExpression); return result; } } @@ -8754,7 +8769,7 @@ Expression InExpression(Expression leftExpression) #InExpression : .withOldOracleJoinSyntax(oldOracleJoin) .withNot(usingNot) .setGlobal(usingGlobal); - linkAST(inExpression,jjtThis); + linkAST(inExpression, jjtThis, leftExpression); return inExpression; } } @@ -8769,7 +8784,7 @@ Expression IncludesExpression(Expression leftExpression) #IncludesExpression : { IncludesExpression includesExpression = new IncludesExpression(leftExpression, rightExpression); - linkAST(includesExpression,jjtThis); + linkAST(includesExpression, jjtThis, leftExpression); return includesExpression; } } @@ -8784,7 +8799,7 @@ Expression ExcludesExpression(Expression leftExpression) #ExcludesExpression : { ExcludesExpression excludesExpression = new ExcludesExpression(leftExpression, rightExpression); - linkAST(excludesExpression,jjtThis); + linkAST(excludesExpression, jjtThis, leftExpression); return excludesExpression; } } @@ -8895,7 +8910,7 @@ Expression LikeExpression(Expression leftExpression) #LikeExpression: { result.setLeftExpression(leftExpression); result.setRightExpression(rightExpression); - linkAST(result,jjtThis); + linkAST(result, jjtThis, leftExpression); return result; } } @@ -8922,7 +8937,7 @@ Expression SimilarToExpression(Expression leftExpression) #SimilarToExpression: { result.setLeftExpression(leftExpression); result.setRightExpression(rightExpression); - linkAST(result,jjtThis); + linkAST(result, jjtThis, leftExpression); return result; } } @@ -8938,7 +8953,7 @@ Expression IsDistinctExpression(Expression leftExpression) #IsDistinctExpression { result.setLeftExpression(leftExpression); result.setRightExpression(rightExpression); - linkAST(result,jjtThis); + linkAST(result, jjtThis, leftExpression); return result; } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java index d45052d61..5fd6a7930 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectASTTest.java @@ -12,6 +12,9 @@ import java.util.ArrayList; import java.util.List; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.parser.ASTNodeAccess; import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor; import net.sf.jsqlparser.parser.CCJSqlParserTreeConstants; import net.sf.jsqlparser.parser.CCJSqlParser; @@ -176,10 +179,30 @@ public Object visit(Node node, Object data) { assertNotNull(subSelectStart); assertNotNull(subSelectEnd); - assertEquals(32, subSelectStart.beginColumn); + // the node spans the whole IN expression, so it starts at the left operand + assertEquals(30, subSelectStart.beginColumn); assertEquals(49, subSelectEnd.endColumn); } + @Test + public void testBinaryConditionNodeStartsAtItsLeftOperand() throws JSQLParserException { + String[][] cases = { + {"SELECT * FROM t WHERE z = 0 AND a = 1", "a = 1"}, + {"SELECT * FROM t WHERE z = 0 AND a IN (1, 2)", "a IN (1, 2)"}, + {"SELECT * FROM t WHERE z = 0 AND a LIKE 'p'", "a LIKE 'p'"}, + {"SELECT * FROM t WHERE z = 0 AND a SIMILAR TO 'p'", "a SIMILAR TO 'p'"}, + {"SELECT * FROM t WHERE z = 0 AND a IS DISTINCT FROM 1", "a IS DISTINCT FROM 1"}}; + + for (String[] testCase : cases) { + String sql = testCase[0]; + Expression condition = ((AndExpression) ((PlainSelect) CCJSqlParserUtil.parse(sql)) + .getWhere()).getRightExpression(); + Node node = ((ASTNodeAccess) condition).getASTNode(); + assertEquals(testCase[1], sql.substring(node.jjtGetFirstToken().absoluteBegin - 1, + node.jjtGetLastToken().absoluteEnd - 1), sql); + } + } + @Test public void testSelectASTExtractWithCommentsIssue1580() throws JSQLParserException { String sql =