mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
19c857a773
The Pike VM and pattern compiler were exporting bare names like `re_compile`, `re_exec`, `re_free`, `re_is_word_char`, `re_utf8_charlen`, `re_utf8_decode`. `re_exec` in particular collides with the obsolete BSD libc function of the same name (still present on FreeBSD/NetBSD base), so embedding mruby alongside platform regex could surface a link-time symbol clash. Rename all six entry points to `mrb_re_*` to keep the gem's external symbols inside mruby's namespace. Source file names and the public header path are unchanged. Refs #6858. Co-authored-by: Claude <noreply@anthropic.com>
495 lines
14 KiB
Ruby
495 lines
14 KiB
Ruby
assert("Regexp.new with string") do
|
|
re = Regexp.new("abc")
|
|
assert_kind_of Regexp, re
|
|
end
|
|
|
|
assert("Regexp.new with regexp") do
|
|
r1 = Regexp.new("abc", Regexp::IGNORECASE)
|
|
r2 = Regexp.new(r1)
|
|
assert_equal r1.source, r2.source
|
|
assert_equal r1.options, r2.options
|
|
assert_true r2.match?("ABC")
|
|
end
|
|
|
|
assert("Regexp#match - simple") do
|
|
re = Regexp.new("abc")
|
|
md = re.match("xabcy")
|
|
assert_kind_of MatchData, md
|
|
assert_equal "abc", md[0]
|
|
end
|
|
|
|
assert("Regexp#match - no match") do
|
|
re = Regexp.new("xyz")
|
|
assert_nil re.match("abc")
|
|
end
|
|
|
|
assert("Regexp#match?") do
|
|
re = Regexp.new("abc")
|
|
assert_true re.match?("xabcy")
|
|
assert_false re.match?("xyz")
|
|
end
|
|
|
|
assert("Regexp#=~") do
|
|
re = Regexp.new("bc")
|
|
assert_equal 1, re =~ "abcd"
|
|
assert_nil re =~ "xyz"
|
|
end
|
|
|
|
assert("Regexp#===") do
|
|
re = Regexp.new("abc")
|
|
assert_true re === "abc"
|
|
assert_false re === "xyz"
|
|
end
|
|
|
|
assert("Regexp - character class") do
|
|
re = Regexp.new("[a-z]+")
|
|
md = re.match("123abc456")
|
|
assert_equal "abc", md[0]
|
|
end
|
|
|
|
assert("Regexp - \\b inside character class is backspace") do
|
|
# Outside [...], \b is the word boundary assertion; inside [...]
|
|
# it must mean U+0008 (backspace), matching MRI/Onigmo.
|
|
assert_equal "Ruby", "Ruby".gsub(/[\b]/, "X")
|
|
assert_equal "aXc", "a\bc".gsub(/[\b]/, "X")
|
|
assert_equal ["\b", "\t", "\n"], "ABC\b\t\n".scan(/[\b-\n]/)
|
|
end
|
|
|
|
assert("Regexp - dot") do
|
|
re = Regexp.new("a.c")
|
|
assert_true re.match?("abc")
|
|
assert_true re.match?("axc")
|
|
assert_false re.match?("ac")
|
|
end
|
|
|
|
assert("Regexp - alternation") do
|
|
re = Regexp.new("cat|dog")
|
|
assert_equal "cat", re.match("I have a cat")[0]
|
|
assert_equal "dog", re.match("I have a dog")[0]
|
|
end
|
|
|
|
assert("Regexp - quantifiers") do
|
|
assert_equal "aaa", Regexp.new("a+").match("aaa")[0]
|
|
assert_equal "", Regexp.new("a*").match("bbb")[0]
|
|
assert_equal "ab", Regexp.new("ab?").match("ab")[0]
|
|
assert_equal "a", Regexp.new("ab?").match("ac")[0]
|
|
end
|
|
|
|
assert("Regexp - captures") do
|
|
re = Regexp.new("(\\w+)@(\\w+)")
|
|
md = re.match("user@host")
|
|
assert_equal "user@host", md[0]
|
|
assert_equal "user", md[1]
|
|
assert_equal "host", md[2]
|
|
end
|
|
|
|
assert("Regexp - \\d \\w \\s") do
|
|
assert_true Regexp.new("\\d+").match?("123")
|
|
assert_true Regexp.new("\\w+").match?("abc_123")
|
|
assert_true Regexp.new("\\s+").match?(" ")
|
|
assert_false Regexp.new("\\d+").match?("abc")
|
|
end
|
|
|
|
assert("Regexp - anchors") do
|
|
assert_true Regexp.new("^abc").match?("abc")
|
|
assert_false Regexp.new("^abc").match?("xabc")
|
|
assert_true Regexp.new("abc$").match?("abc")
|
|
assert_false Regexp.new("abc$").match?("abcx")
|
|
end
|
|
|
|
assert("Regexp - case insensitive") do
|
|
re = Regexp.new("abc", Regexp::IGNORECASE)
|
|
assert_true re.match?("ABC")
|
|
assert_true re.match?("Abc")
|
|
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]
|
|
assert_equal "aaa", Regexp.new("a{2,3}").match("aaaa")[0]
|
|
end
|
|
|
|
assert("MatchData#captures") do
|
|
re = Regexp.new("(a)(b)(c)")
|
|
md = re.match("abc")
|
|
assert_equal ["a", "b", "c"], md.captures
|
|
end
|
|
|
|
assert("MatchData#pre_match / #post_match") do
|
|
re = Regexp.new("bc")
|
|
md = re.match("abcde")
|
|
assert_equal "a", md.pre_match
|
|
assert_equal "de", md.post_match
|
|
end
|
|
|
|
assert("MatchData#string") do
|
|
md = Regexp.new("bc").match("abcde")
|
|
assert_equal "abcde", md.string
|
|
end
|
|
|
|
assert("MatchData#regexp") do
|
|
re = Regexp.new("bc")
|
|
md = re.match("abcde")
|
|
assert_equal re, md.regexp
|
|
end
|
|
|
|
assert("MatchData#to_s") do
|
|
md = Regexp.new("bc").match("abcde")
|
|
assert_equal "bc", md.to_s
|
|
end
|
|
|
|
assert("MatchData#begin / #end") do
|
|
re = Regexp.new("bc")
|
|
md = re.match("abcde")
|
|
assert_equal 1, md.begin(0)
|
|
assert_equal 3, md.end(0)
|
|
end
|
|
|
|
assert("Regexp.escape") do
|
|
assert_equal "a\\.b\\*c", Regexp.escape("a.b*c")
|
|
end
|
|
|
|
assert("Regexp#inspect") do
|
|
re = Regexp.new("abc", Regexp::IGNORECASE)
|
|
assert_equal "/abc/i", re.inspect
|
|
end
|
|
|
|
assert("Regexp#to_s") do
|
|
assert_equal "(?:abc)", Regexp.new("abc").to_s
|
|
assert_equal "(?i:abc)", Regexp.new("abc", Regexp::IGNORECASE).to_s
|
|
assert_equal "(?m:abc)", Regexp.new("abc", Regexp::MULTILINE).to_s
|
|
assert_equal "(?im:abc)", Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).to_s
|
|
end
|
|
|
|
assert("Regexp#== and Regexp#eql?") do
|
|
r1 = Regexp.new("abc", Regexp::IGNORECASE)
|
|
r2 = Regexp.new("abc", Regexp::IGNORECASE)
|
|
r3 = Regexp.new("abc")
|
|
r4 = Regexp.new("def", Regexp::IGNORECASE)
|
|
assert_true r1 == r2
|
|
assert_true r1.eql?(r2)
|
|
assert_false r1 == r3 # different flags
|
|
assert_false r1 == r4 # different source
|
|
assert_false r1 == "abc" # not a Regexp
|
|
end
|
|
|
|
assert("Regexp#hash") do
|
|
r1 = Regexp.new("abc", Regexp::IGNORECASE)
|
|
r2 = Regexp.new("abc", Regexp::IGNORECASE)
|
|
r3 = Regexp.new("abc")
|
|
assert_equal r1.hash, r2.hash
|
|
assert_not_equal r1.hash, r3.hash
|
|
end
|
|
|
|
assert("Regexp#hash/== on uninitialized regexp") do
|
|
# Regexp.allocate yields an object with no @source IV; hash/== must
|
|
# not crash (regression: ObjectSpace.each_object could expose a
|
|
# half-initialized Regexp after Regexp.new raised a compile error).
|
|
r = Regexp.allocate
|
|
assert_kind_of Integer, r.hash
|
|
assert_true r == r
|
|
assert_false r == Regexp.allocate
|
|
assert_false r == Regexp.new("abc")
|
|
end
|
|
|
|
assert("Regexp#options") do
|
|
assert_equal 0, Regexp.new("abc").options
|
|
assert_equal Regexp::IGNORECASE, Regexp.new("abc", Regexp::IGNORECASE).options
|
|
assert_equal Regexp::MULTILINE, Regexp.new("abc", Regexp::MULTILINE).options
|
|
assert_equal Regexp::EXTENDED, Regexp.new("abc", Regexp::EXTENDED).options
|
|
assert_equal Regexp::IGNORECASE | Regexp::MULTILINE,
|
|
Regexp.new("abc", Regexp::IGNORECASE | Regexp::MULTILINE).options
|
|
assert_equal Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE,
|
|
Regexp.new("abc", Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE).options
|
|
end
|
|
|
|
assert("Regexp#casefold?") do
|
|
assert_true Regexp.new("abc", Regexp::IGNORECASE).casefold?
|
|
assert_false Regexp.new("abc").casefold?
|
|
end
|
|
|
|
assert("Regexp extended mode (x flag)") do
|
|
# whitespace is ignored
|
|
re = Regexp.new('a b c', Regexp::EXTENDED)
|
|
assert_true re.match?("abc")
|
|
assert_false re.match?("a b c")
|
|
|
|
# comments are ignored
|
|
re = Regexp.new("a # match a\nb # match b\nc", Regexp::EXTENDED)
|
|
assert_true re.match?("abc")
|
|
|
|
# whitespace inside character class is literal
|
|
re = Regexp.new('[ ]', Regexp::EXTENDED)
|
|
assert_true re.match?(" ")
|
|
|
|
# escaped whitespace is preserved
|
|
re = Regexp.new('a\\ b', Regexp::EXTENDED)
|
|
assert_true re.match?("a b")
|
|
|
|
# inspect shows x flag
|
|
assert_equal "/abc/x", Regexp.new("abc", Regexp::EXTENDED).inspect
|
|
|
|
# to_s shows x flag
|
|
assert_equal "(?x:abc)", Regexp.new("abc", Regexp::EXTENDED).to_s
|
|
end
|
|
|
|
assert("String#match") do
|
|
md = "hello world".match(Regexp.new("(\\w+)\\s(\\w+)"))
|
|
assert_equal "hello", md[1]
|
|
assert_equal "world", md[2]
|
|
end
|
|
|
|
assert("String#sub") do
|
|
assert_equal "hXllo", "hello".sub(Regexp.new("e"), "X")
|
|
end
|
|
|
|
assert("String#gsub") do
|
|
assert_equal "h-ll-", "hello".gsub(Regexp.new("[eo]"), "-")
|
|
end
|
|
|
|
assert("String#sub with \\& \\` \\' specials") do
|
|
# \& = full match
|
|
assert_equal "a[bc]d", "abcd".sub(/bc/, '[\\&]')
|
|
# \` = pre_match
|
|
assert_equal "a[a]d", "abcd".sub(/bc/, '[\\`]')
|
|
# \' = post_match
|
|
assert_equal "a[d]d", "abcd".sub(/bc/, "[\\']")
|
|
# \+ = last capture
|
|
assert_equal "a[c]d", "abcd".sub(/(b)(c)/, '[\\+]')
|
|
# \\ = literal backslash
|
|
assert_equal "a\\d", "abcd".sub(/bc/, "\\\\")
|
|
# \1 still works
|
|
assert_equal "abbd", "abcd".sub(/(b)c/, '\\1\\1')
|
|
end
|
|
|
|
assert("String#gsub with \\& special") do
|
|
assert_equal "[a][b][c]", "abc".gsub(/./, '[\\&]')
|
|
end
|
|
|
|
assert("String#scan") do
|
|
assert_equal ["1", "2", "3"], "a1b2c3".scan(Regexp.new("\\d"))
|
|
end
|
|
|
|
assert("Regexp literal /regex/") do
|
|
assert_true /abc/.match?("abc")
|
|
assert_equal "123", /\d+/.match("abc123")[0]
|
|
assert_true /hello/i.match?("HELLO")
|
|
end
|
|
|
|
assert("$~ global variable") do
|
|
/(\w+)@(\w+)/ =~ "user@host"
|
|
assert_kind_of MatchData, $~
|
|
assert_equal "user", $~[1]
|
|
assert_equal "host", $~[2]
|
|
end
|
|
|
|
assert("$~ is nil on no match") do
|
|
/xyz/ =~ "abc"
|
|
assert_nil $~
|
|
end
|
|
|
|
assert("Regexp.last_match") do
|
|
/(\d+)/ =~ "abc123"
|
|
assert_equal "123", Regexp.last_match(1)
|
|
assert_equal "123", Regexp.last_match(0)
|
|
end
|
|
|
|
assert("Regexp - empty pattern") do
|
|
assert_true //.match?("")
|
|
assert_true //.match?("abc")
|
|
end
|
|
|
|
assert("Regexp - nested captures") do
|
|
md = /((a)(b))c/.match("abc")
|
|
assert_equal "abc", md[0]
|
|
assert_equal "ab", md[1]
|
|
assert_equal "a", md[2]
|
|
assert_equal "b", md[3]
|
|
end
|
|
|
|
assert("Regexp - non-greedy quantifiers") do
|
|
|
|
assert_equal "a", /a+?/.match("aaa")[0]
|
|
assert_equal "", /a*?/.match("aaa")[0]
|
|
end
|
|
|
|
assert("Regexp - word boundary") do
|
|
assert_equal "cat", /\bcat\b/.match("the cat sat")[0]
|
|
assert_nil /\bcat\b/.match("concatenate")
|
|
end
|
|
|
|
assert("Regexp - non-capturing group") do
|
|
md = /(?:a)(b)/.match("ab")
|
|
assert_equal "ab", md[0]
|
|
assert_equal "b", md[1]
|
|
assert_nil md[2]
|
|
end
|
|
|
|
assert("String#sub with block") do
|
|
assert_equal "HELLO world", "hello world".sub(/\w+/) { |m| m.upcase }
|
|
end
|
|
|
|
assert("String#gsub with block") do
|
|
assert_equal "HELLO WORLD", "hello world".gsub(/\w+/) { |m| m.upcase }
|
|
end
|
|
|
|
assert("String#gsub date reformat") do
|
|
result = "2026-03-21".gsub(/(\d+)-(\d+)-(\d+)/) { "#{$~[3]}/#{$~[2]}/#{$~[1]}" }
|
|
assert_equal "21/03/2026", result
|
|
end
|
|
|
|
assert("String#scan with captures") do
|
|
assert_equal [["1","a"],["2","b"]], "1a2b".scan(/(\d)(\w)/)
|
|
end
|
|
|
|
assert("String#split with regexp") do
|
|
assert_equal ["a", "b", "c"], "a, b, c".split(/,\s*/)
|
|
end
|
|
|
|
assert("Regexp - case in when") do
|
|
result = case "hello123"
|
|
when /\d+/ then "has digits"
|
|
else "no digits"
|
|
end
|
|
assert_equal "has digits", result
|
|
end
|
|
|
|
assert("Regexp - backreference \\1") do
|
|
# match repeated word
|
|
md = /(\w+) \1/.match("hello hello world")
|
|
assert_equal "hello hello", md[0]
|
|
assert_equal "hello", md[1]
|
|
end
|
|
|
|
assert("Regexp - backreference no match") do
|
|
assert_nil /(\w+) \1/.match("hello world")
|
|
end
|
|
|
|
assert("Regexp - named captures") do
|
|
md = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/.match("2026-03-21")
|
|
assert_equal "2026", md[:year]
|
|
assert_equal "03", md[:month]
|
|
assert_equal "21", md[:day]
|
|
assert_equal "2026", md["year"]
|
|
end
|
|
|
|
assert("MatchData#named_captures") do
|
|
md = /(?<a>\w+)@(?<b>\w+)/.match("user@host")
|
|
nc = md.named_captures
|
|
assert_equal "user", nc["a"]
|
|
assert_equal "host", nc["b"]
|
|
end
|
|
|
|
assert("Regexp - named captures survive /x preprocessing") do
|
|
# Regression: with /x, mrb_re_compile freed the stripped buffer that
|
|
# named_captures[i].name pointed into.
|
|
re = /(?<n>\d+) # comment
|
|
\s* (?<u>\w+) /x
|
|
m = re.match("42 px")
|
|
assert_equal "42", m[:n]
|
|
assert_equal "px", m[:u]
|
|
end
|
|
|
|
assert("Regexp - named captures survive source string mutation") do
|
|
# Regression: name pointer used to alias RSTRING_PTR of the source.
|
|
s = String.new("(?<key>\\d+)")
|
|
re = Regexp.new(s)
|
|
s.replace("X" * 10000) # force buffer reallocation
|
|
m = re.match("abc 123 def")
|
|
assert_equal "123", m[:key]
|
|
end
|
|
|
|
assert("Regexp - positive lookahead (?=...)") do
|
|
md = /\w+(?=@)/.match("user@host")
|
|
assert_equal "user", md[0]
|
|
end
|
|
|
|
assert("Regexp - negative lookahead (?!...)") do
|
|
md = /\d+(?!%)/.match("100%")
|
|
assert_equal "10", md[0]
|
|
end
|
|
|
|
assert("Regexp - lookahead does not consume") do
|
|
md = /foo(?=bar)/.match("foobar")
|
|
assert_equal "foo", md[0]
|
|
assert_nil /foo(?=baz)/.match("foobar")
|
|
end
|
|
|
|
assert("Regexp - positive lookbehind (?<=...)") do
|
|
md = Regexp.new("(?<=@)\\w+").match("user@host")
|
|
assert_equal "host", md[0]
|
|
assert_nil Regexp.new("(?<=@)\\w+").match("user_host")
|
|
end
|
|
|
|
assert("Regexp - negative lookbehind (?<!...)") do
|
|
md = Regexp.new("(?<!\\d)px").match("12px auto")
|
|
assert_nil md # preceded by digit
|
|
md = Regexp.new("(?<!\\d)em").match("12px 1.5em auto")
|
|
assert_nil md # preceded by digit
|
|
md = Regexp.new("(?<!\\d)px").match("top px")
|
|
assert_equal "px", md[0]
|
|
end
|
|
|
|
assert("Regexp - lookbehind with literal string") do
|
|
md = Regexp.new("(?<=foo)bar").match("foobar")
|
|
assert_equal "bar", md[0]
|
|
assert_nil Regexp.new("(?<=foo)bar").match("bazbar")
|
|
end
|
|
|
|
assert("Regexp - lookbehind at string start") do
|
|
# lookbehind should fail if not enough text before
|
|
assert_nil Regexp.new("(?<=abc)d").match("d")
|
|
# but should work at correct position
|
|
md = Regexp.new("(?<=abc)d").match("abcd")
|
|
assert_equal "d", md[0]
|
|
end
|
|
|
|
assert("Regexp - negative lookbehind at string start") do
|
|
# negative lookbehind succeeds when not enough text before
|
|
md = Regexp.new("(?<!x)a").match("a")
|
|
assert_equal "a", md[0]
|
|
end
|
|
|
|
assert("$1-$9 global variables") do
|
|
/(\w+)\s(\w+)/ =~ "hello world"
|
|
assert_equal "hello", $1
|
|
assert_equal "world", $2
|
|
assert_nil $3
|
|
end
|
|
|
|
assert("$1-$9 cleared on no match") do
|
|
/(\w+)/ =~ "hello"
|
|
assert_equal "hello", $1
|
|
/xyz/ =~ "abc"
|
|
assert_nil $1
|
|
end
|
|
|
|
assert("Regexp - consecutive optional quantifiers (#6853)") do
|
|
# insert_inst was over-incrementing jump offsets that pointed *at* the
|
|
# insertion site, sending earlier "skip this atom" SPLITs into the next
|
|
# atom's body. Two adjacent zero-matchable atoms then both failed even
|
|
# when both should match zero characters.
|
|
assert_equal ["a", nil], /\Aa(b)?c?\z/.match("a").to_a
|
|
assert_equal ["ab", "b"], /\Aa(b)?c?\z/.match("ab").to_a
|
|
assert_equal ["ac", nil], /\Aa(b)?c?\z/.match("ac").to_a
|
|
assert_equal ["abc", "b"], /\Aa(b)?c?\z/.match("abc").to_a
|
|
|
|
assert_equal [""], /a?b?/.match("").to_a
|
|
assert_equal [""], /a*b*/.match("").to_a
|
|
assert_equal [""], /a?b?c?d?/.match("").to_a
|
|
end
|
|
|
|
assert("Regexp - empty-matchable patterns find earliest match position") do
|
|
# When a regex can match zero characters via epsilon transitions, the
|
|
# first-byte skip-ahead optimization is unsafe: skipping past bytes
|
|
# that aren't in the first-byte set would also skip past valid
|
|
# empty-match positions.
|
|
md = /a?/.match("b")
|
|
assert_equal "", md[0]
|
|
assert_equal 0, md.begin(0)
|
|
|
|
md = /a?b?/.match("c")
|
|
assert_equal "", md[0]
|
|
assert_equal 0, md.begin(0)
|
|
end
|