Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions java/com/google/re2j/Regexp.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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;
Expand Down
43 changes: 43 additions & 0 deletions javatests/com/google/re2j/FoldCaseFactoringTest.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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());
}
}
3 changes: 3 additions & 0 deletions javatests/com/google/re2j/RegexpHashcodeEqualsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ public static Iterable<Object[]> testCases() {
{"a{2,3}", "a{1,3}", false, RE2.POSIX},
{"^((?P<foo>what)a)$", "^((?P<foo>what)a)$", true, RE2.PERL},
{"^((?P<foo>what)a)$", "^((?P<bar>what)a)$", false, RE2.PERL},
{"(?i:Z)", "Z", false, RE2.PERL},
{"(?i:Z)", "(?i:Z)", true, RE2.PERL},
{"(?i:[Z])", "[Z]", false, RE2.PERL},
});
}

Expand Down