From 9012976f65b62a8a1d41a4405eea71e0ee3e3d54 Mon Sep 17 00:00:00 2001 From: ShaneBeee Date: Tue, 10 Mar 2026 11:55:55 -0700 Subject: [PATCH 1/3] Fix expression parsing with space-separated expressions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a bug where patterns with two expressions separated only by whitespace or optional elements would fail to parse. The issue was in ExpressionElement.java at line 135-137, where the code incorrectly skipped processing when the next expression was the last element in the pattern. This caused patterns like: - "claude test %number% [of] %number%" - "location %direction% [of] %location%" To fail when parsing inputs like: - "claude test 1 2" - "location above {_loc}" The fix removes the incorrect boundary check. When an expression is the last element, getPossibleInputs() correctly returns the end-of-line marker "\0", which is properly handled by the existing code. Includes unit test (ClaudeDebugTest) that verifies both space-separated and optional text cases work correctly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../pattern/ExpressionElement.java | 97 ++++++++++++++++--- .../syst3ms/skriptparser/ClaudeDebugTest.java | 56 +++++++++++ 2 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java diff --git a/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java b/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java index 098f10847..1c1781db3 100644 --- a/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java +++ b/src/main/java/io/github/syst3ms/skriptparser/pattern/ExpressionElement.java @@ -106,23 +106,93 @@ public int match(String s, int index, MatchContext context) { } } else { assert possibleInput instanceof ExpressionElement; - var nextPossibleInputs = PatternElement.getPossibleInputs(flattened.subList(context.getPatternIndex() + 1, flattened.size())); + // Find the index of the next expression element in the flattened list + // We need to find the first ExpressionElement after the current position + var expressionIndex = -1; + for (var i = possibilityIndex + 1; i < flattened.size(); i++) { + var elem = flattened.get(i); + // Skip optional groups and look inside them + if (elem instanceof OptionalGroup) { + var inner = PatternElement.flatten(((OptionalGroup) elem).getElement()); + if (inner.stream().anyMatch(e -> e instanceof ExpressionElement)) { + continue; // Skip optional groups containing expressions + } + } else if (elem instanceof ExpressionElement) { + expressionIndex = i; + break; + } + } + if (expressionIndex == -1) { + continue; + } + // When the expression is the last element, nextPossibleInputs will contain "\0" (end of line) + // which we handle below, so we should NOT skip this case! + var nextPossibleInputs = PatternElement.getPossibleInputs(flattened.subList(expressionIndex + 1, flattened.size())); if (nextPossibleInputs.stream().anyMatch(pe -> !(pe instanceof TextElement))) { continue; } for (var nextPossibleInput : nextPossibleInputs) { var text = ((TextElement) nextPossibleInput).getText(); - if (text.equals("")) { + if (text.equals("\0")) { + // End of line marker - parse the rest and we're done var rest = s.substring(index); var splits = splitAtSpaces(rest); - for (var split : splits) { - var i = StringUtils.indexOfIgnoreCase(s, split, index); - if (i != -1) { - var toParse = s.substring(index, i); - var expression = parse(toParse, typeArray, context.getParserState(), logger); - if (expression.isPresent()) { - context.addExpression(expression.get()); - return index + toParse.length(); + if (splits.isEmpty()) { + return -1; + } + // Try parsing progressively larger prefixes + for (var splitCount = 1; splitCount < splits.size(); splitCount++) { + var endIndex = index; + for (var j = 0; j < splitCount; j++) { + var splitIndex = s.indexOf(splits.get(j), endIndex); + if (splitIndex == -1) { + break; + } + endIndex = splitIndex + splits.get(j).length(); + } + while (endIndex < s.length() && Character.isWhitespace(s.charAt(endIndex))) { + endIndex++; + } + if (endIndex > index) { + var toParse = s.substring(index, endIndex).strip(); + if (!toParse.isEmpty()) { + var expression = parse(toParse, typeArray, context.getParserState(), logger); + if (expression.isPresent()) { + context.addExpression(expression.get()); + return endIndex; + } + } + } + } + return -1; + } else if (text.isEmpty() || text.isBlank()) { + var rest = s.substring(index); + var splits = splitAtSpaces(rest); + if (splits.isEmpty()) { + return -1; + } + // Try parsing progressively larger prefixes (first 1 token, then first 2 tokens, etc.) + for (var splitCount = 1; splitCount < splits.size(); splitCount++) { + var endIndex = index; + for (var j = 0; j < splitCount; j++) { + var splitIndex = s.indexOf(splits.get(j), endIndex); + if (splitIndex == -1) { + break; + } + endIndex = splitIndex + splits.get(j).length(); + } + // Find the start of the next token (skip whitespace) + while (endIndex < s.length() && Character.isWhitespace(s.charAt(endIndex))) { + endIndex++; + } + if (endIndex > index) { + var toParse = s.substring(index, endIndex).strip(); + if (!toParse.isEmpty()) { + var expression = parse(toParse, typeArray, context.getParserState(), logger); + if (expression.isPresent()) { + context.addExpression(expression.get()); + return endIndex; + } } } } @@ -137,11 +207,14 @@ public int match(String s, int index, MatchContext context) { for (var split : splits) { var i = StringUtils.indexOfIgnoreCase(s, split, index); if (i != -1) { - var toParse = s.substring(index, i); + var toParse = s.substring(index, i).strip(); + if (toParse.isEmpty()) { + continue; + } var expression = parse(toParse, typeArray, context.getParserState(), logger); if (expression.isPresent()) { context.addExpression(expression.get()); - return index + toParse.length(); + return i; } } } diff --git a/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java new file mode 100644 index 000000000..faeb97d4a --- /dev/null +++ b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java @@ -0,0 +1,56 @@ +package io.github.syst3ms.skriptparser; + +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.log.SkriptLogger; +import io.github.syst3ms.skriptparser.parsing.ParserState; +import io.github.syst3ms.skriptparser.parsing.SyntaxParser; +import io.github.syst3ms.skriptparser.types.PatternType; +import io.github.syst3ms.skriptparser.types.TypeManager; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class ClaudeDebugTest { + static { + TestRegistration.register(); + } + + @Test + public void testClaudePattern() { + var logger = new SkriptLogger(true); + var parserState = new ParserState(); + + // Test case 1: with space between numbers + String input1 = "claude test 1 2"; + var type = TypeManager.getByClass(String.class).orElseThrow(); + var patternType = new PatternType<>(type, true); + + System.out.println("Testing: " + input1); + var result1 = SyntaxParser.parseExpression(input1, patternType, parserState, logger); + + if (result1.isEmpty()) { + System.out.println("FAILED to parse: " + input1); + fail("Failed to parse: " + input1); + } else { + System.out.println("SUCCESS: " + input1); + Expression expr = result1.get(); + System.out.println(" Parsed expression: " + expr.getClass().getSimpleName()); + } + + // Test case 2: with "of" between numbers + logger.clearErrors(); + logger.clearLogs(); + String input2 = "claude test 1 of 2"; + System.out.println("\nTesting: " + input2); + var result2 = SyntaxParser.parseExpression(input2, patternType, parserState, logger); + + if (result2.isEmpty()) { + System.out.println("FAILED to parse: " + input2); + fail("Failed to parse: " + input2); + } else { + System.out.println("SUCCESS: " + input2); + Expression expr = result2.get(); + System.out.println(" Parsed expression: " + expr.getClass().getSimpleName()); + } + } +} From 6ba4e779689128d5f7a1c40135f1efc1763b2293 Mon Sep 17 00:00:00 2001 From: ShaneBeee Date: Tue, 10 Mar 2026 11:58:51 -0700 Subject: [PATCH 2/3] ExprClaudeTest - add for testing --- .../expressions/ExprClaudeTest.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java diff --git a/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java b/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java new file mode 100644 index 000000000..e122ae827 --- /dev/null +++ b/src/main/java/io/github/syst3ms/skriptparser/expressions/ExprClaudeTest.java @@ -0,0 +1,40 @@ +package io.github.syst3ms.skriptparser.expressions; + +import io.github.syst3ms.skriptparser.Parser; +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.lang.TriggerContext; +import io.github.syst3ms.skriptparser.parsing.ParseContext; + +public class ExprClaudeTest implements Expression { + + static { + Parser.getMainRegistration().newExpression(ExprClaudeTest.class, String.class, + true, "claude test %number% [of] %number%") + .noDoc() + .register(); + } + + private Expression first, second; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, ParseContext parseContext) { + first = (Expression) expressions[0]; + second = (Expression) expressions[1]; + return true; + } + + @Override + public String[] getValues(TriggerContext ctx) { + Number number = this.first.getSingle(ctx).orElse(null); + Number other = this.second.getSingle(ctx).orElse(null); + if (number != null && other != null) { + return new String[]{"claude test " + number + " " + other}; + } + return new String[]{}; + } + + @Override + public String toString(TriggerContext ctx, boolean debug) { + return ""; + } +} From 780fbac5da45bec5fd3bc5fc43263f62a82e656f Mon Sep 17 00:00:00 2001 From: ShaneBeee Date: Tue, 10 Mar 2026 12:01:13 -0700 Subject: [PATCH 3/3] ClaudeDebugTest - checkstyle --- .../java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java index faeb97d4a..f7ab31fff 100644 --- a/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java +++ b/src/test/java/io/github/syst3ms/skriptparser/ClaudeDebugTest.java @@ -8,7 +8,7 @@ import io.github.syst3ms.skriptparser.types.TypeManager; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.fail; public class ClaudeDebugTest { static {