From 491c713de2aaf1883e2f8f7daf166f6764191497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=98=E5=85=B8?= Date: Sun, 6 Sep 2026 15:22:54 +0800 Subject: [PATCH] fix(parser): parse stacked NOT prefixes as nested NotExpression (#1170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Condition() consumed at most one leading NOT / ! token; a second one leaked into PrimaryExpression() and attached to the left operand only (NOT NOT 1 = 1 parsed as NOT (NOT 1) = 1), a third one raised a ParseException. Consume all leading NOT / ! tokens in a loop and wrap the parsed condition once per token, leftmost token outermost. Signed-off-by: 付典 --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 14 +- .../expression/ExpressionPrecedenceTest.java | 187 ++++++++++++++++++ 2 files changed, 197 insertions(+), 4 deletions(-) diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index c0372c5b18..b3e1e53654 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -7812,13 +7812,16 @@ Expression Condition(): { Expression result; Expression left; - boolean not = false; - boolean exclamationMarkNot = false; + // Leading NOT / ! prefixes, leftmost token wraps outermost (issue #1170). + final Deque exclamationMarks = new ArrayDeque(); int oraclePrior = EqualsTo.NO_ORACLE_PRIOR; int oracleJoin = EqualsTo.NO_ORACLE_JOIN; } { - [ LOOKAHEAD(2, { getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) ( { not=true; } | "!" { not=true; exclamationMarkNot=true; })] + ( + LOOKAHEAD({ getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) + ( { exclamationMarks.addFirst(Boolean.FALSE); } | "!" { exclamationMarks.addFirst(Boolean.TRUE); } ) + )* ( result=ExistsExpression() | @@ -7863,7 +7866,10 @@ Expression Condition(): && result instanceof SupportsOldOracleJoinSyntax) { ((SupportsOldOracleJoinSyntax) result).setOraclePriorPosition(oraclePrior); } - return not ? new NotExpression(result, exclamationMarkNot) : result; + while (!exclamationMarks.isEmpty()) { + result = new NotExpression(result, exclamationMarks.pollFirst()); + } + return result; } } diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java index a601f6e0ac..a0a4973718 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionPrecedenceTest.java @@ -10,9 +10,20 @@ package net.sf.jsqlparser.expression; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.Concat; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.relational.Between; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.expression.operators.relational.IsNullExpression; +import net.sf.jsqlparser.expression.operators.relational.LikeExpression; +import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.test.TestUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,4 +41,180 @@ public void testGetSign() throws JSQLParserException { Assertions.assertInstanceOf(BitwiseAnd.class, ((Concat) expr).getLeftExpression()); Assertions.assertInstanceOf(LongValue.class, ((Concat) expr).getRightExpression()); } + + @Test + public void testNotNotConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not 1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertEquals("NOT NOT 1 = 1", expr.toString()); + } + + @Test + public void testNotNotNotConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not not 1 = 1"); + NotExpression first = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression second = Assertions.assertInstanceOf(NotExpression.class, + first.getExpression()); + NotExpression third = Assertions.assertInstanceOf(NotExpression.class, + second.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, third.getExpression()); + Assertions.assertEquals("NOT NOT NOT 1 = 1", expr.toString()); + } + + @Test + public void testNotNotExistsIssue1170() throws JSQLParserException { + Expression expr = + CCJSqlParserUtil.parseCondExpression("not not exists (select 1 from mytable)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(ExistsExpression.class, inner.getExpression()); + } + + @Test + public void testNotNotInIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a in (1, 2)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(InExpression.class, inner.getExpression()); + } + + @Test + public void testNotNotNotBetweenIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not not a between 1 and 2"); + NotExpression first = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression second = Assertions.assertInstanceOf(NotExpression.class, + first.getExpression()); + NotExpression third = Assertions.assertInstanceOf(NotExpression.class, + second.getExpression()); + Assertions.assertInstanceOf(Between.class, third.getExpression()); + } + + @Test + public void testNotNotNotWhereClauseIssue1170() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed("SELECT * FROM mytable WHERE NOT NOT NOT a = 1"); + } + + @Test + public void testMixedNotAndExclamationMarkIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not !1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + Assertions.assertFalse(outer.isExclamationMark()); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertTrue(inner.isExclamationMark()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertEquals("NOT ! 1 = 1", expr.toString()); + } + + @Test + public void testSingleNotConditionStaysFlat() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not 1 = 1"); + NotExpression not = Assertions.assertInstanceOf(NotExpression.class, expr); + Assertions.assertInstanceOf(EqualsTo.class, not.getExpression()); + } + + @Test + public void testNotNotLikeIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a like 'x'"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + LikeExpression like = Assertions.assertInstanceOf(LikeExpression.class, + inner.getExpression()); + Assertions.assertFalse(like.isNot()); + Assertions.assertInstanceOf(Column.class, like.getLeftExpression()); + } + + @Test + public void testNotNotIsNullIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a is null"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + IsNullExpression isNull = Assertions.assertInstanceOf(IsNullExpression.class, + inner.getExpression()); + Assertions.assertFalse(isNull.isNot()); + Assertions.assertInstanceOf(Column.class, isNull.getLeftExpression()); + } + + @Test + public void testNotNotSimilarToIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not a similar to 'x'"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + LikeExpression similar = Assertions.assertInstanceOf(LikeExpression.class, + inner.getExpression()); + Assertions.assertFalse(similar.isNot()); + Assertions.assertInstanceOf(Column.class, similar.getLeftExpression()); + } + + @Test + public void testNotNotInsideAndChainIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("a and not not b = 1"); + AndExpression and = Assertions.assertInstanceOf(AndExpression.class, expr); + Assertions.assertInstanceOf(Column.class, and.getLeftExpression()); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, + and.getRightExpression()); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + EqualsTo equalsTo = Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + Assertions.assertInstanceOf(Column.class, equalsTo.getLeftExpression()); + } + + @Test + public void testNotNotCaseInsensitiveIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("NoT nOt 1 = 1"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(EqualsTo.class, inner.getExpression()); + } + + @Test + public void testEightNotIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil + .parseCondExpression("not not not not not not not not 1 = 1"); + int depth = 0; + while (expr instanceof NotExpression) { + expr = ((NotExpression) expr).getExpression(); + depth++; + } + Assertions.assertEquals(8, depth); + Assertions.assertInstanceOf(EqualsTo.class, expr); + } + + @Test + public void testNotNotParenthesizedConditionIssue1170() throws JSQLParserException { + Expression expr = CCJSqlParserUtil.parseCondExpression("not not (a = 1 and b = 2)"); + NotExpression outer = Assertions.assertInstanceOf(NotExpression.class, expr); + NotExpression inner = Assertions.assertInstanceOf(NotExpression.class, + outer.getExpression()); + Assertions.assertInstanceOf(ParenthesedExpressionList.class, inner.getExpression()); + } + + @Test + public void testNotNotInStatementPositionsIssue1170() throws JSQLParserException { + TestUtils.assertSqlCanBeParsedAndDeparsed( + "SELECT * FROM t WHERE a IN (SELECT id FROM u WHERE NOT NOT x = 1)"); + TestUtils.assertSqlCanBeParsedAndDeparsed("UPDATE t SET a = 1 WHERE NOT NOT b = 2"); + TestUtils.assertSqlCanBeParsedAndDeparsed("DELETE FROM t WHERE NOT NOT b = 2"); + } + + @Test + public void testValuePositionNotStaysOnOperand() throws JSQLParserException { + Expression equalsTo = CCJSqlParserUtil.parseCondExpression("a = not 1"); + EqualsTo eq = Assertions.assertInstanceOf(EqualsTo.class, equalsTo); + Assertions.assertInstanceOf(Column.class, eq.getLeftExpression()); + Assertions.assertInstanceOf(NotExpression.class, eq.getRightExpression()); + + Expression addition = CCJSqlParserUtil.parseCondExpression("1 + not 2"); + Addition add = Assertions.assertInstanceOf(Addition.class, addition); + Assertions.assertInstanceOf(NotExpression.class, add.getRightExpression()); + } }