From 58e695e80443d884f237155d141ed3a0c5abc4e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:42:12 +0000 Subject: [PATCH 01/16] Initial plan From c400ca92df644a67cefb6de825dc68ea29a4cdf0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:47:04 +0000 Subject: [PATCH 02/16] Implement logical operators (&&, ||, !) for NBT path expressions Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../cyclops/cyclopscore/nbt/path/NbtPath.java | 3 + .../path/parse/NbtPathExpressionHelpers.java | 67 ++++++ ...pressionParseHandlerBooleanLogicalAnd.java | 90 ++++++++ ...pressionParseHandlerBooleanLogicalNot.java | 84 +++++++ ...xpressionParseHandlerBooleanLogicalOr.java | 90 ++++++++ .../nbt/path/TestNbtPathLogicalOperators.java | 210 ++++++++++++++++++ ...athExpressionHandlerBooleanLogicalAnd.java | 104 +++++++++ ...athExpressionHandlerBooleanLogicalNot.java | 87 ++++++++ ...PathExpressionHandlerBooleanLogicalOr.java | 104 +++++++++ 9 files changed, 839 insertions(+) create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java create mode 100644 src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java create mode 100644 src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalAnd.java create mode 100644 src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java create mode 100644 src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalOr.java diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java index 3d9f7dc00e3..a09409f0b44 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java @@ -27,6 +27,9 @@ public class NbtPath { new NbtPathExpressionParseHandlerBooleanRelationalGreaterThanOrEqual(), new NbtPathExpressionParseHandlerBooleanRelationalEqual(), new NbtPathExpressionParseHandlerStringEqual(), + new NbtPathExpressionParseHandlerBooleanLogicalAnd(), + new NbtPathExpressionParseHandlerBooleanLogicalOr(), + new NbtPathExpressionParseHandlerBooleanLogicalNot(), new NbtPathExpressionParseHandlerFilterExpression() ); diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java new file mode 100644 index 00000000000..a1dc7d18403 --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java @@ -0,0 +1,67 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.Tag; + +/** + * Utility methods for NBT path expression handling. + */ +public class NbtPathExpressionHelpers { + + /** + * Determine if a tag is truthy. + * ByteTag with value 1 is true, 0 is false. + * Any other non-null tag is considered true. + * This follows the same logic as {@link org.cyclops.cyclopscore.nbt.path.INbtPathExpression#test(Tag)}. + * + * @param tag The tag to check + * @return true if the tag is truthy, false otherwise + */ + public static boolean isTruthy(Tag tag) { + if (tag == null) { + return false; + } + if (tag.getId() == Tag.TAG_BYTE) { + return ((ByteTag) tag).getAsByte() == (byte) 1; + } + // Non-null non-ByteTags are truthy + return true; + } + + /** + * Find the end position of an expression, stopping at logical operators or closing parenthesis. + * This method is shared by logical operator handlers to identify expression boundaries. + * + * @param expression The full expression string + * @param start The starting position to search from + * @return The position where the expression ends + */ + public static int findExpressionEnd(String expression, int start) { + int depth = 0; + for (int i = start; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (c == '(') { + depth++; + } else if (c == ')') { + if (depth == 0) { + return i; + } + depth--; + } else if (depth == 0) { + // Check for logical operators at top level + if (i + 1 < expression.length()) { + String twoChar = expression.substring(i, i + 2); + if (twoChar.equals("&&") || twoChar.equals("||")) { + return i; + } + } + // Check for NOT operator (but not != which is handled differently) + if (c == '!' && (i + 1 >= expression.length() || expression.charAt(i + 1) != '=')) { + return i; + } + } + } + return expression.length(); + } +} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java new file mode 100644 index 00000000000..f82da1ba9ef --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -0,0 +1,90 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.Tag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.cyclops.cyclopscore.nbt.path.NbtParseException; +import org.cyclops.cyclopscore.nbt.path.NbtPath; +import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; + +import javax.annotation.Nullable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * A handler that handles boolean AND expressions in the form of "expression1 && expression2". + */ +public class NbtPathExpressionParseHandlerBooleanLogicalAnd implements INbtPathExpressionParseHandler { + + private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *&& *"); + + @Nullable + @Override + public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { + Matcher matcher = REGEX_EXPRESSION + .matcher(nbtPathExpression) + .region(pos, nbtPathExpression.length()); + if (!matcher.find()) { + return HandleResult.INVALID; + } + + // Parse the right-hand side expression + int rightPos = pos + matcher.group().length(); + if (rightPos >= nbtPathExpression.length()) { + return HandleResult.INVALID; + } + + // Find the end of the right expression + int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); + if (endPos == rightPos) { + return HandleResult.INVALID; + } + + String rightExpressionString = nbtPathExpression.substring(rightPos, endPos); + try { + INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString); + return new HandleResult(new Expression(rightExpression), + matcher.group().length() + rightExpressionString.length()); + } catch (NbtParseException e) { + return HandleResult.INVALID; + } + } + + public static class Expression implements INbtPathExpression { + + private final INbtPathExpression rightExpression; + + public Expression(INbtPathExpression rightExpression) { + this.rightExpression = rightExpression; + } + + public INbtPathExpression getRightExpression() { + return rightExpression; + } + + @Override + public NbtPathExpressionMatches matchContexts(Stream executionContexts) { + return new NbtPathExpressionMatches(executionContexts + .map(executionContext -> { + Tag currentTag = executionContext.getCurrentTag(); + + // The left side is the current tag (should be a boolean result from previous expression) + boolean leftValue = NbtPathExpressionHelpers.isTruthy(currentTag); + + // Evaluate the right expression against the parent context (original tag before boolean conversion) + Tag originalTag = executionContext.getParentContext() != null + ? executionContext.getParentContext().getCurrentTag() + : currentTag; + boolean rightValue = rightExpression.test(originalTag); + + // AND operation + boolean result = leftValue && rightValue; + + return new NbtPathExpressionExecutionContext( + ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext); + }) + ); + } + } +} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java new file mode 100644 index 00000000000..a1180685371 --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java @@ -0,0 +1,84 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.Tag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.cyclops.cyclopscore.nbt.path.NbtParseException; +import org.cyclops.cyclopscore.nbt.path.NbtPath; +import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; + +import javax.annotation.Nullable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * A handler that handles boolean NOT expressions in the form of "!expression". + */ +public class NbtPathExpressionParseHandlerBooleanLogicalNot implements INbtPathExpressionParseHandler { + + private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *! *"); + + @Nullable + @Override + public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { + Matcher matcher = REGEX_EXPRESSION + .matcher(nbtPathExpression) + .region(pos, nbtPathExpression.length()); + if (!matcher.find()) { + return HandleResult.INVALID; + } + + // Parse the expression to negate + int exprPos = pos + matcher.group().length(); + if (exprPos >= nbtPathExpression.length()) { + return HandleResult.INVALID; + } + + // Find the end of the expression + int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, exprPos); + if (endPos == exprPos) { + return HandleResult.INVALID; + } + + String expressionString = nbtPathExpression.substring(exprPos, endPos); + try { + INbtPathExpression expression = NbtPath.parse(expressionString); + return new HandleResult(new Expression(expression), + matcher.group().length() + expressionString.length()); + } catch (NbtParseException e) { + return HandleResult.INVALID; + } + } + + public static class Expression implements INbtPathExpression { + + private final INbtPathExpression expression; + + public Expression(INbtPathExpression expression) { + this.expression = expression; + } + + public INbtPathExpression getExpression() { + return expression; + } + + @Override + public NbtPathExpressionMatches matchContexts(Stream executionContexts) { + return new NbtPathExpressionMatches(executionContexts + .map(executionContext -> { + Tag currentTag = executionContext.getCurrentTag(); + + // Evaluate the expression + boolean value = expression.test(currentTag); + + // NOT operation + boolean result = !value; + + return new NbtPathExpressionExecutionContext( + ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext); + }) + ); + } + } +} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java new file mode 100644 index 00000000000..4520f514e5a --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java @@ -0,0 +1,90 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.Tag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.cyclops.cyclopscore.nbt.path.NbtParseException; +import org.cyclops.cyclopscore.nbt.path.NbtPath; +import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; + +import javax.annotation.Nullable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * A handler that handles boolean OR expressions in the form of "expression1 || expression2". + */ +public class NbtPathExpressionParseHandlerBooleanLogicalOr implements INbtPathExpressionParseHandler { + + private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *\\|\\| *"); + + @Nullable + @Override + public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { + Matcher matcher = REGEX_EXPRESSION + .matcher(nbtPathExpression) + .region(pos, nbtPathExpression.length()); + if (!matcher.find()) { + return HandleResult.INVALID; + } + + // Parse the right-hand side expression + int rightPos = pos + matcher.group().length(); + if (rightPos >= nbtPathExpression.length()) { + return HandleResult.INVALID; + } + + // Find the end of the right expression + int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); + if (endPos == rightPos) { + return HandleResult.INVALID; + } + + String rightExpressionString = nbtPathExpression.substring(rightPos, endPos); + try { + INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString); + return new HandleResult(new Expression(rightExpression), + matcher.group().length() + rightExpressionString.length()); + } catch (NbtParseException e) { + return HandleResult.INVALID; + } + } + + public static class Expression implements INbtPathExpression { + + private final INbtPathExpression rightExpression; + + public Expression(INbtPathExpression rightExpression) { + this.rightExpression = rightExpression; + } + + public INbtPathExpression getRightExpression() { + return rightExpression; + } + + @Override + public NbtPathExpressionMatches matchContexts(Stream executionContexts) { + return new NbtPathExpressionMatches(executionContexts + .map(executionContext -> { + Tag currentTag = executionContext.getCurrentTag(); + + // The left side is the current tag (should be a boolean result from previous expression) + boolean leftValue = NbtPathExpressionHelpers.isTruthy(currentTag); + + // Evaluate the right expression against the parent context (original tag before boolean conversion) + Tag originalTag = executionContext.getParentContext() != null + ? executionContext.getParentContext().getCurrentTag() + : currentTag; + boolean rightValue = rightExpression.test(originalTag); + + // OR operation + boolean result = leftValue || rightValue; + + return new NbtPathExpressionExecutionContext( + ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext); + }) + ); + } + } +} diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java new file mode 100644 index 00000000000..a50bf761e0d --- /dev/null +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -0,0 +1,210 @@ +package org.cyclops.cyclopscore.nbt.path; + +import com.google.common.collect.Lists; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestNbtPathLogicalOperators { + + @Test + public void testParseLogicalNotSimple() throws NbtParseException { + // Test: !(@.a == 5) + INbtPathExpression expression = NbtPath.parse("!@.a == 5"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 3); // 3 == 5 is false, so !(false) should be true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + } + + @Test + public void testParseLogicalNotFalse() throws NbtParseException { + // Test: !(@.a == 5) where a is 5 + INbtPathExpression expression = NbtPath.parse("!@.a == 5"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); // 5 == 5 is true, so !(true) should be false + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag1), is(false)); + } + + @Test + public void testParseLogicalAndBothTrue() throws NbtParseException { + // Test: (@.a > 2) && (< 10) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a > 2 && < 10"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); // 5 > 2 is true, 5 < 10 is true, so true && true = true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + } + + @Test + public void testParseLogicalAndLeftFalse() throws NbtParseException { + // Test: (@.a > 10) && (< 20) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a > 10 && < 20"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); // 5 > 10 is false, 5 < 20 is true, so false && true = false + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag1), is(false)); + } + + @Test + public void testParseLogicalOrBothFalse() throws NbtParseException { + // Test: (@.a < 2) || (> 10) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a < 2 || > 10"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); // 5 < 2 is false, 5 > 10 is false, so false || false = false + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag1), is(false)); + } + + @Test + public void testParseLogicalOrRightTrue() throws NbtParseException { + // Test: (@.a < 2) || (> 10) with value 15 + INbtPathExpression expression = NbtPath.parse("@.a < 2 || > 10"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 15); // 15 < 2 is false, 15 > 10 is true, so false || true = true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + } + + @Test + public void testParseComplexLogicalExpression() throws NbtParseException { + // Test: (@.a > 5) && (< 15) || (== 20) + INbtPathExpression expression = NbtPath.parse("@.a > 5 && < 15 || == 20"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 10); // 10 > 5 is true, 10 < 15 is true, so (true && true) = true, true || false = true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + } + + @Test + public void testParseLogicalWithFilterExpression() throws NbtParseException { + // Test filter expression with OR: [?(@.value < 5 || > 10)] + INbtPathExpression expression = NbtPath.parse("$.items[?(@.value < 5 || > 10)]"); + + CompoundTag root = new CompoundTag(); + ListTag items = new ListTag(); + + CompoundTag item1 = new CompoundTag(); + item1.putInt("value", 3); // 3 < 5 is true, should match + + CompoundTag item2 = new CompoundTag(); + item2.putInt("value", 7); // 7 < 5 is false, 7 > 10 is false, should not match + + CompoundTag item3 = new CompoundTag(); + item3.putInt("value", 15); // 15 < 5 is false, 15 > 10 is true, should match + + items.add(item1); + items.add(item2); + items.add(item3); + root.put("items", items); + + ListTag expectedFiltered = new ListTag(); + expectedFiltered.add(item1); + expectedFiltered.add(item3); + + List expected = Lists.newArrayList(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + assertThat(expression.test(root), is(true)); + } + + @Test + public void testParseLogicalWithFilterExpressionAnd() throws NbtParseException { + // Test filter expression with AND: [?(@.min < 10 && > 5)] + INbtPathExpression expression = NbtPath.parse("$.items[?(@.min < 10 && > 5)]"); + + CompoundTag root = new CompoundTag(); + ListTag items = new ListTag(); + + CompoundTag item1 = new CompoundTag(); + item1.putInt("min", 7); // 7 < 10 is true, 7 > 5 is true, should match + + CompoundTag item2 = new CompoundTag(); + item2.putInt("min", 3); // 3 < 10 is true, 3 > 5 is false, should not match + + CompoundTag item3 = new CompoundTag(); + item3.putInt("min", 12); // 12 < 10 is false, should not match + + items.add(item1); + items.add(item2); + items.add(item3); + root.put("items", items); + + ListTag expectedFiltered = new ListTag(); + expectedFiltered.add(item1); + + List expected = Lists.newArrayList(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + assertThat(expression.test(root), is(true)); + } + + @Test + public void testParseLogicalNotWithFilterExpression() throws NbtParseException { + // Test filter expression with NOT: [?(!(@.active == 1))] + INbtPathExpression expression = NbtPath.parse("$.items[?(!(@.active == 1))]"); + + CompoundTag root = new CompoundTag(); + ListTag items = new ListTag(); + + CompoundTag item1 = new CompoundTag(); + item1.putInt("active", 0); // 0 == 1 is false, !(false) is true, should match + + CompoundTag item2 = new CompoundTag(); + item2.putInt("active", 1); // 1 == 1 is true, !(true) is false, should not match + + CompoundTag item3 = new CompoundTag(); + item3.putInt("active", 0); // 0 == 1 is false, !(false) is true, should match + + items.add(item1); + items.add(item2); + items.add(item3); + root.put("items", items); + + ListTag expectedFiltered = new ListTag(); + expectedFiltered.add(item1); + expectedFiltered.add(item3); + + List expected = Lists.newArrayList(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + assertThat(expression.test(root), is(true)); + } +} diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalAnd.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalAnd.java new file mode 100644 index 00000000000..80cc104e252 --- /dev/null +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalAnd.java @@ -0,0 +1,104 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import com.google.common.collect.Lists; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.IntTag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.junit.Before; +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestNbtPathExpressionHandlerBooleanLogicalAnd { + + private NbtPathExpressionParseHandlerBooleanLogicalAnd handler; + + @Before + public void beforeEach() { + handler = new NbtPathExpressionParseHandlerBooleanLogicalAnd(); + } + + @Test + public void testNonMatch() { + assertThat(handler.handlePrefixOf("$", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testNonMatchSingleAmpersand() { + assertThat(handler.handlePrefixOf("&", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testMatchExpressionSimple() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("&& < 10", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(7)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalAnd.Expression.class)); + } + + @Test + public void testMatchExpressionWithSpaces() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" && > 5", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(9)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalAnd.Expression.class)); + } + + @Test + public void testExpressionStreamBothTrue() { + // Create an expression that evaluates "< 10" (the right side of &&) + INbtPathExpression expression = handler.handlePrefixOf("&& < 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(1) as current (left side = true) + // and IntTag(5) as parent (which will be used to evaluate right side: 5 < 10 = true) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 1), parentContext); + + // true && true should be true + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } + + @Test + public void testExpressionStreamLeftFalse() { + // Create an expression that evaluates "< 10" + INbtPathExpression expression = handler.handlePrefixOf("&& < 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(0) as current (left side = false) + // and IntTag(5) as parent (right side would be: 5 < 10 = true, but left is false) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 0), parentContext); + + // false && true should be false + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } + + @Test + public void testExpressionStreamRightFalse() { + // Create an expression that evaluates "> 10" + INbtPathExpression expression = handler.handlePrefixOf("&& > 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(1) as current (left side = true) + // and IntTag(5) as parent (right side: 5 > 10 = false) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 1), parentContext); + + // true && false should be false + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } +} diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java new file mode 100644 index 00000000000..010cc05e941 --- /dev/null +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java @@ -0,0 +1,87 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import com.google.common.collect.Lists; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.IntTag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.junit.Before; +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestNbtPathExpressionHandlerBooleanLogicalNot { + + private NbtPathExpressionParseHandlerBooleanLogicalNot handler; + + @Before + public void beforeEach() { + handler = new NbtPathExpressionParseHandlerBooleanLogicalNot(); + } + + @Test + public void testNonMatch() { + assertThat(handler.handlePrefixOf("$", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testMatchExpressionSimple() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("!< 10", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(5)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalNot.Expression.class)); + } + + @Test + public void testMatchExpressionWithSpaces() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" ! > 5", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(8)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalNot.Expression.class)); + } + + @Test + public void testExpressionStreamNegateTrue() { + // Create an expression that evaluates "!< 10" + INbtPathExpression expression = handler.handlePrefixOf("!< 10", 0).getPrefixExpression(); + + // 5 < 10 is true, so !(true) should be false + assertThat(expression.match(Stream.of(IntTag.valueOf(5))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } + + @Test + public void testExpressionStreamNegateFalse() { + // Create an expression that evaluates "!< 10" + INbtPathExpression expression = handler.handlePrefixOf("!< 10", 0).getPrefixExpression(); + + // 15 < 10 is false, so !(false) should be true + assertThat(expression.match(Stream.of(IntTag.valueOf(15))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } + + @Test + public void testExpressionStreamNegateByteTagTrue() { + // Create an expression that evaluates "!== 1" + INbtPathExpression expression = handler.handlePrefixOf("!== 1", 0).getPrefixExpression(); + + // 1 == 1 is true, so !(true) should be false + assertThat(expression.match(Stream.of(ByteTag.valueOf((byte) 1))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } + + @Test + public void testExpressionStreamNegateByteTagFalse() { + // Create an expression that evaluates "!== 1" + INbtPathExpression expression = handler.handlePrefixOf("!== 1", 0).getPrefixExpression(); + + // 0 == 1 is false, so !(false) should be true + assertThat(expression.match(Stream.of(ByteTag.valueOf((byte) 0))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } +} diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalOr.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalOr.java new file mode 100644 index 00000000000..9a59581349f --- /dev/null +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalOr.java @@ -0,0 +1,104 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import com.google.common.collect.Lists; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.IntTag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.junit.Before; +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestNbtPathExpressionHandlerBooleanLogicalOr { + + private NbtPathExpressionParseHandlerBooleanLogicalOr handler; + + @Before + public void beforeEach() { + handler = new NbtPathExpressionParseHandlerBooleanLogicalOr(); + } + + @Test + public void testNonMatch() { + assertThat(handler.handlePrefixOf("$", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testNonMatchSinglePipe() { + assertThat(handler.handlePrefixOf("|", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testMatchExpressionSimple() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("|| < 10", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(7)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalOr.Expression.class)); + } + + @Test + public void testMatchExpressionWithSpaces() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" || > 5", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(9)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalOr.Expression.class)); + } + + @Test + public void testExpressionStreamBothTrue() { + // Create an expression that evaluates "< 10" + INbtPathExpression expression = handler.handlePrefixOf("|| < 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(1) as current (left side = true) + // and IntTag(5) as parent (right side: 5 < 10 = true) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 1), parentContext); + + // true || true should be true + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } + + @Test + public void testExpressionStreamLeftTrue() { + // Create an expression that evaluates "> 10" + INbtPathExpression expression = handler.handlePrefixOf("|| > 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(1) as current (left side = true) + // and IntTag(5) as parent (right side: 5 > 10 = false) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 1), parentContext); + + // true || false should be true + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } + + @Test + public void testExpressionStreamBothFalse() { + // Create an expression that evaluates "> 10" + INbtPathExpression expression = handler.handlePrefixOf("|| > 10", 0).getPrefixExpression(); + + // Create execution context with ByteTag(0) as current (left side = false) + // and IntTag(5) as parent (right side: 5 > 10 = false) + NbtPathExpressionExecutionContext parentContext = new NbtPathExpressionExecutionContext(IntTag.valueOf(5)); + NbtPathExpressionExecutionContext context = new NbtPathExpressionExecutionContext(ByteTag.valueOf((byte) 0), parentContext); + + // false || false should be false + assertThat(expression.match(Stream.of(context.getCurrentTag())) + .matchContexts(Stream.of(context)) + .getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } +} From 19f447f717b11f345f4daf02155cd080e2a1f702 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:56:03 +0000 Subject: [PATCH 03/16] Fix Javadoc HTML entity errors for logical operators Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java | 2 +- .../parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java index f82da1ba9ef..d0be3a49373 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -13,7 +13,7 @@ import java.util.stream.Stream; /** - * A handler that handles boolean AND expressions in the form of "expression1 && expression2". + * A handler that handles boolean AND expressions in the form of "expression1 {@literal &&} expression2". */ public class NbtPathExpressionParseHandlerBooleanLogicalAnd implements INbtPathExpressionParseHandler { diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java index 4520f514e5a..ba5deb86a35 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java @@ -13,7 +13,7 @@ import java.util.stream.Stream; /** - * A handler that handles boolean OR expressions in the form of "expression1 || expression2". + * A handler that handles boolean OR expressions in the form of "expression1 {@literal ||} expression2". */ public class NbtPathExpressionParseHandlerBooleanLogicalOr implements INbtPathExpressionParseHandler { From 53499c90288cb844b63e775029e50805a4e525ec Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Sat, 14 Feb 2026 09:39:31 +0100 Subject: [PATCH 04/16] Cleanup code and fix test build failures --- ...tPathExpressionParseHandlerBooleanLogicalAnd.java | 12 ++++-------- ...tPathExpressionParseHandlerBooleanLogicalNot.java | 6 +----- ...btPathExpressionParseHandlerBooleanLogicalOr.java | 12 ++++-------- ...estNbtPathExpressionHandlerBooleanLogicalAnd.java | 3 --- ...TestNbtPathExpressionHandlerBooleanLogicalOr.java | 3 --- 5 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java index d0be3a49373..a3062da0645 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -53,14 +53,10 @@ public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { public static class Expression implements INbtPathExpression { - private final INbtPathExpression rightExpression; + protected final INbtPathExpression expression; - public Expression(INbtPathExpression rightExpression) { - this.rightExpression = rightExpression; - } - - public INbtPathExpression getRightExpression() { - return rightExpression; + public Expression(INbtPathExpression expression) { + this.expression = expression; } @Override @@ -76,7 +72,7 @@ public NbtPathExpressionMatches matchContexts(Stream executionContexts) { return new NbtPathExpressionMatches(executionContexts diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java index ba5deb86a35..dde61914599 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java @@ -53,14 +53,10 @@ public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { public static class Expression implements INbtPathExpression { - private final INbtPathExpression rightExpression; + protected final INbtPathExpression expression; - public Expression(INbtPathExpression rightExpression) { - this.rightExpression = rightExpression; - } - - public INbtPathExpression getRightExpression() { - return rightExpression; + public Expression(INbtPathExpression expression) { + this.expression = expression; } @Override @@ -76,7 +72,7 @@ public NbtPathExpressionMatches matchContexts(Stream Date: Sat, 14 Feb 2026 08:44:11 +0000 Subject: [PATCH 05/16] Require explicit NBT references in AND/OR and add != operator Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../cyclops/cyclopscore/nbt/path/NbtPath.java | 1 + ...pressionParseHandlerBooleanLogicalAnd.java | 9 +- ...pressionParseHandlerBooleanLogicalNot.java | 2 +- ...xpressionParseHandlerBooleanLogicalOr.java | 9 +- ...ParseHandlerBooleanRelationalNotEqual.java | 15 ++++ .../nbt/path/TestNbtPathLogicalOperators.java | 87 ++++++++++++++++--- ...ssionHandlerBooleanRelationalNotEqual.java | 65 ++++++++++++++ 7 files changed, 163 insertions(+), 25 deletions(-) create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanRelationalNotEqual.java create mode 100644 src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java index a09409f0b44..854fcdddf9a 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java @@ -25,6 +25,7 @@ public class NbtPath { new NbtPathExpressionParseHandlerBooleanRelationalLessThanOrEqual(), new NbtPathExpressionParseHandlerBooleanRelationalGreaterThan(), new NbtPathExpressionParseHandlerBooleanRelationalGreaterThanOrEqual(), + new NbtPathExpressionParseHandlerBooleanRelationalNotEqual(), new NbtPathExpressionParseHandlerBooleanRelationalEqual(), new NbtPathExpressionParseHandlerStringEqual(), new NbtPathExpressionParseHandlerBooleanLogicalAnd(), diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java index a3062da0645..b01efc4748c 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -68,11 +68,10 @@ public NbtPathExpressionMatches matchContexts(Stream 2 && < 10"); + // Test: (@.a > 2) && (@.a < 10) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a > 2 && @.a < 10"); CompoundTag tag1 = new CompoundTag(); tag1.putInt("a", 5); // 5 > 2 is true, 5 < 10 is true, so true && true = true @@ -62,8 +62,8 @@ public void testParseLogicalAndBothTrue() throws NbtParseException { @Test public void testParseLogicalAndLeftFalse() throws NbtParseException { - // Test: (@.a > 10) && (< 20) with value 5 - INbtPathExpression expression = NbtPath.parse("@.a > 10 && < 20"); + // Test: (@.a > 10) && (@.a < 20) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a > 10 && @.a < 20"); CompoundTag tag1 = new CompoundTag(); tag1.putInt("a", 5); // 5 > 10 is false, 5 < 20 is true, so false && true = false @@ -76,8 +76,8 @@ public void testParseLogicalAndLeftFalse() throws NbtParseException { @Test public void testParseLogicalOrBothFalse() throws NbtParseException { - // Test: (@.a < 2) || (> 10) with value 5 - INbtPathExpression expression = NbtPath.parse("@.a < 2 || > 10"); + // Test: (@.a < 2) || (@.a > 10) with value 5 + INbtPathExpression expression = NbtPath.parse("@.a < 2 || @.a > 10"); CompoundTag tag1 = new CompoundTag(); tag1.putInt("a", 5); // 5 < 2 is false, 5 > 10 is false, so false || false = false @@ -90,8 +90,8 @@ public void testParseLogicalOrBothFalse() throws NbtParseException { @Test public void testParseLogicalOrRightTrue() throws NbtParseException { - // Test: (@.a < 2) || (> 10) with value 15 - INbtPathExpression expression = NbtPath.parse("@.a < 2 || > 10"); + // Test: (@.a < 2) || (@.a > 10) with value 15 + INbtPathExpression expression = NbtPath.parse("@.a < 2 || @.a > 10"); CompoundTag tag1 = new CompoundTag(); tag1.putInt("a", 15); // 15 < 2 is false, 15 > 10 is true, so false || true = true @@ -104,8 +104,8 @@ public void testParseLogicalOrRightTrue() throws NbtParseException { @Test public void testParseComplexLogicalExpression() throws NbtParseException { - // Test: (@.a > 5) && (< 15) || (== 20) - INbtPathExpression expression = NbtPath.parse("@.a > 5 && < 15 || == 20"); + // Test: (@.a > 5) && (@.a < 15) || (@.a == 20) + INbtPathExpression expression = NbtPath.parse("@.a > 5 && @.a < 15 || @.a == 20"); CompoundTag tag1 = new CompoundTag(); tag1.putInt("a", 10); // 10 > 5 is true, 10 < 15 is true, so (true && true) = true, true || false = true @@ -118,8 +118,8 @@ public void testParseComplexLogicalExpression() throws NbtParseException { @Test public void testParseLogicalWithFilterExpression() throws NbtParseException { - // Test filter expression with OR: [?(@.value < 5 || > 10)] - INbtPathExpression expression = NbtPath.parse("$.items[?(@.value < 5 || > 10)]"); + // Test filter expression with OR: [?(@.value < 5 || @.value > 10)] + INbtPathExpression expression = NbtPath.parse("$.items[?(@.value < 5 || @.value > 10)]"); CompoundTag root = new CompoundTag(); ListTag items = new ListTag(); @@ -149,8 +149,8 @@ public void testParseLogicalWithFilterExpression() throws NbtParseException { @Test public void testParseLogicalWithFilterExpressionAnd() throws NbtParseException { - // Test filter expression with AND: [?(@.min < 10 && > 5)] - INbtPathExpression expression = NbtPath.parse("$.items[?(@.min < 10 && > 5)]"); + // Test filter expression with AND: [?(@.min < 10 && @.min > 5)] + INbtPathExpression expression = NbtPath.parse("$.items[?(@.min < 10 && @.min > 5)]"); CompoundTag root = new CompoundTag(); ListTag items = new ListTag(); @@ -207,4 +207,63 @@ public void testParseLogicalNotWithFilterExpression() throws NbtParseException { assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } + + @Test + public void testParseNotEqualTrue() throws NbtParseException { + // Test: @.a != 5 where a is 3 + INbtPathExpression expression = NbtPath.parse("@.a != 5"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 3); // 3 != 5 is true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + } + + @Test + public void testParseNotEqualFalse() throws NbtParseException { + // Test: @.a != 5 where a is 5 + INbtPathExpression expression = NbtPath.parse("@.a != 5"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); // 5 != 5 is false + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag1), is(false)); + } + + @Test + public void testParseNotEqualWithFilterExpression() throws NbtParseException { + // Test filter expression with !=: [?(@.status != 0)] + INbtPathExpression expression = NbtPath.parse("$.items[?(@.status != 0)]"); + + CompoundTag root = new CompoundTag(); + ListTag items = new ListTag(); + + CompoundTag item1 = new CompoundTag(); + item1.putInt("status", 1); // 1 != 0 is true, should match + + CompoundTag item2 = new CompoundTag(); + item2.putInt("status", 0); // 0 != 0 is false, should not match + + CompoundTag item3 = new CompoundTag(); + item3.putInt("status", 2); // 2 != 0 is true, should match + + items.add(item1); + items.add(item2); + items.add(item3); + root.put("items", items); + + ListTag expectedFiltered = new ListTag(); + expectedFiltered.add(item1); + expectedFiltered.add(item3); + + List expected = Lists.newArrayList(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + assertThat(expression.test(root), is(true)); + } } diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java new file mode 100644 index 00000000000..10f26618e88 --- /dev/null +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java @@ -0,0 +1,65 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import com.google.common.collect.Lists; +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.IntTag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.junit.Before; +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class TestNbtPathExpressionHandlerBooleanRelationalNotEqual { + + private NbtPathExpressionParseHandlerBooleanRelationalNotEqual handler; + + @Before + public void beforeEach() { + handler = new NbtPathExpressionParseHandlerBooleanRelationalNotEqual(); + } + + @Test + public void testNonMatch() { + assertThat(handler.handlePrefixOf("$", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testMatchExpressionInt() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("!=2", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(3)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanRelationalNotEqual.Expression.class)); + assertThat(((NbtPathExpressionParseHandlerBooleanRelationalNotEqual.Expression) result.getPrefixExpression()).getTargetDouble(), is(2D)); + } + + @Test + public void testMatchExpressionDoubleSpaces() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" != 24.23", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getConsumedExpressionLength(), is(9)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanRelationalNotEqual.Expression.class)); + assertThat(((NbtPathExpressionParseHandlerBooleanRelationalNotEqual.Expression) result.getPrefixExpression()).getTargetDouble(), is(24.23D)); + } + + @Test + public void testExpressionStreamSingleLeafIntValid() { + INbtPathExpression expression = handler.handlePrefixOf("aa != 3", 2).getPrefixExpression(); + assertThat(expression.match(Stream.of(IntTag.valueOf(5))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); + } + + @Test + public void testExpressionStreamSingleLeafIntInvalid() { + INbtPathExpression expression = handler.handlePrefixOf("aa != 3", 2).getPrefixExpression(); + assertThat(expression.match(Stream.of(IntTag.valueOf(3))).getMatches().collect(Collectors.toList()), + is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); + } + +} From 5e9c0a117a94e0460195168c7fae83bf6d4b2204 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 08:44:59 +0000 Subject: [PATCH 06/16] Add test for different fields in AND/OR expressions Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../nbt/path/TestNbtPathLogicalOperators.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java index f1d3ccb8f8a..55b8b1f96f2 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -266,4 +266,28 @@ public void testParseNotEqualWithFilterExpression() throws NbtParseException { assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } + + @Test + public void testParseLogicalAndDifferentFields() throws NbtParseException { + // Test: (@.a > 10) && (@.b < 5) with different fields + INbtPathExpression expression = NbtPath.parse("@.a > 10 && @.b < 5"); + + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 15); + tag1.putInt("b", 3); // 15 > 10 is true, 3 < 5 is true, so true && true = true + + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + + CompoundTag tag2 = new CompoundTag(); + tag2.putInt("a", 15); + tag2.putInt("b", 8); // 15 > 10 is true, 8 < 5 is false, so true && false = false + + assertThat(expression.match(Stream.of(tag2)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag2), is(false)); + } } From 4e87561be0202fa37b465915f76f203737f06529 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Sat, 14 Feb 2026 10:55:34 +0100 Subject: [PATCH 07/16] Fix spotless errors --- .../cyclopscore/nbt/path/TestNbtPathLogicalOperators.java | 1 - .../TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java | 1 - 2 files changed, 2 deletions(-) diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java index 55b8b1f96f2..8633b34dd62 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -3,7 +3,6 @@ import com.google.common.collect.Lists; import net.minecraft.nbt.ByteTag; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.IntTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import org.junit.Test; diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java index 10f26618e88..20314d5eed2 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanRelationalNotEqual.java @@ -2,7 +2,6 @@ import com.google.common.collect.Lists; import net.minecraft.nbt.ByteTag; -import net.minecraft.nbt.DoubleTag; import net.minecraft.nbt.IntTag; import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; import org.junit.Before; From 243a79482d8c06fcada90cb295928191a6266d67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:05:59 +0000 Subject: [PATCH 08/16] Fix failing tests for logical operators Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- ...btPathExpressionParseHandlerBooleanLogicalAnd.java | 2 +- ...NbtPathExpressionParseHandlerBooleanLogicalOr.java | 2 +- .../nbt/path/TestNbtPathLogicalOperators.java | 11 ++++++----- ...TestNbtPathExpressionHandlerBooleanLogicalNot.java | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java index b01efc4748c..7a93595c28e 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -69,7 +69,7 @@ public NbtPathExpressionMatches matchContexts(Stream Date: Sun, 15 Feb 2026 07:24:18 +0100 Subject: [PATCH 09/16] Improve logical expression abstraction --- .github/workflows/copilot-setup-steps.yml | 41 ++++++++ ...sionParseHandlerBooleanLogicalAdapter.java | 94 +++++++++++++++++++ ...pressionParseHandlerBooleanLogicalAnd.java | 81 ++-------------- ...pressionParseHandlerBooleanLogicalNot.java | 2 +- ...xpressionParseHandlerBooleanLogicalOr.java | 81 ++-------------- .../nbt/path/TestNbtPathLogicalOperators.java | 10 +- 6 files changed, 155 insertions(+), 154 deletions(-) create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000000..20fbcca973b --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,41 @@ +name: "Copilot Setup Steps" + +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + permissions: + contents: read + + # Steps that will run before the agent starts + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + submodules: true + - name: 'Cache' + uses: actions/cache@v4 + with: + path: | + ~/.m2 + ~/.gradle + key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle') }} + - name: 'Setup Java' + uses: actions/setup-java@v1 + with: + java-version: 17 + java-package: jdk + - name: 'Build' + run: ./gradlew build --max-workers 1 + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_KEY: ${{ secrets.MAVEN_KEY }} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java new file mode 100644 index 00000000000..d389a4550c7 --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java @@ -0,0 +1,94 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.Tag; +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.cyclops.cyclopscore.nbt.path.NbtParseException; +import org.cyclops.cyclopscore.nbt.path.NbtPath; +import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; + +import javax.annotation.Nullable; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * @author rubensworks + */ +public abstract class NbtPathExpressionParseHandlerBooleanLogicalAdapter implements INbtPathExpressionParseHandler { + + private final Pattern regex; + + protected NbtPathExpressionParseHandlerBooleanLogicalAdapter(String relation) { + this.regex = Pattern.compile("^ *" + relation + " *"); + } + + protected abstract boolean getLogicalValue(boolean left, Supplier right); + + @Nullable + @Override + public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { + Matcher matcher = this.regex + .matcher(nbtPathExpression) + .region(pos, nbtPathExpression.length()); + if (!matcher.find()) { + return HandleResult.INVALID; + } + + // Parse the right-hand side expression + int rightPos = pos + matcher.group().length(); + if (rightPos >= nbtPathExpression.length()) { + return HandleResult.INVALID; + } + + // Find the end of the right expression + int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); + if (endPos == rightPos) { + return HandleResult.INVALID; + } + + String rightExpressionString = nbtPathExpression.substring(rightPos, endPos); + try { + INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString.trim()); + return new HandleResult(new Expression(rightExpression, this), + matcher.group().length() + rightExpressionString.length()); + } catch (NbtParseException e) { + return HandleResult.INVALID; + } + } + + public static class Expression implements INbtPathExpression { + + protected final INbtPathExpression expression; + protected final NbtPathExpressionParseHandlerBooleanLogicalAdapter handler; + + public Expression(INbtPathExpression expression, NbtPathExpressionParseHandlerBooleanLogicalAdapter handler) { + this.expression = expression; + this.handler = handler; + } + + @Override + public NbtPathExpressionMatches matchContexts(Stream executionContexts) { + return new NbtPathExpressionMatches(executionContexts + .map(executionContext -> { + Tag currentTag = executionContext.getCurrentTag(); + + // The left side is the current tag (should be a boolean result from previous expression) + boolean leftValue = NbtPathExpressionHelpers.isTruthy(currentTag); + + // Evaluate the right expression against the root context + // This ensures both sides of the expression are evaluated against the same base context + Tag rootTag = executionContext.getRootContext().getCurrentTag(); + + // AND operation + boolean result = handler.getLogicalValue(leftValue, () -> expression.test(rootTag)); + + return new NbtPathExpressionExecutionContext(ByteTag.valueOf(result), executionContext); + }) + ); + } + + } + +} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java index 7a93595c28e..2274cd0fc0d 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAnd.java @@ -1,85 +1,18 @@ package org.cyclops.cyclopscore.nbt.path.parse; -import net.minecraft.nbt.ByteTag; -import net.minecraft.nbt.Tag; -import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; -import org.cyclops.cyclopscore.nbt.path.NbtParseException; -import org.cyclops.cyclopscore.nbt.path.NbtPath; -import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; - -import javax.annotation.Nullable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; +import java.util.function.Supplier; /** * A handler that handles boolean AND expressions in the form of "expression1 {@literal &&} expression2". */ -public class NbtPathExpressionParseHandlerBooleanLogicalAnd implements INbtPathExpressionParseHandler { - - private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *&& *"); - - @Nullable - @Override - public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { - Matcher matcher = REGEX_EXPRESSION - .matcher(nbtPathExpression) - .region(pos, nbtPathExpression.length()); - if (!matcher.find()) { - return HandleResult.INVALID; - } +public class NbtPathExpressionParseHandlerBooleanLogicalAnd extends NbtPathExpressionParseHandlerBooleanLogicalAdapter { - // Parse the right-hand side expression - int rightPos = pos + matcher.group().length(); - if (rightPos >= nbtPathExpression.length()) { - return HandleResult.INVALID; - } - - // Find the end of the right expression - int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); - if (endPos == rightPos) { - return HandleResult.INVALID; - } - - String rightExpressionString = nbtPathExpression.substring(rightPos, endPos); - try { - INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString); - return new HandleResult(new Expression(rightExpression), - matcher.group().length() + rightExpressionString.length()); - } catch (NbtParseException e) { - return HandleResult.INVALID; - } + public NbtPathExpressionParseHandlerBooleanLogicalAnd() { + super("&&"); } - public static class Expression implements INbtPathExpression { - - protected final INbtPathExpression expression; - - public Expression(INbtPathExpression expression) { - this.expression = expression; - } - - @Override - public NbtPathExpressionMatches matchContexts(Stream executionContexts) { - return new NbtPathExpressionMatches(executionContexts - .map(executionContext -> { - Tag currentTag = executionContext.getCurrentTag(); - - // The left side is the current tag (should be a boolean result from previous expression) - boolean leftValue = NbtPathExpressionHelpers.isTruthy(currentTag); - - // Evaluate the right expression against the root context - // This ensures both sides of the expression are evaluated against the same base context - Tag rootTag = executionContext.getRootContext().getCurrentTag(); - boolean rightValue = expression.test(rootTag); - - // AND operation - boolean result = leftValue && rightValue; - - return new NbtPathExpressionExecutionContext( - ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext); - }) - ); - } + @Override + protected boolean getLogicalValue(boolean left, Supplier right) { + return left && right.get(); } } diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java index dbe4fbdf60b..d2bc4493940 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java @@ -43,7 +43,7 @@ public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { String expressionString = nbtPathExpression.substring(exprPos, endPos); try { - INbtPathExpression expression = NbtPath.parse(expressionString); + INbtPathExpression expression = NbtPath.parse(expressionString.trim()); return new HandleResult(new Expression(expression), matcher.group().length() + expressionString.length()); } catch (NbtParseException e) { diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java index 7e856cac39d..e6339e45b2d 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalOr.java @@ -1,85 +1,18 @@ package org.cyclops.cyclopscore.nbt.path.parse; -import net.minecraft.nbt.ByteTag; -import net.minecraft.nbt.Tag; -import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; -import org.cyclops.cyclopscore.nbt.path.NbtParseException; -import org.cyclops.cyclopscore.nbt.path.NbtPath; -import org.cyclops.cyclopscore.nbt.path.NbtPathExpressionMatches; - -import javax.annotation.Nullable; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; +import java.util.function.Supplier; /** * A handler that handles boolean OR expressions in the form of "expression1 {@literal ||} expression2". */ -public class NbtPathExpressionParseHandlerBooleanLogicalOr implements INbtPathExpressionParseHandler { - - private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *\\|\\| *"); - - @Nullable - @Override - public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { - Matcher matcher = REGEX_EXPRESSION - .matcher(nbtPathExpression) - .region(pos, nbtPathExpression.length()); - if (!matcher.find()) { - return HandleResult.INVALID; - } +public class NbtPathExpressionParseHandlerBooleanLogicalOr extends NbtPathExpressionParseHandlerBooleanLogicalAdapter { - // Parse the right-hand side expression - int rightPos = pos + matcher.group().length(); - if (rightPos >= nbtPathExpression.length()) { - return HandleResult.INVALID; - } - - // Find the end of the right expression - int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); - if (endPos == rightPos) { - return HandleResult.INVALID; - } - - String rightExpressionString = nbtPathExpression.substring(rightPos, endPos); - try { - INbtPathExpression rightExpression = NbtPath.parse(rightExpressionString); - return new HandleResult(new Expression(rightExpression), - matcher.group().length() + rightExpressionString.length()); - } catch (NbtParseException e) { - return HandleResult.INVALID; - } + public NbtPathExpressionParseHandlerBooleanLogicalOr() { + super("\\|\\|"); } - public static class Expression implements INbtPathExpression { - - protected final INbtPathExpression expression; - - public Expression(INbtPathExpression expression) { - this.expression = expression; - } - - @Override - public NbtPathExpressionMatches matchContexts(Stream executionContexts) { - return new NbtPathExpressionMatches(executionContexts - .map(executionContext -> { - Tag currentTag = executionContext.getCurrentTag(); - - // The left side is the current tag (should be a boolean result from previous expression) - boolean leftValue = NbtPathExpressionHelpers.isTruthy(currentTag); - - // Evaluate the right expression against the root context - // This ensures both sides of the expression are evaluated against the same base context - Tag rootTag = executionContext.getRootContext().getCurrentTag(); - boolean rightValue = expression.test(rootTag); - - // OR operation - boolean result = leftValue || rightValue; - - return new NbtPathExpressionExecutionContext( - ByteTag.valueOf(result ? (byte) 1 : (byte) 0), executionContext); - }) - ); - } + @Override + protected boolean getLogicalValue(boolean left, Supplier right) { + return left || right.get(); } } diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java index f7ac38c0e57..050fdb2de3b 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -141,7 +141,7 @@ public void testParseLogicalWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -171,7 +171,7 @@ public void testParseLogicalWithFilterExpressionAnd() throws NbtParseException { ListTag expectedFiltered = new ListTag(); expectedFiltered.add(item1); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -203,7 +203,7 @@ public void testParseLogicalNotWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -262,8 +262,8 @@ public void testParseNotEqualWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); - assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + List expected = List.of(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().toList(), equalTo(expected)); assertThat(expression.test(root), is(true)); } From 0a52d1a076e8f7ad2c7f79655d53e3cc7ff4f0f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 06:29:33 +0000 Subject: [PATCH 10/16] Refactor logical operators and move helpers to adapter class Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../path/parse/NbtPathExpressionHelpers.java | 67 ---------- ...sionParseHandlerBooleanLogicalAdapter.java | 61 ++++++++- ...pressionParseHandlerBooleanLogicalNot.java | 48 ++++++- .../nbt/path/TestNbtPathLogicalOperators.java | 120 ++++++++++++++++++ ...athExpressionHandlerBooleanLogicalNot.java | 67 ++++++---- 5 files changed, 266 insertions(+), 97 deletions(-) delete mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java deleted file mode 100644 index a1dc7d18403..00000000000 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionHelpers.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.cyclops.cyclopscore.nbt.path.parse; - -import net.minecraft.nbt.ByteTag; -import net.minecraft.nbt.Tag; - -/** - * Utility methods for NBT path expression handling. - */ -public class NbtPathExpressionHelpers { - - /** - * Determine if a tag is truthy. - * ByteTag with value 1 is true, 0 is false. - * Any other non-null tag is considered true. - * This follows the same logic as {@link org.cyclops.cyclopscore.nbt.path.INbtPathExpression#test(Tag)}. - * - * @param tag The tag to check - * @return true if the tag is truthy, false otherwise - */ - public static boolean isTruthy(Tag tag) { - if (tag == null) { - return false; - } - if (tag.getId() == Tag.TAG_BYTE) { - return ((ByteTag) tag).getAsByte() == (byte) 1; - } - // Non-null non-ByteTags are truthy - return true; - } - - /** - * Find the end position of an expression, stopping at logical operators or closing parenthesis. - * This method is shared by logical operator handlers to identify expression boundaries. - * - * @param expression The full expression string - * @param start The starting position to search from - * @return The position where the expression ends - */ - public static int findExpressionEnd(String expression, int start) { - int depth = 0; - for (int i = start; i < expression.length(); i++) { - char c = expression.charAt(i); - - if (c == '(') { - depth++; - } else if (c == ')') { - if (depth == 0) { - return i; - } - depth--; - } else if (depth == 0) { - // Check for logical operators at top level - if (i + 1 < expression.length()) { - String twoChar = expression.substring(i, i + 2); - if (twoChar.equals("&&") || twoChar.equals("||")) { - return i; - } - } - // Check for NOT operator (but not != which is handled differently) - if (c == '!' && (i + 1 >= expression.length() || expression.charAt(i + 1) != '=')) { - return i; - } - } - } - return expression.length(); - } -} diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java index d389a4550c7..062b4f97487 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalAdapter.java @@ -26,6 +26,63 @@ protected NbtPathExpressionParseHandlerBooleanLogicalAdapter(String relation) { protected abstract boolean getLogicalValue(boolean left, Supplier right); + /** + * Determine if a tag is truthy. + * ByteTag with value 1 is true, 0 is false. + * Any other non-null tag is considered true. + * This follows the same logic as {@link org.cyclops.cyclopscore.nbt.path.INbtPathExpression#test(Tag)}. + * + * @param tag The tag to check + * @return true if the tag is truthy, false otherwise + */ + public static boolean isTruthy(Tag tag) { + if (tag == null) { + return false; + } + if (tag.getId() == Tag.TAG_BYTE) { + return ((ByteTag) tag).getAsByte() == (byte) 1; + } + // Non-null non-ByteTags are truthy + return true; + } + + /** + * Find the end position of an expression, stopping at logical operators or closing parenthesis. + * This method is shared by logical operator handlers to identify expression boundaries. + * + * @param expression The full expression string + * @param start The starting position to search from + * @return The position where the expression ends + */ + public static int findExpressionEnd(String expression, int start) { + int depth = 0; + for (int i = start; i < expression.length(); i++) { + char c = expression.charAt(i); + + if (c == '(') { + depth++; + } else if (c == ')') { + if (depth == 0) { + return i; + } + depth--; + } else if (depth == 0) { + // Check for logical operators at top level + if (i + 1 < expression.length()) { + String twoChar = expression.substring(i, i + 2); + if (twoChar.equals("&&") || twoChar.equals("||")) { + return i; + } + } + // Check for NOT operator (but not != which is handled differently) + if (c == '!' && (i + 1 >= expression.length() || expression.charAt(i + 1) != '=')) { + return i; + } + } + } + return expression.length(); + } + @Nullable @Override public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { @@ -43,7 +100,7 @@ public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { } // Find the end of the right expression - int endPos = NbtPathExpressionHelpers.findExpressionEnd(nbtPathExpression, rightPos); + int endPos = findExpressionEnd(nbtPathExpression, rightPos); if (endPos == rightPos) { return HandleResult.INVALID; } @@ -75,7 +132,7 @@ public NbtPathExpressionMatches matchContexts(Stream 5 && (@.a < 15 || @.a == 20) + // This should evaluate as: (a > 5) AND ((a < 15) OR (a == 20)) + INbtPathExpression expression = NbtPath.parse("@.a > 5 && (@.a < 15 || @.a == 20)"); + + // Test with a=10: 10 > 5 is true, (10 < 15 is true || 10 == 20 is false) = true, so true && true = true + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 10); + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + + // Test with a=20: 20 > 5 is true, (20 < 15 is false || 20 == 20 is true) = true, so true && true = true + CompoundTag tag2 = new CompoundTag(); + tag2.putInt("a", 20); + assertThat(expression.match(Stream.of(tag2)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag2), is(true)); + + // Test with a=3: 3 > 5 is false, doesn't matter what's in parentheses, false && anything = false + CompoundTag tag3 = new CompoundTag(); + tag3.putInt("a", 3); + assertThat(expression.match(Stream.of(tag3)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag3), is(false)); + + // Test with a=17: 17 > 5 is true, (17 < 15 is false || 17 == 20 is false) = false, so true && false = false + CompoundTag tag4 = new CompoundTag(); + tag4.putInt("a", 17); + assertThat(expression.match(Stream.of(tag4)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag4), is(false)); + } + + @Test + public void testParseParenthesesWithAnd() throws NbtParseException { + // Test: (@.a > 5 && @.a < 15) || @.a == 20 + // This should evaluate as: ((a > 5) AND (a < 15)) OR (a == 20) + INbtPathExpression expression = NbtPath.parse("(@.a > 5 && @.a < 15) || @.a == 20"); + + // Test with a=10: (10 > 5 is true && 10 < 15 is true) = true, 10 == 20 is false, so true || false = true + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 10); + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + + // Test with a=20: (20 > 5 is true && 20 < 15 is false) = false, 20 == 20 is true, so false || true = true + CompoundTag tag2 = new CompoundTag(); + tag2.putInt("a", 20); + assertThat(expression.match(Stream.of(tag2)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag2), is(true)); + + // Test with a=3: (3 > 5 is false && anything) = false, 3 == 20 is false, so false || false = false + CompoundTag tag3 = new CompoundTag(); + tag3.putInt("a", 3); + assertThat(expression.match(Stream.of(tag3)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag3), is(false)); + } + + @Test + public void testParseNestedParentheses() throws NbtParseException { + // Test: ((@.a > 5 && @.a < 15) || @.a == 20) && @.b != 0 + // This tests nested parentheses with multiple levels + INbtPathExpression expression = NbtPath.parse("((@.a > 5 && @.a < 15) || @.a == 20) && @.b != 0"); + + // Test with a=10, b=1: inner AND is true, OR with false is true, outer AND with true is true + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 10); + tag1.putInt("b", 1); + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + + // Test with a=10, b=0: left side is true but b != 0 is false, so true && false = false + CompoundTag tag2 = new CompoundTag(); + tag2.putInt("a", 10); + tag2.putInt("b", 0); + assertThat(expression.match(Stream.of(tag2)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag2), is(false)); + } + + @Test + public void testParseParenthesesWithNot() throws NbtParseException { + // Test: !(@.a > 10) && @.b < 5 + // This tests NOT with parentheses combined with AND + INbtPathExpression expression = NbtPath.parse("!(@.a > 10) && @.b < 5"); + + // Test with a=5, b=3: !(5 > 10) = !false = true, 3 < 5 = true, so true && true = true + CompoundTag tag1 = new CompoundTag(); + tag1.putInt("a", 5); + tag1.putInt("b", 3); + assertThat(expression.match(Stream.of(tag1)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 1) + ))); + assertThat(expression.test(tag1), is(true)); + + // Test with a=15, b=3: !(15 > 10) = !true = false, 3 < 5 = true, so false && true = false + CompoundTag tag2 = new CompoundTag(); + tag2.putInt("a", 15); + tag2.putInt("b", 3); + assertThat(expression.match(Stream.of(tag2)).getMatches().collect(Collectors.toList()), equalTo(Lists.newArrayList( + ByteTag.valueOf((byte) 0) + ))); + assertThat(expression.test(tag2), is(false)); + } } diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java index 6c19922940e..37afc7ef164 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java @@ -2,6 +2,7 @@ import com.google.common.collect.Lists; import net.minecraft.nbt.ByteTag; +import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.IntTag; import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; import org.junit.Before; @@ -30,58 +31,78 @@ public void testNonMatch() { } @Test - public void testMatchExpressionSimple() { - INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("!< 10", 0); + public void testNonMatchPartialExpression() { + // Should not match partial expressions like "!< 10" + assertThat(handler.handlePrefixOf("!< 10", 0), + is(INbtPathExpressionParseHandler.HandleResult.INVALID)); + } + + @Test + public void testMatchExpressionWithParentheses() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("!(@.a < 10)", 0); + assertThat(result.isValid(), is(true)); + assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalNot.Expression.class)); + } + + @Test + public void testMatchExpressionWithPath() { + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf("!@.a == 5", 0); assertThat(result.isValid(), is(true)); - assertThat(result.getConsumedExpressionLength(), is(5)); assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalNot.Expression.class)); } @Test public void testMatchExpressionWithSpaces() { - INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" ! > 5", 0); + INbtPathExpressionParseHandler.HandleResult result = handler.handlePrefixOf(" ! (@.a > 5)", 0); assertThat(result.isValid(), is(true)); - assertThat(result.getConsumedExpressionLength(), is(8)); assertThat(result.getPrefixExpression(), instanceOf(NbtPathExpressionParseHandlerBooleanLogicalNot.Expression.class)); } @Test public void testExpressionStreamNegateTrue() { - // Create an expression that evaluates "!< 10" - INbtPathExpression expression = handler.handlePrefixOf("!< 10", 0).getPrefixExpression(); + // Create an expression that evaluates "!(@.a < 10)" + INbtPathExpression expression = handler.handlePrefixOf("!(@.a < 10)", 0).getPrefixExpression(); + CompoundTag tag = new CompoundTag(); + tag.putInt("a", 5); // 5 < 10 is true, so !(true) should be false - assertThat(expression.match(Stream.of(IntTag.valueOf(5))).getMatches().collect(Collectors.toList()), + assertThat(expression.match(Stream.of(tag)).getMatches().collect(Collectors.toList()), is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); } @Test public void testExpressionStreamNegateFalse() { - // Create an expression that evaluates "!< 10" - INbtPathExpression expression = handler.handlePrefixOf("!< 10", 0).getPrefixExpression(); + // Create an expression that evaluates "!(@.a < 10)" + INbtPathExpression expression = handler.handlePrefixOf("!(@.a < 10)", 0).getPrefixExpression(); + CompoundTag tag = new CompoundTag(); + tag.putInt("a", 15); // 15 < 10 is false, so !(false) should be true - assertThat(expression.match(Stream.of(IntTag.valueOf(15))).getMatches().collect(Collectors.toList()), + assertThat(expression.match(Stream.of(tag)).getMatches().collect(Collectors.toList()), is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); } @Test - public void testExpressionStreamNegateByteTagTrue() { - // Create an expression that evaluates "! == 1" (with space to avoid != operator) - INbtPathExpression expression = handler.handlePrefixOf("! == 1", 0).getPrefixExpression(); - - // 1 == 1 is true, so !(true) should be false - assertThat(expression.match(Stream.of(ByteTag.valueOf((byte) 1))).getMatches().collect(Collectors.toList()), + public void testExpressionStreamNegatePathTrue() { + // Create an expression that evaluates "!@.a == 1" + INbtPathExpression expression = handler.handlePrefixOf("!@.a == 1", 0).getPrefixExpression(); + + CompoundTag tag = new CompoundTag(); + tag.putInt("a", 1); + // @.a == 1 evaluates to: get a (=1), then == 1, result is true, so !(true) should be false + assertThat(expression.match(Stream.of(tag)).getMatches().collect(Collectors.toList()), is(Lists.newArrayList(ByteTag.valueOf((byte) 0)))); } @Test - public void testExpressionStreamNegateByteTagFalse() { - // Create an expression that evaluates "! == 1" (with space to avoid != operator) - INbtPathExpression expression = handler.handlePrefixOf("! == 1", 0).getPrefixExpression(); - - // 0 == 1 is false, so !(false) should be true - assertThat(expression.match(Stream.of(ByteTag.valueOf((byte) 0))).getMatches().collect(Collectors.toList()), + public void testExpressionStreamNegatePathFalse() { + // Create an expression that evaluates "!@.a == 1" + INbtPathExpression expression = handler.handlePrefixOf("!@.a == 1", 0).getPrefixExpression(); + + CompoundTag tag = new CompoundTag(); + tag.putInt("a", 0); + // @.a == 1 evaluates to: get a (=0), then == 1, result is false, so !(false) should be true + assertThat(expression.match(Stream.of(tag)).getMatches().collect(Collectors.toList()), is(Lists.newArrayList(ByteTag.valueOf((byte) 1)))); } } From 73168b54cfbeb5bce0e4de17b3763b9c262a5498 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Sun, 15 Feb 2026 07:36:28 +0100 Subject: [PATCH 11/16] Remove copilot-setup-steps.yml workflow --- .github/workflows/copilot-setup-steps.yml | 41 ----------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/copilot-setup-steps.yml diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index 20fbcca973b..00000000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -jobs: - # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. - copilot-setup-steps: - runs-on: ubuntu-latest - permissions: - contents: read - - # Steps that will run before the agent starts - steps: - - name: 'Checkout' - uses: actions/checkout@v2 - with: - submodules: true - - name: 'Cache' - uses: actions/cache@v4 - with: - path: | - ~/.m2 - ~/.gradle - key: ${{ runner.os }}-gradle-${{ hashFiles('build.gradle') }} - - name: 'Setup Java' - uses: actions/setup-java@v1 - with: - java-version: 17 - java-package: jdk - - name: 'Build' - run: ./gradlew build --max-workers 1 - env: - MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} - MAVEN_KEY: ${{ secrets.MAVEN_KEY }} From 9938ad19f17349bb02c50786aa53a182498dc7e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 06:47:02 +0000 Subject: [PATCH 12/16] Fix Java compatibility issues in test files Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../nbt/path/TestNbtPathLogicalOperators.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java index 05717b737ae..59b762d67ba 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -141,7 +141,7 @@ public void testParseLogicalWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = List.of(expectedFiltered); + List expected = Lists.newArrayList(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -171,7 +171,7 @@ public void testParseLogicalWithFilterExpressionAnd() throws NbtParseException { ListTag expectedFiltered = new ListTag(); expectedFiltered.add(item1); - List expected = List.of(expectedFiltered); + List expected = Lists.newArrayList(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -203,7 +203,7 @@ public void testParseLogicalNotWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = List.of(expectedFiltered); + List expected = Lists.newArrayList(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -262,8 +262,8 @@ public void testParseNotEqualWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = List.of(expectedFiltered); - assertThat(expression.match(Stream.of(root)).getMatches().toList(), equalTo(expected)); + List expected = Lists.newArrayList(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } From 7041de89e53f6b38cb8d2522c65ed5f06d3dda4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 06:57:58 +0000 Subject: [PATCH 13/16] Revert "Fix Java compatibility issues in test files" This reverts commit 9938ad19f17349bb02c50786aa53a182498dc7e6. --- .../nbt/path/TestNbtPathLogicalOperators.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java index 59b762d67ba..05717b737ae 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/TestNbtPathLogicalOperators.java @@ -141,7 +141,7 @@ public void testParseLogicalWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -171,7 +171,7 @@ public void testParseLogicalWithFilterExpressionAnd() throws NbtParseException { ListTag expectedFiltered = new ListTag(); expectedFiltered.add(item1); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -203,7 +203,7 @@ public void testParseLogicalNotWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); + List expected = List.of(expectedFiltered); assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); assertThat(expression.test(root), is(true)); } @@ -262,8 +262,8 @@ public void testParseNotEqualWithFilterExpression() throws NbtParseException { expectedFiltered.add(item1); expectedFiltered.add(item3); - List expected = Lists.newArrayList(expectedFiltered); - assertThat(expression.match(Stream.of(root)).getMatches().collect(Collectors.toList()), equalTo(expected)); + List expected = List.of(expectedFiltered); + assertThat(expression.match(Stream.of(root)).getMatches().toList(), equalTo(expected)); assertThat(expression.test(root), is(true)); } From 594e5dda8ddf236b22b8da143a89f009ff181d8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:25:48 +0000 Subject: [PATCH 14/16] Add parentheses grouping handler to fix failing tests Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../cyclops/cyclopscore/nbt/path/NbtPath.java | 1 + ...NbtPathExpressionParseHandlerGrouping.java | 72 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerGrouping.java diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java index 854fcdddf9a..fd308162fec 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/NbtPath.java @@ -21,6 +21,7 @@ public class NbtPath { new NbtPathExpressionParseHandlerListElement(), new NbtPathExpressionParseHandlerListSlice(), new NbtPathExpressionParseHandlerUnion(), + new NbtPathExpressionParseHandlerGrouping(), new NbtPathExpressionParseHandlerBooleanRelationalLessThan(), new NbtPathExpressionParseHandlerBooleanRelationalLessThanOrEqual(), new NbtPathExpressionParseHandlerBooleanRelationalGreaterThan(), diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerGrouping.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerGrouping.java new file mode 100644 index 00000000000..ab5fa6bd435 --- /dev/null +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerGrouping.java @@ -0,0 +1,72 @@ +package org.cyclops.cyclopscore.nbt.path.parse; + +import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; +import org.cyclops.cyclopscore.nbt.path.NbtParseException; +import org.cyclops.cyclopscore.nbt.path.NbtPath; + +import javax.annotation.Nullable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A handler that handles parenthesized expressions for grouping: "(expression)". + * This allows precedence control in complex logical expressions. + */ +public class NbtPathExpressionParseHandlerGrouping implements INbtPathExpressionParseHandler { + + private static final Pattern REGEX_EXPRESSION = Pattern.compile("^ *\\("); + + @Nullable + @Override + public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { + Matcher matcher = REGEX_EXPRESSION + .matcher(nbtPathExpression) + .region(pos, nbtPathExpression.length()); + if (!matcher.find()) { + return HandleResult.INVALID; + } + + // Find the matching closing parenthesis + int openPos = pos + matcher.group().length() - 1; // Position of '(' + int closePos = findMatchingClosingParenthesis(nbtPathExpression, openPos); + if (closePos == -1) { + return HandleResult.INVALID; + } + + // Extract the expression inside the parentheses + String innerExpression = nbtPathExpression.substring(openPos + 1, closePos); + + try { + // Parse the inner expression + INbtPathExpression expression = NbtPath.parse(innerExpression.trim()); + + // The grouping itself doesn't change the expression, it just controls precedence + // So we return the inner expression directly wrapped in a pass-through + return new HandleResult(expression, matcher.group().length() + innerExpression.length() + 1); + } catch (NbtParseException e) { + return HandleResult.INVALID; + } + } + + /** + * Find the matching closing parenthesis for an opening parenthesis. + * @param expression The expression string + * @param openPos The position of the opening parenthesis + * @return The position of the matching closing parenthesis, or -1 if not found + */ + private int findMatchingClosingParenthesis(String expression, int openPos) { + int depth = 0; + for (int i = openPos; i < expression.length(); i++) { + char c = expression.charAt(i); + if (c == '(') { + depth++; + } else if (c == ')') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; + } +} From 082c791c2d8e3bd85d6b97d2e87969b71c6a9cba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 07:26:28 +0000 Subject: [PATCH 15/16] Final verification - all tests passing Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java | 2 +- .../parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java index 6e842a2ec56..618cfd4c62b 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java @@ -40,7 +40,7 @@ public HandleResult handlePrefixOf(String nbtPathExpression, int pos) { // Check if expression starts with parenthesis boolean hasParenthesis = nbtPathExpression.charAt(exprPos) == '('; int endPos; - + if (hasParenthesis) { // Find matching closing parenthesis endPos = findMatchingClosingParenthesis(nbtPathExpression, exprPos); diff --git a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java index 37afc7ef164..25446fb4d5e 100644 --- a/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java +++ b/src/test/java/org/cyclops/cyclopscore/nbt/path/parse/TestNbtPathExpressionHandlerBooleanLogicalNot.java @@ -3,7 +3,6 @@ import com.google.common.collect.Lists; import net.minecraft.nbt.ByteTag; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.IntTag; import org.cyclops.cyclopscore.nbt.path.INbtPathExpression; import org.junit.Before; import org.junit.Test; From 6c23a7ba1af84d130d911ac614e0f7b6851c9203 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 15 Feb 2026 08:26:51 +0000 Subject: [PATCH 16/16] Fix javadoc HTML malformed errors in NOT handler Co-authored-by: rubensworks <440384+rubensworks@users.noreply.github.com> --- .../parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java index 618cfd4c62b..2354f588b81 100644 --- a/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java +++ b/src/main/java/org/cyclops/cyclopscore/nbt/path/parse/NbtPathExpressionParseHandlerBooleanLogicalNot.java @@ -14,7 +14,7 @@ /** * A handler that handles boolean NOT expressions in the form of "!expression". - * Only accepts full expressions like "!(@.a < 15)" or "!@.a", not partial expressions like "!< 10". + * Only accepts full expressions like "!(@.a {@literal <} 15)" or "!@.a", not partial expressions like "!{@literal <} 10". */ public class NbtPathExpressionParseHandlerBooleanLogicalNot implements INbtPathExpressionParseHandler {