From f46f8df34f1e8de4936492e7fb044608451b2a33 Mon Sep 17 00:00:00 2001 From: SEPURI-SAI-KRISHNA Date: Wed, 12 Aug 2026 14:36:00 +0530 Subject: [PATCH] Support the logical not operator in profile activation conditions The condition tokenizer already treats ! as an operator character and emits a bare "!" token when it is not followed by "=", but no parser rule ever consumed it. parseUnary() handled only "-", so a "!" fell through to parseTerm(), failed to parse as a number, and ended up in parseVariableOrUnknownFunction(). Writing the natural form of a negated condition: !exists('some/path') therefore failed with "Unknown variable: !", which is confusing since nothing in the expression is a variable. The workaround was the not() function. Handle "!" in parseUnary() as sugar for the existing not() function, reusing the same toBoolean() coercion so no new semantics are introduced. Handling it in parseUnary() gives it the same precedence as in Java: tighter than arithmetic, comparison, && and ||. Recursing into parseUnary() allows stacking such as !!x. != is unaffected, because the tokenizer emits it as a single token before a bare "!" can be produced. --- .../impl/model/profile/ConditionParser.java | 12 +++- .../model/profile/ConditionParserTest.java | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java index 4b73b5217ecc..a34e6e23cd42 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionParser.java @@ -288,11 +288,21 @@ private Object parseMultiplyDivide() { } /** - * Parses unary operations (negation). + * Parses unary operations: logical not ({@code !}) and arithmetic negation ({@code -}). + *

+ * {@code !} is equivalent to the {@code not()} function and uses the same {@link #toBoolean} + * coercion. Being handled here gives both operators a higher precedence than multiplication, + * addition, comparison and the logical operators, so {@code !a && b} parses as + * {@code (!a) && b}. * * @return the result of parsing unary operations */ private Object parseUnary() { + if (current < tokens.size() && tokens.get(current).equals("!")) { + current++; + Object value = parseUnary(); + return !toBoolean(value); + } if (current < tokens.size() && tokens.get(current).equals("-")) { current++; Object value = parseUnary(); diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java index be1da09919c1..c1eb0a57354e 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/profile/ConditionParserTest.java @@ -159,6 +159,71 @@ void testComplexExpression() { assertEquals("SUCCESS!", parser.parse(expression)); } + /** + * The tokenizer emits a bare {@code !} token, so the parser has to be able to consume it. + * The {@code contains(..)} cases also cover a {@code !} inside a string literal, which the + * tokenizer must leave alone. + */ + @Test + void testLogicalNotOperator() { + assertFalse((Boolean) parser.parse("!true")); + assertTrue((Boolean) parser.parse("!false")); + assertTrue((Boolean) parser.parse("!!true")); + assertFalse((Boolean) parser.parse("!!!true")); + assertTrue((Boolean) parser.parse("!contains('Hello, World!', 'OpenAI')")); + assertFalse((Boolean) parser.parse("!contains('Hello, World!', 'World')")); + } + + /** + * {@code !x} is sugar for the {@code not(x)} function and must agree with it. + */ + @Test + void testLogicalNotOperatorMatchesNotFunction() { + assertEquals( + parser.parse("not(contains('Hello, World!', 'OpenAI'))"), + parser.parse("!contains('Hello, World!', 'OpenAI')")); + assertEquals( + parser.parse("not(contains('Hello, World!', 'World'))"), + parser.parse("!contains('Hello, World!', 'World')")); + assertEquals(parser.parse("not(true)"), parser.parse("!true")); + } + + /** + * {@code !} binds tighter than comparison and the logical operators, as it does in Java. + */ + @Test + void testLogicalNotOperatorPrecedence() { + assertTrue((Boolean) parser.parse("!false && true")); + assertFalse((Boolean) parser.parse("!true && false")); + assertTrue((Boolean) parser.parse("!true || true")); + assertTrue((Boolean) parser.parse("!(true && false)")); + assertTrue((Boolean) parser.parse("!(1 > 2)")); + assertFalse((Boolean) parser.parse("!('a' != 'b')")); + } + + /** + * Coercion of non-boolean operands must match what the {@code not()} function does. + */ + @Test + void testLogicalNotOperatorCoercion() { + assertFalse((Boolean) parser.parse("!'abc'")); + assertTrue((Boolean) parser.parse("!''")); + assertFalse((Boolean) parser.parse("!length('ab')")); + assertTrue((Boolean) parser.parse("!0")); + assertFalse((Boolean) parser.parse("!1")); + } + + /** + * A bare {@code !} must not disturb the {@code !=} operator, which the tokenizer emits as a + * single token. + */ + @Test + void testNotEqualsStillParsesAsOneOperator() { + assertTrue((Boolean) parser.parse("1 != 2")); + assertFalse((Boolean) parser.parse("'abc' != 'abc'")); + assertTrue((Boolean) parser.parse("'abc' != 'cdf'")); + } + @Test void testStringComparison() { assertTrue((Boolean) parser.parse("'abc' != 'cdf'"));