From 5dd7ab81e85e8e1959cebd8bdb59b2b309316180 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 19:04:35 +0200 Subject: [PATCH 1/2] refactor(regex): route Perl conditionals through Joni Detect real Perl conditional constructs with the existing syntax-aware admission scanner so the temporary Java policy delegates their semantics and diagnostics to Joni. Add policy, direct-engine, and standard-Perl regression coverage for valid, backtracking, and malformed forms. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../runtime/regex/JoniRegexPattern.java | 32 +++++---- .../runtime/regex/RegexBackendPolicyTest.java | 29 ++++++++ .../unit/regex/conditional_native_routing.t | 69 +++++++++++++++++++ .../org/joni/test/TestPerlConditions.java | 40 +++++++++++ 4 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 src/test/resources/unit/regex/conditional_native_routing.t diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index a27b7de9f8..a925034c19 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -95,8 +95,8 @@ private static CharacterPropertyResolver.Result resolveCharacterProperty( JoniRegexPattern(String perlPattern, RegexFlags flags, int trustedCalloutCount, boolean forceAsciiClasses, boolean byteMode, boolean byteBackedPattern) { - KeepSyntax keepSyntax = analyzeKeepSyntax(perlPattern, flags.isExtended()); - if (keepSyntax.inLookaround()) { + PerlSyntaxFeatures syntaxFeatures = analyzePerlSyntax(perlPattern, flags.isExtended()); + if (syntaxFeatures.keepInLookaround()) { throw new PerlCompilerException("\\K not permitted in lookahead/lookbehind in regex"); } this.flags = flags; @@ -342,7 +342,10 @@ static boolean requiresJoniBackend(String pattern, RegexFlags flags) { // implementation passes its combined imported-corpus gate. Explicit // Joni mode still exercises the native implementation directly. boolean branchResetCallUsesJava = pattern.contains("(?|") && hasSubroutineCall; - return analyzeKeepSyntax(pattern, flags != null && flags.isExtended()).present() + PerlSyntaxFeatures syntaxFeatures = analyzePerlSyntax( + pattern, flags != null && flags.isExtended()); + return syntaxFeatures.keepPresent() + || syntaxFeatures.conditionalPresent() || pattern.contains("(?{=CALL:") || pattern.contains("(?{=DYNAMIC:") || pattern.contains("(*ACCEPT)") @@ -352,24 +355,22 @@ static boolean requiresJoniBackend(String pattern, RegexFlags flags) { || pattern.contains("(*COMMIT") || pattern.contains("(*MARK") || pattern.contains("(*:") - || pattern.contains("(?(DEFINE)") - || pattern.contains("(?(?{=CALL:") - || pattern.contains("(?(R") - || pattern.contains("(?(<") - || pattern.contains("(?('") || (hasSubroutineCall && !branchResetCallUsesJava); } - private record KeepSyntax(boolean present, boolean inLookaround) {} + private record PerlSyntaxFeatures(boolean keepPresent, + boolean keepInLookaround, + boolean conditionalPresent) {} - private static KeepSyntax analyzeKeepSyntax(String pattern, boolean extended) { + private static PerlSyntaxFeatures analyzePerlSyntax(String pattern, boolean extended) { boolean quoted = false; boolean inClass = false; boolean classStart = false; int extendedClassDepth = 0; int lookaroundDepth = 0; java.util.ArrayDeque groups = new java.util.ArrayDeque<>(); - boolean present = false; + boolean keepPresent = false; + boolean conditionalPresent = false; for (int i = 0; i < pattern.length(); i++) { char ch = pattern.charAt(i); @@ -431,8 +432,10 @@ private static KeepSyntax analyzeKeepSyntax(String pattern, boolean extended) { if (escaped == 'Q') { quoted = true; } else if (escaped == 'K') { - present = true; - if (lookaroundDepth > 0) return new KeepSyntax(true, true); + keepPresent = true; + if (lookaroundDepth > 0) { + return new PerlSyntaxFeatures(true, true, conditionalPresent); + } } continue; } @@ -443,6 +446,7 @@ private static KeepSyntax analyzeKeepSyntax(String pattern, boolean extended) { i = close; continue; } + if (pattern.startsWith("(?(", i)) conditionalPresent = true; boolean lookaround = pattern.startsWith("(?=", i) || pattern.startsWith("(?!", i) || pattern.startsWith("(?<=", i) @@ -453,7 +457,7 @@ private static KeepSyntax analyzeKeepSyntax(String pattern, boolean extended) { if (groups.pop()) lookaroundDepth--; } } - return new KeepSyntax(present, false); + return new PerlSyntaxFeatures(keepPresent, false, conditionalPresent); } private static boolean hasControlVerbState(String pattern) { diff --git a/src/test/java/org/perlonjava/runtime/regex/RegexBackendPolicyTest.java b/src/test/java/org/perlonjava/runtime/regex/RegexBackendPolicyTest.java index 3d4721285d..d9dde15a86 100644 --- a/src/test/java/org/perlonjava/runtime/regex/RegexBackendPolicyTest.java +++ b/src/test/java/org/perlonjava/runtime/regex/RegexBackendPolicyTest.java @@ -78,6 +78,35 @@ void javaModeRetainsRequiredAdvancedJoniRouting() { assertTrue(RegexBackendPolicy.useJoni("(?&recursive)")); } + @Test + void javaModeRoutesRealPerlConditionalsToJoni() { + System.setProperty(RegexBackendPolicy.PROPERTY, "java"); + + assertTrue(RegexBackendPolicy.useJoni("(a)(?(1)b|c)")); + assertTrue(RegexBackendPolicy.useJoni("(a)(?(1)b)")); + assertTrue(RegexBackendPolicy.useJoni("(?x)(?()y|z)")); + assertTrue(RegexBackendPolicy.useJoni("(?(?=a)a|b)")); + assertTrue(RegexBackendPolicy.useJoni("(?(DEFINE)(?x))")); + assertTrue(RegexBackendPolicy.useJoni("(?(R)recursive|plain)")); + assertTrue(RegexBackendPolicy.useJoni("(?(bogus)x|y)")); + assertTrue(RegexBackendPolicy.useJoni("(?(1)x")); + } + + @Test + void conditionalLookalikesRemainOnTheOrdinaryRoute() { + System.setProperty(RegexBackendPolicy.PROPERTY, "java"); + + assertFalse(RegexBackendPolicy.useJoni("\\(\\?\\(")); + assertFalse(RegexBackendPolicy.useJoni("[(?()]")); + assertFalse(RegexBackendPolicy.useJoni("(?[ [(?()] ])")); + assertFalse(RegexBackendPolicy.useJoni("\\Q(?(DEFINE)(?x))\\E")); + assertFalse(RegexBackendPolicy.useJoni("(?# (?(1)x|y))ordinary")); + + String extendedComment = "# (?(1)x|y)\nordinary"; + RegexFlags extendedFlags = RegexFlags.fromModifiers("x", extendedComment); + assertFalse(RegexBackendPolicy.useJoni(extendedComment, extendedFlags)); + } + @Test void joniModeRoutesOrdinaryPatternsToJoni() { System.setProperty(RegexBackendPolicy.PROPERTY, "joni"); diff --git a/src/test/resources/unit/regex/conditional_native_routing.t b/src/test/resources/unit/regex/conditional_native_routing.t new file mode 100644 index 0000000000..cd47564dbe --- /dev/null +++ b/src/test/resources/unit/regex/conditional_native_routing.t @@ -0,0 +1,69 @@ +use strict; +use warnings; +use Test::More; + +ok('ab' =~ /^(a)(?(1)b)$/, + 'numeric yes-only conditional consumes its required branch'); +ok('a' !~ /^(a)(?(1)b)$/, + 'numeric yes-only conditional does not make the required branch optional'); + +ok('ab' =~ /^(a)?(?(1)b|c)$/, + 'numeric conditional takes the yes branch when its capture participated'); +is($1, 'a', 'true numeric condition publishes its capture'); +ok('c' =~ /^(a)?(?(1)b|c)$/, + 'numeric conditional takes the no branch when its capture did not participate'); +ok(!defined($1), 'false numeric condition leaves its capture undefined'); + +ok('ac' =~ /^(a)?(?(1)b|ac)$/, + 'conditional capture state is reevaluated after backtracking'); +ok(!defined($1), 'backtracking clears the abandoned conditional capture'); + +ok('xy' =~ /^(?x)(?()y)$/, + 'named yes-only conditional takes its required branch'); +ok('x' !~ /^(?x)(?()y)$/, + 'named yes-only conditional does not make its branch optional'); +ok('z' =~ /^(?x)?(?()y|z)$/, + 'named conditional takes its no branch'); + +ok('a' =~ /^(?(?=a)a|b)$/, + 'positive assertion conditional takes its yes branch'); +ok('b' =~ /^(?(?=a)a|b)$/, + 'positive assertion conditional takes its no branch'); +ok('a' =~ /^(?(?!a)b|a)$/, + 'negative assertion conditional takes its no branch'); +ok('b' =~ /^(?(?!a)b|a)$/, + 'negative assertion conditional takes its yes branch'); + +my $loader = qr/ + ^ (? \w+) + (? [(])? + (? [^)]*) + (?() [)]) + $ +/x; +ok('call(value)' =~ $loader, 'compiled named conditional matches'); +is($+{arg}, 'value', 'compiled named conditional preserves later captures'); + +my $text = '${foo} $bar'; +my $subst = qr/(^|\G|[^\\])\$(\{)?([A-Za-z][\w-]*)(?(2)\})/; +$text =~ s/$subst/$1 . uc($3)/eg; +is($text, 'FOO BAR', 'yes-only numeric conditional works in substitution'); + +ok('?((' =~ /^\?\(\($/, + 'escaped literal conditional introducer remains ordinary pattern text'); + +sub compile_error { + my ($source) = @_; + local $SIG{__WARN__} = sub {}; + eval "qr/$source/"; + return $@; +} + +ok(length(compile_error('(?(1)x|y|z)')), + 'conditional with too many branches remains rejected'); +ok(length(compile_error('(?(bogus)x|y)')), + 'unknown conditional remains rejected'); +ok(length(compile_error('(?(1)x')), + 'unterminated conditional remains rejected'); + +done_testing; diff --git a/third_party/joni/test/org/joni/test/TestPerlConditions.java b/third_party/joni/test/org/joni/test/TestPerlConditions.java index 321ebb9d98..82ced22466 100644 --- a/third_party/joni/test/org/joni/test/TestPerlConditions.java +++ b/third_party/joni/test/org/joni/test/TestPerlConditions.java @@ -20,6 +20,7 @@ package org.joni.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; import java.nio.charset.StandardCharsets; @@ -28,6 +29,7 @@ import org.joni.Option; import org.joni.Regex; import org.joni.Syntax; +import org.joni.exception.SyntaxException; import org.junit.Test; public class TestPerlConditions { @@ -73,4 +75,42 @@ public void numberedAndNamedRecursionConditionsSelectTheirGroup() { assertEquals(0, matcher("(x)(?foo(?(R&A)bar))?\\g", "xfoofoobar") .search(0, 10, Option.NONE)); } + + @Test + public void perlCaptureConditionsSelectRequiredAndAlternateBranches() { + assertEquals(0, matcher("^(a)(?(1)b)$", "ab") + .search(0, 2, Option.NONE)); + assertEquals(-1, matcher("^(a)(?(1)b)$", "a") + .search(0, 1, Option.NONE)); + assertEquals(0, matcher("^(a)?(?(1)b|c)$", "c") + .search(0, 1, Option.NONE)); + } + + @Test + public void perlNamedConditionsPreserveYesOnlySemantics() { + assertEquals(0, matcher("^(?x)(?()y)$", "xy") + .search(0, 2, Option.NONE)); + assertEquals(-1, matcher("^(?x)(?()y)$", "x") + .search(0, 1, Option.NONE)); + assertEquals(0, matcher("^(?x)?(?()y|z)$", "z") + .search(0, 1, Option.NONE)); + } + + @Test + public void perlCaptureConditionIsReevaluatedAfterBacktracking() { + Matcher matcher = matcher("^(a)?(?(1)b|ac)$", "ac"); + assertEquals(0, matcher.search(0, 2, Option.NONE)); + assertEquals(-1, matcher.getRegion().getBeg(1)); + assertEquals(-1, matcher.getRegion().getEnd(1)); + } + + @Test + public void malformedPerlConditionsRemainRejected() { + assertThrows(SyntaxException.class, + () -> matcher("(?(1)x|y|z)", "")); + assertThrows(SyntaxException.class, + () -> matcher("(?(bogus)x|y)", "")); + assertThrows(SyntaxException.class, + () -> matcher("(?(1)x", "")); + } } From a29cfb753acf51793d466d11ea909338b0f76e9b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 20:17:40 +0200 Subject: [PATCH 2/2] refactor(regex): delete unreachable conditional rewrites Ordinary and malformed Perl conditionals are now routed to native Joni before the Java regex preprocessor runs. Remove the lossy conditional pre-pass and Java Pattern emulation that can no longer be reached. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../runtime/regex/RegexPreprocessor.java | 393 ------------------ 1 file changed, 393 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java index d078cf6f36..4897b45d7c 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java @@ -140,7 +140,6 @@ private static String preProcessRegexInternal(String s, RegexFlags regexFlags) { duplicateNameCounter = 0; warningsOnUse.clear(); - s = transformSimpleConditionals(s); s = removeUnderscoresFromEscapes(s); s = normalizeQuantifiers(s); @@ -1144,11 +1143,6 @@ private static int handleParentheses(String s, int offset, int length, StringBui // offset now points at ')' closing the (??{...}) construct // Fall through to common ')' handling at end of handleParentheses - } else if (c3 == '(') { - // Handle (?(condition)yes|no) conditionals - // handleConditionalPattern processes the entire conditional including its closing ) - // so we need to return directly without further processing - return handleConditionalPattern(s, offset, length, sb, regexFlags); } else if (c3 == ';') { // (?;...) is not recognized - marker should be after ; regexError(s, offset + 3, "Sequence (?;...) not recognized"); @@ -1862,164 +1856,6 @@ private static int findMatchingParen(String s, int start, int length) { return -1; // Not found } - // Handle conditional patterns - private static int handleConditionalPattern(String s, int offset, int length, StringBuilder sb, RegexFlags regexFlags) { - // offset is at '(' of (?(condition)yes|no) - int condStart = offset + 3; // Skip (?( - - // Find the end of the condition - int condEnd = condStart; - int parenDepth = 0; - boolean foundEnd = false; - - while (condEnd < length && !foundEnd) { - char ch = s.charAt(condEnd); - if (ch == '(') { - parenDepth++; - } else if (ch == ')' && parenDepth == 0) { - foundEnd = true; - break; - } else if (ch == ')') { - parenDepth--; - } - condEnd++; - } - - if (!foundEnd) { - // Error should point to where we expected the closing paren - regexError(s, condEnd, "Switch (?(condition)... not terminated"); - } - - // Extract and validate the condition - String condition = s.substring(condStart, condEnd).trim(); - - // System.err.println("DEBUG: Conditional pattern condition: '" + condition + "'"); - - // Check for invalid conditions like "1x" or "1x(?#)" - if (condition.matches("\\d+[a-zA-Z].*")) { - // Find where the alphabetic part starts - int i = 0; - while (i < condition.length() && Character.isDigit(condition.charAt(i))) { - i++; - } - // For "1x(?#)", we want the marker after "x", not after the comment - // So we need to find where the alphabetic part ends - int alphaEnd = i; - while (alphaEnd < condition.length() && Character.isLetter(condition.charAt(alphaEnd))) { - alphaEnd++; - } - // Error should point after the alphanumeric part - regexError(s, condStart + alphaEnd, "Switch condition not recognized"); - } - - // Check for specific invalid patterns - if (condition.equals("??{}") || condition.equals("?[")) { - // Marker should be after the first ? - regexUnimplemented(s, condStart + 1, "Unknown switch condition (?(...))"); - } - - boolean assertionCondition = condition.startsWith("?=") || condition.startsWith("?!"); - - if (condition.startsWith("?") && !assertionCondition) { - // Marker should be after the first ? - regexUnimplemented(s, condStart + 1, "Unknown switch condition (?(...))"); - } - - // Check for non-numeric conditions that aren't valid - if (!assertionCondition && !condition.matches("\\d+") && !condition.matches("<[^>]+>") && !condition.matches("'[^']+'")) { - // For single character conditions like "x", marker should be after the character - if (condition.length() == 1) { - regexUnimplemented(s, condStart + 1, "Unknown switch condition (?(...))"); - } else { - regexUnimplemented(s, condStart, "Unknown switch condition (?(...))"); - } - } - - // Now parse the yes|no branches - int pos = condEnd + 1; // Skip past the closing ) - int pipeCount = 0; - int branchStart = pos; - int pipePos = -1; - parenDepth = 0; - - // First, check if we have any content after the condition - if (pos >= length) { - // No branches at all - for /(?(1)/ the marker should be right after the ) - // condEnd is at the position of ), so condEnd + 1 is after it - regexError(s, condEnd + 1, "Switch (?(condition)... not terminated"); - } - - while (pos < length) { - char ch = s.charAt(pos); - if (ch == '(') { - parenDepth++; - } else if (ch == ')' && parenDepth == 0) { - // End of conditional - break; - } else if (ch == ')') { - parenDepth--; - } else if (ch == '|' && parenDepth == 0) { - pipeCount++; - if (pipeCount == 1) { - pipePos = pos; - } - if (pipeCount > 1) { - // Mark the error right after this pipe character - regexError(s, pos + 1, "Switch (?(condition)... contains too many branches"); - } - } else if (ch == '\\' && pos + 1 < length) { - pos++; // Skip escaped character - } - pos++; - } - - if (pos >= length || s.charAt(pos) != ')') { - // The pattern ends without closing the conditional - regexError(s, pos, "Switch (?(condition)... not terminated"); - } - - String yesBranch = pipePos >= 0 ? s.substring(branchStart, pipePos) : s.substring(branchStart, pos); - String noBranch = pipePos >= 0 ? s.substring(pipePos + 1, pos) : ""; - - // Java has lookaround but no Perl conditional syntax. An assertion - // conditional can be expressed as two mutually exclusive alternations: - // (?(?=A)Y|N) -> (?:(?=A)Y|(?!A)N), with the inverse for (?!A). - // Run each branch recursively so nested assertion conditionals work too. - if (assertionCondition) { - String assertionBody = condition.substring(2); - boolean positive = condition.startsWith("?="); - sb.append("(?:").append(positive ? "(?=" : "(?!").append(assertionBody).append(")"); - handleRegex(yesBranch, 0, sb, regexFlags, false); - if (pipePos >= 0) { - sb.append("|").append(positive ? "(?!" : "(?=").append(assertionBody).append(")"); - handleRegex(noBranch, 0, sb, regexFlags, false); - } - sb.append(")"); - return pos; - } - - if (condition.matches("\\d+") && pipeCount == 0) { - sb.append("(?:"); - handleRegex(yesBranch, 0, sb, regexFlags, false); - sb.append(")?"); - return pos; - } - - // Conditional patterns are not supported by Java regex - // (?(1)yes|no) means: if group 1 matched, use 'yes' branch, else use 'no' branch - // This is fundamentally different from alternation and cannot be converted - - // Simple cases are transformed in transformSimpleConditionals() - // If we reach here, it's a complex case that couldn't be transformed - - // Use regexUnimplemented so it can be caught with JPERL_UNIMPLEMENTED=warn - // Append a placeholder that won't match anything - sb.append("(?!)"); // Negative lookahead that always fails - - regexUnimplemented(s, condStart - 1, "Conditional patterns (?(...)...) not implemented"); - - return pos; - } /** * Handle a potential quantifier starting with '{'. @@ -2135,235 +1971,6 @@ private static boolean isValidQuantifierAt(String s, int offset) { } - /** - * Transform simple conditional patterns (?(N)yes|no) that can be converted to alternations. - *

- * Phase 1 implementation handles the common pattern: (group)?(?(N)yes|no) - * Transforms to: (?:(group)yes|no) - *

- * This works because: - * - If group matches: first alternative (group)yes is tried - * - If group doesn't match: second alternative no is tried - * - * @param pattern The regex pattern - * @return Transformed pattern with simple conditionals converted to alternations - */ - private static String transformSimpleConditionals(String pattern) { - // For now, we'll handle the simplest case: (?)?(?(1)yes|no) or (?)?(?(1)yes) - // More complex transformations can be added later - - // Pattern to match: (capture)? followed by (?(N)yes|no) or (?(N)yes) - // We need to be careful about nested parentheses and escapes - - StringBuilder result = new StringBuilder(); - int pos = 0; - int len = pattern.length(); - - while (pos < len) { - // Look for (?(digit) - int condStart = pattern.indexOf("(?(", pos); - if (condStart == -1) { - // No more conditionals - result.append(pattern.substring(pos)); - break; - } - - // Check if next char after (?( is a digit - if (condStart + 3 >= len || !Character.isDigit(pattern.charAt(condStart + 3))) { - // Not a simple numeric conditional, skip it - result.append(pattern, pos, condStart + 3); - pos = condStart + 3; - continue; - } - - // Extract the group number - int digitEnd = condStart + 3; - while (digitEnd < len && Character.isDigit(pattern.charAt(digitEnd))) { - digitEnd++; - } - - if (digitEnd >= len || pattern.charAt(digitEnd) != ')') { - // Invalid format, skip - result.append(pattern, pos, digitEnd); - pos = digitEnd; - continue; - } - - int groupNum = Integer.parseInt(pattern.substring(condStart + 3, digitEnd)); - - // Now find the yes|no branches - // We need to find the matching ) for the conditional - int branchStart = digitEnd + 1; // After the ) of (?(N) - int parenDepth = 0; - int pipePos = -1; - int condEnd = branchStart; - boolean inCharClass = false; - boolean escaped = false; - - while (condEnd < len) { - char ch = pattern.charAt(condEnd); - - if (escaped) { - escaped = false; - condEnd++; - continue; - } - - if (ch == '\\') { - escaped = true; - condEnd++; - continue; - } - - if (inCharClass) { - if (ch == ']') { - inCharClass = false; - } - condEnd++; - continue; - } - - if (ch == '[') { - inCharClass = true; - condEnd++; - continue; - } - - if (ch == '(') { - parenDepth++; - } else if (ch == ')') { - if (parenDepth == 0) { - // Found the end of conditional - break; - } - parenDepth--; - } else if (ch == '|' && parenDepth == 0 && pipePos == -1) { - pipePos = condEnd; - } - - condEnd++; - } - - if (condEnd >= len) { - // Unterminated conditional, let normal error handling catch it - result.append(pattern.substring(pos)); - break; - } - - // Extract yes and no branches - String yesBranch = pipePos > 0 ? pattern.substring(branchStart, pipePos) : pattern.substring(branchStart, condEnd); - String noBranch = pipePos > 0 ? pattern.substring(pipePos + 1, condEnd) : ""; - - // Now try to find the referenced group BEFORE this conditional - // For simplicity in Phase 1, we only handle if the group appears immediately before - // or with simple pattern between (like literals) - - // Look backwards for group N - // Count groups from the start to condStart - int groupCount = 0; - int groupNStart = -1; - int groupNEnd = -1; - int searchPos = 0; - int depth = 0; - boolean isOptional = false; - - while (searchPos < condStart) { - char ch = pattern.charAt(searchPos); - - if (ch == '\\') { - searchPos += 2; // Skip escaped char - continue; - } - - if (ch == '(') { - // Check if it's a capturing group - if (searchPos + 1 < condStart && pattern.charAt(searchPos + 1) != '?') { - // It's a capturing group - groupCount++; - if (groupCount == groupNum) { - groupNStart = searchPos; - // Find the end of this group - depth = 1; - int endPos = searchPos + 1; - while (endPos < condStart && depth > 0) { - char c = pattern.charAt(endPos); - if (c == '\\') { - endPos += 2; - continue; - } - if (c == '(') depth++; - if (c == ')') depth--; - endPos++; - } - groupNEnd = endPos; - - // Check if followed by ? or * or {0, - if (groupNEnd < condStart) { - char next = pattern.charAt(groupNEnd); - if (next == '?' || next == '*') { - isOptional = true; - } else if (next == '{') { - // Check for {0,n} - int closePos = pattern.indexOf('}', groupNEnd); - if (closePos > 0 && closePos < condStart) { - String quant = pattern.substring(groupNEnd + 1, closePos); - if (quant.startsWith("0,")) { - isOptional = true; - } - } - } - } - } - } - } - searchPos++; - } - - // Only transform if we found the group and it's optional and appears directly before the conditional - if (groupNStart >= 0 && isOptional) { - // Check if there's only simple content between group and conditional - String between = pattern.substring(groupNEnd + 1, condStart); // +1 to skip the ? or * after group - - // For Phase 1, only transform if: - // 1. The group is immediately before conditional OR - // 2. There's only simple literal text between - boolean canTransform = between.isEmpty() || between.matches("[a-zA-Z0-9\\s\\^\\$]+"); - - if (canTransform) { - // Perform transformation! - // Append everything before the group - result.append(pattern, pos, groupNStart); - - // Build the alternation: (?:(group)between+yes|between+no) - result.append("(?:"); - - // First alternative: group+between+yes - result.append(pattern, groupNStart, groupNEnd); // The group itself (without the ? or *) - result.append(between); - result.append(yesBranch); - - // Second alternative: between+no - // Always add second alternative (even if noBranch is empty) - // Empty noBranch means "match nothing" when group doesn't match - result.append("|"); - result.append(between); - result.append(noBranch); - - result.append(")"); - - // Continue after the conditional - pos = condEnd + 1; - continue; - } - } - - // Could not transform, keep original - result.append(pattern, pos, condEnd + 1); - pos = condEnd + 1; - } - - return result.toString(); - } /** * Handles (?{...}) code blocks in regex patterns.