Skip to content
Merged
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
20 changes: 20 additions & 0 deletions mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ class_set_bit(re_charclass *cc, uint8_t ch)
}
}

static mrb_bool
class_get_bit(const re_charclass *cc, uint8_t ch)
{
if (ch >= 128) return FALSE;
return (cc->bitmap[ch >> 3] >> (ch & 7)) & 1;
}

/* Append a non-ASCII codepoint range [lo, hi]. Both bounds must be >= 128. */
static void
class_add_range(re_compiler *c, re_charclass *cc, uint32_t lo, uint32_t hi)
Expand Down Expand Up @@ -466,6 +473,19 @@ compile_charclass(re_compiler *c)
}
next_char(c); /* skip ']' */

/* Fold ASCII letters under /i. This runs once the class is complete, so a
single pass covers every form the loop above merges into the bitmap:
POSIX brackets, shorthands, ranges and single literals. Negation is
applied at match time against this same bitmap (RE_NCLASS), so folding
the positive set also fixes [^a-c] under /i. Non-ASCII case folding is
out of scope, so the codepoint range list is left alone. */
if (c->flags & RE_FLAG_IGNORECASE) {
for (int ch = 'a'; ch <= 'z'; ch++) {
if (class_get_bit(cc, (uint8_t)ch)) class_set_bit(cc, (uint8_t)(ch - 32));
else if (class_get_bit(cc, (uint8_t)(ch - 32))) class_set_bit(cc, (uint8_t)ch);
}
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
cc->negated = negated;
emit(c, negated ? RE_NCLASS : RE_CLASS, (uint8_t)id, 0);
}
Expand Down
26 changes: 26 additions & 0 deletions mrbgems/mruby-regexp/test/regexp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@
assert_equal "snake_case", "snake_case".match(/[[:word:]]+/)[0]
assert_equal "!", "ab!cd".match(/[[:punct:]]/)[0]
assert_equal "AB", "abAB".match(/[[:upper:]]+/)[0]
# /i makes the two letter-case classes equivalent.
assert_equal "abAB", "abAB".match(/[[:upper:]]+/i)[0]
assert_equal "abAB", "abAB".match(/[[:lower:]]+/i)[0]
# combine with literals and other classes
assert_equal "a1", "a1-".match(/[a[:digit:]]+/)[0]
assert_equal "ab12", "ab12 ".match(/[[:alpha:][:digit:]]+/)[0]
Expand Down Expand Up @@ -333,6 +336,24 @@
assert_true re.match?("Abc")
end

assert("Regexp - case insensitive character class") do
# /i used to be folded in only where a single literal was emitted, so a
# character class ignored it entirely.
assert_true(/[abc]/i.match?("A"))
assert_true(/[a-c]/i.match?("A"))
assert_true(/[A-C]/i.match?("a"))
assert_true(/[a-c]+/i.match?("AB"))
assert_true Regexp.new("[a-c]", Regexp::IGNORECASE).match?("A")
# A negated class matched what it had to reject, which is a false positive.
assert_false(/[^a-c]/i.match?("A"))
assert_false(/[^A-C]/i.match?("a"))
assert_true(/[^a-c]/i.match?("d"))
# Folding must not widen the class beyond the ASCII letters.
assert_false(/[a-c]/i.match?("D"))
assert_false(/[\[]/i.match?("{")) # `[` and `{` are 32 apart but are not a case pair
assert_false(/[@]/i.match?("`"))
end

assert("Regexp - repetition {n,m}") do
assert_equal "aaa", Regexp.new("a{3}").match("aaaa")[0]
assert_equal "aa", Regexp.new("a{2,3}").match("aa")[0]
Expand Down Expand Up @@ -409,6 +430,11 @@
assert_nil (/(?i:a)b/ =~ "aB") # option must not leak past the `)`
assert_equal 0, (/(?i:ab)+/ =~ "AbaB") # scoped group is still quantifiable

# A character class reads the inline-scoped flag, not the pattern-wide one.
assert_equal 0, (/(?i)[a-c]/ =~ "A")
assert_equal 0, (/(?i:[a-c])/ =~ "A")
assert_nil (/(?i:[a-c])[a-c]/ =~ "AB") # option must not leak past the `)`

# The toggle inside a group is confined to that group.
assert_equal 0, (/(a(?i)b)c/ =~ "aBc")
assert_nil (/(a(?i)b)c/ =~ "aBC") # trailing `c` is case-sensitive again
Expand Down
Loading