diff --git a/java/com/google/re2j/Regexp.java b/java/com/google/re2j/Regexp.java index 612b8900..f17da5b9 100644 --- a/java/com/google/re2j/Regexp.java +++ b/java/com/google/re2j/Regexp.java @@ -294,7 +294,8 @@ public int hashCode() { break; case LITERAL: case CHAR_CLASS: - hashcode += 31 * Arrays.hashCode(runes); + // FoldCase in the hash too — equals() now compares it (see there). + hashcode += 31 * Arrays.hashCode(runes) + (flags & RE2.FOLD_CASE); break; case ALTERNATE: case CONCAT: @@ -335,7 +336,15 @@ public boolean equals(Object that) { break; case LITERAL: case CHAR_CLASS: - if (!Arrays.equals(x.runes, y.runes)) { + // FoldCase must participate in equality: alternation factoring + // (Parser.factor round 1) merges arms by equals(), and a folded + // literal must never factor with its case-sensitive twin — the + // factored prefix would carry the fold into arms that never asked + // for it. (?i:Z)x|Z matched lowercase "z" via the case-sensitive + // arm. Go's regexp/syntax compares Flags&FoldCase here; the port + // dropped it. + if ((x.flags & RE2.FOLD_CASE) != (y.flags & RE2.FOLD_CASE) + || !Arrays.equals(x.runes, y.runes)) { return false; } break; diff --git a/javatests/com/google/re2j/FoldCaseFactoringTest.java b/javatests/com/google/re2j/FoldCaseFactoringTest.java new file mode 100644 index 00000000..118bebd2 --- /dev/null +++ b/javatests/com/google/re2j/FoldCaseFactoringTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 The Go Authors. All rights reserved. + * + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ +package com.google.re2j; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Tests that alternation factoring keeps case-insensitive and case-sensitive literals apart. + * + *

+ * Regexp.equals ignored FoldCase for LITERAL/CHAR_CLASS, so Parser.factor merged a folded literal + * with its case-sensitive twin and the fold leaked into the merged arm: {@code "(?i:Z)x|Z"} matched + * lowercase {@code "z"}. + */ +@RunWith(JUnit4.class) +public class FoldCaseFactoringTest { + + @Test + public void foldDoesNotLeakIntoCaseSensitiveArm() { + assertFalse(Pattern.compile("(?i:Z)x|Z").matcher("z").find()); + assertFalse(Pattern.compile("(?i:[Z])x|[Z]").matcher("z").find()); + } + + @Test + public void bothArmsStillMatch() { + assertTrue(Pattern.compile("(?i:Z)x|Z").matcher("Zx").find()); // folded arm + assertTrue(Pattern.compile("(?i:Z)x|Z").matcher("Z").find()); // case-sensitive arm + } + + @Test + public void foldedArmStillFolds() { + assertTrue(Pattern.compile("(?i:Z)x|Z").matcher("zx").find()); + } +} diff --git a/javatests/com/google/re2j/RegexpHashcodeEqualsTest.java b/javatests/com/google/re2j/RegexpHashcodeEqualsTest.java index bb0a665e..993e19fb 100644 --- a/javatests/com/google/re2j/RegexpHashcodeEqualsTest.java +++ b/javatests/com/google/re2j/RegexpHashcodeEqualsTest.java @@ -30,6 +30,9 @@ public static Iterable testCases() { {"a{2,3}", "a{1,3}", false, RE2.POSIX}, {"^((?Pwhat)a)$", "^((?Pwhat)a)$", true, RE2.PERL}, {"^((?Pwhat)a)$", "^((?Pwhat)a)$", false, RE2.PERL}, + {"(?i:Z)", "Z", false, RE2.PERL}, + {"(?i:Z)", "(?i:Z)", true, RE2.PERL}, + {"(?i:[Z])", "[Z]", false, RE2.PERL}, }); }