diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index f187b7da1f..f3bbdc7ac0 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -278,50 +278,37 @@ private static ColDataType parseColDataType(String columnDataType, boolean allow } } - @SuppressWarnings("PMD.CyclomaticComplexity") public static Expression parseExpression(String expressionStr, boolean allowPartialParse, Consumer consumer) throws JSQLParserException { if (expressionStr == null || expressionStr.isEmpty()) { return null; } - Expression expression = null; - // first, try to parse fast and simple + // First require a complete simple parse, even when the caller allows a partial result. try { - CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(false); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (ParseException ex) { - throw new JSQLParserException(expressionStr, ex); - } - } catch (JSQLParserException ex1) { - // when fast simple parsing fails, try complex parsing but only if it has a chance to - // succeed - CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(true); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (!allowPartialParse - && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (JSQLParserException ex) { - throw ex; - } catch (ParseException ex) { - throw new JSQLParserException(ex); + return parseExpression(expressionStr, false, false, consumer); + } catch (JSQLParserException ex) { + return parseExpression(expressionStr, true, allowPartialParse, consumer); + } + } + + private static Expression parseExpression(String expressionStr, boolean allowComplexParsing, + boolean allowPartialParse, Consumer consumer) throws JSQLParserException { + CCJSqlParser parser = newParser(expressionStr).withAllowComplexParsing(allowComplexParsing); + if (consumer != null) { + consumer.accept(parser); + } + + try { + Expression expression = parser.Expression(); + if (!allowPartialParse && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { + throw new JSQLParserException( + "could only parse partial expression " + expression.toString()); } + return expression; + } catch (ParseException ex) { + throw new JSQLParserException(ex); } - return expression; } /** @@ -356,50 +343,9 @@ public static Expression parseCondExpression(String condExpr, boolean allowParti }); } - @SuppressWarnings("PMD.CyclomaticComplexity") public static Expression parseCondExpression(String conditionalExpressionStr, boolean allowPartialParse, Consumer consumer) throws JSQLParserException { - if (conditionalExpressionStr == null || conditionalExpressionStr.isEmpty()) { - return null; - } - - Expression expression = null; - // first, try to parse fast and simple - try { - CCJSqlParser parser = - newParser(conditionalExpressionStr).withAllowComplexParsing(false); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (ParseException ex) { - throw new JSQLParserException(ex); - } - } catch (JSQLParserException ex1) { - CCJSqlParser parser = - newParser(conditionalExpressionStr).withAllowComplexParsing(true); - if (consumer != null) { - consumer.accept(parser); - } - try { - expression = parser.Expression(); - if (!allowPartialParse - && parser.getNextToken().kind != CCJSqlParserTokenManager.EOF) { - throw new JSQLParserException( - "could only parse partial expression " + expression.toString()); - } - } catch (JSQLParserException ex) { - throw ex; - } catch (ParseException ex) { - throw new JSQLParserException(ex); - } - } - return expression; + return parseExpression(conditionalExpressionStr, allowPartialParse, consumer); } /** diff --git a/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java b/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java new file mode 100644 index 0000000000..924e8ef85a --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/ExpressionParsingFallbackTest.java @@ -0,0 +1,129 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.parser; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.parser.feature.Feature; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ExpressionParsingFallbackTest { + private static final String PARTIAL_EXPRESSION = "[customer].[id] = 1 trailing tokens"; + + @ParameterizedTest + @EnumSource(ParserApi.class) + void configuresCompleteExpressionsOnce(ParserApi api) throws JSQLParserException { + List attempts = new ArrayList<>(); + Expression result = api.parse("[customer].[id] = 1", false, parser -> { + attempts.add(parser); + parser.withSquareBracketQuotation(true); + }); + + assertEquals("[customer].[id] = 1", result.toString()); + assertEquals(1, attempts.size()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void reappliesConfigurationToAFreshParserForPartialInput(ParserApi api) + throws JSQLParserException { + List attempts = new ArrayList<>(); + List modes = new ArrayList<>(); + Expression result = api.parse(PARTIAL_EXPRESSION, true, parser -> { + attempts.add(parser); + modes.add(parser.getAsBoolean(Feature.allowComplexParsing)); + parser.withSquareBracketQuotation(true); + }); + + assertEquals("[customer].[id] = 1", result.toString()); + assertEquals(List.of(false, true), modes); + assertNotSame(attempts.get(0), attempts.get(1)); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void rejectsTrailingTokensWhenPartialParsingIsDisabled(ParserApi api) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> api.parse(PARTIAL_EXPRESSION, false, + parser -> parser.withSquareBracketQuotation(true))); + + assertEquals("could only parse partial expression [customer].[id] = 1", + exception.getMessage()); + assertNull(exception.getCause()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void preservesSyntaxFailureCauses(ParserApi api) { + for (boolean partial : new boolean[] {false, true}) { + JSQLParserException exception = assertThrows(JSQLParserException.class, + () -> api.parse(";", partial, null)); + + ParseException cause = assertInstanceOf(ParseException.class, exception.getCause()); + assertEquals(cause.getMessage(), exception.getMessage()); + } + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void leavesNullAndEmptyInputUnconfigured(ParserApi api) throws JSQLParserException { + Consumer unexpectedConfiguration = parser -> { + throw new AssertionError("Empty input must not configure a parser"); + }; + for (boolean partial : new boolean[] {false, true}) { + assertNull(api.parse(null, partial, unexpectedConfiguration)); + assertNull(api.parse("", partial, unexpectedConfiguration)); + } + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void acceptsNullConfiguration(ParserApi api) throws JSQLParserException { + assertEquals("x = 92", api.parse("x=92", false, null).toString()); + assertEquals("x = 92", api.parse("x=92 trailing tokens", true, null).toString()); + } + + @ParameterizedTest + @EnumSource(ParserApi.class) + void propagatesConfigurationFailuresWithoutRetrying(ParserApi api) { + IllegalStateException failure = new IllegalStateException("Configuration failed"); + List attempts = new ArrayList<>(); + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> api.parse("x=92", false, parser -> { + attempts.add(parser); + throw failure; + })); + + assertSame(failure, exception); + assertEquals(1, attempts.size()); + } + + private enum ParserApi { + EXPRESSION, CONDITION; + + Expression parse(String text, boolean allowPartialParse, Consumer consumer) + throws JSQLParserException { + return this == EXPRESSION + ? CCJSqlParserUtil.parseExpression(text, allowPartialParse, consumer) + : CCJSqlParserUtil.parseCondExpression(text, allowPartialParse, consumer); + } + } +}