[K/N][stdlib] Support \V, \v, \H, \h, \R in regex engine
Issue #KT-50742 Fixed
This commit is contained in:
@@ -25,6 +25,37 @@ class PatternTest {
|
||||
fun assertTrue(msg: String, value: Boolean) = assertTrue(value, msg)
|
||||
fun assertFalse(msg: String, value: Boolean) = assertFalse(value, msg)
|
||||
|
||||
private fun String.asEscapeSeq() = buildString {
|
||||
this@asEscapeSeq.forEach {
|
||||
when {
|
||||
it.isLetterOrDigit() -> append(it)
|
||||
it == '\n' -> append("\\n")
|
||||
it == '\r' -> append("\\r")
|
||||
it == '\t' -> append("\\t")
|
||||
else -> {
|
||||
val hexCode = it.code.toString(16)
|
||||
append("\\u$hexCode")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertMatch(regex: Regex, string: String) {
|
||||
assertTrue(regex.matches(string), "Regex `$regex` expected to match string `${string.asEscapeSeq()}`")
|
||||
}
|
||||
|
||||
private fun assertNoMatch(regex: Regex, string: String) {
|
||||
assertFalse(regex.matches(string), "Regex `$regex` expected to not match string `${string.asEscapeSeq()}`")
|
||||
}
|
||||
|
||||
private fun assertFind(regex: Regex, string: String, expectedRange: IntRange) {
|
||||
assertEquals(
|
||||
expectedRange,
|
||||
regex.find(string)?.range,
|
||||
"Wrong `find` result for regex `$regex` in string `${string.asEscapeSeq()}`"
|
||||
)
|
||||
}
|
||||
|
||||
internal var testPatterns = arrayOf("(a|b)*abb", "(1*2*3*4*)*567", "(a|b|c|d)*aab", "(1|2|3|4|5|6|7|8|9|0)(1|2|3|4|5|6|7|8|9|0)*", "(abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ)*", "(a|b)*(a|b)*A(a|b)*lice.*", "(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)(a|b|c|d|e|f|g|h|" + "i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*(1|2|3|4|5|6|7|8|9|0)*|while|for|struct|if|do", "x(?c)y", "x(?cc)y", "x(?:c)y")
|
||||
|
||||
@Test fun testCommentsInPattern() {
|
||||
@@ -1260,4 +1291,323 @@ class PatternTest {
|
||||
|
||||
assertNull(result)
|
||||
}
|
||||
|
||||
@Test fun testVerticalWhitespaceChar() {
|
||||
// From Java 8+ `Pattern` doc:
|
||||
// \v - A vertical whitespace character: [\n\x0B\f\r\x85\u2028\u2029]
|
||||
// \V - A non-vertical whitespace character: [^\v]
|
||||
|
||||
val positiveRegex = Regex("\\v")
|
||||
val negativeRegex = Regex("\\V")
|
||||
val verticalWhitespaces = listOf("\n", "\u000B", "\u000C" /* aka \f */, "\r", "\u0085", "\u2028", "\u2029")
|
||||
val nonVerticalWhitespaces = listOf("1", "K", " ", "${Char.MIN_HIGH_SURROGATE}${Char.MIN_LOW_SURROGATE}")
|
||||
|
||||
// Smoke tests
|
||||
verticalWhitespaces.forEach {
|
||||
assertMatch(positiveRegex, it)
|
||||
assertNoMatch(negativeRegex, it)
|
||||
|
||||
assertFind(positiveRegex, "prefix$it", 6..6)
|
||||
assertFind(positiveRegex, "prefix${it}suffix", 6..6)
|
||||
assertFind(positiveRegex, "${it}suffix", 0..0)
|
||||
}
|
||||
|
||||
nonVerticalWhitespaces.forEach {
|
||||
assertNoMatch(positiveRegex, it)
|
||||
assertMatch(negativeRegex, it)
|
||||
|
||||
assertFind(negativeRegex, "\n\n$it", 2 until (2 + it.length))
|
||||
assertFind(negativeRegex, "\n\n${it}\n\n", 2 until (2 + it.length))
|
||||
assertFind(negativeRegex, "${it}\n\n", 0 until it.length)
|
||||
}
|
||||
|
||||
// Test that \v and \V can be a part of a more complex regex.
|
||||
val neighbors = listOf(
|
||||
// regex expression to its match.
|
||||
"x" to "x",
|
||||
" " to " ",
|
||||
"\\n" to "\n",
|
||||
"\\\\" to "\\",
|
||||
"[abc]" to "a",
|
||||
"." to "x",
|
||||
"\\d" to "5",
|
||||
"\\D" to "x",
|
||||
"\\s" to " ",
|
||||
"\\S" to "x",
|
||||
"\\w" to "x",
|
||||
"\\W" to "|",
|
||||
"\\p{Alnum}" to "x",
|
||||
"\\p{Space}" to " ",
|
||||
"\\p{Blank}" to " ",
|
||||
"\\p{Sc}" to "$"
|
||||
).flatMap { (expression, match) ->
|
||||
listOf(
|
||||
expression to match,
|
||||
"$expression+" to match.repeat(2),
|
||||
"$expression{2,4}" to match.repeat(3)
|
||||
)
|
||||
}
|
||||
|
||||
neighbors.forEach { (neighbor, match) ->
|
||||
// \v
|
||||
assertMatch(Regex("$neighbor\\v"), "$match\n")
|
||||
assertMatch(Regex("$neighbor\\v$neighbor"), "$match\n$match")
|
||||
assertMatch(Regex("\\v$neighbor"), "\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\v+"), "$match\n\n")
|
||||
assertMatch(Regex("$neighbor\\v+$neighbor"), "$match\n\n$match")
|
||||
assertMatch(Regex("\\v+$neighbor"), "\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\v{2,4}"), "$match\n\n\n")
|
||||
assertMatch(Regex("$neighbor\\v{2,4}$neighbor"), "$match\n\n\n$match")
|
||||
assertMatch(Regex("\\v{2,4}$neighbor"), "\n\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\v*"), match)
|
||||
assertMatch(Regex("$neighbor\\v*$neighbor"), "$match$match")
|
||||
assertMatch(Regex("\\v*$neighbor"), match)
|
||||
|
||||
// \V
|
||||
assertMatch(Regex("$neighbor\\V"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\V$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\V$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\V+"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\V+$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\V+$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\V{2,4}"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\V{2,4}$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\V{2,4}$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\V*"), match)
|
||||
assertMatch(Regex("$neighbor\\V*$neighbor"), "$match$match")
|
||||
assertMatch(Regex("\\V*$neighbor"), match)
|
||||
}
|
||||
|
||||
// Backrefs and `or` expression
|
||||
assertMatch(Regex("(\\v)x\\1"), "\nx\n")
|
||||
assertMatch(Regex("\\v|x"), "\n")
|
||||
assertMatch(Regex("\\v|x"), "x")
|
||||
|
||||
assertMatch(Regex("(\\V)\n\\1"), "x\nx")
|
||||
assertMatch(Regex("\\V|\n"), "x")
|
||||
assertMatch(Regex("\\V|\n"), "\n")
|
||||
|
||||
// Boundaries: ^, $, \b, \B
|
||||
assertMatch(Regex("^\\vx"), "\nx")
|
||||
assertNoMatch(Regex("^\\vx"), "x\n")
|
||||
assertMatch(Regex("x\\v$"), "x\n")
|
||||
assertNoMatch(Regex("x\\v$"), "\nx")
|
||||
assertMatch(Regex("abc\\b\\v"), "abc\n")
|
||||
assertMatch(Regex("abc\\b\\v"), "abc\n")
|
||||
|
||||
assertMatch(Regex("^\\V\n"), "x\n")
|
||||
assertNoMatch(Regex("^\\V\n"), "\nx")
|
||||
assertMatch(Regex("\n\\V$"), "\nx")
|
||||
assertNoMatch(Regex("\n\\V$"), "x\n")
|
||||
assertMatch(Regex("abc\\B\\V"), "abcd")
|
||||
assertMatch(Regex("abc\\B\\V"), "abcd")
|
||||
}
|
||||
|
||||
@Test fun testHorizontalWhitespaceChar() {
|
||||
// From Java 8+ `Pattern` doc:
|
||||
// \h - A horizontal whitespace character: [ \t\xA0\u1680\u180e\u2000-\u200a\u202f\u205f\u3000]
|
||||
// \H - A non-horizontal whitespace character: [^\h]
|
||||
|
||||
val positiveRegex = Regex("\\h")
|
||||
val negativeRegex = Regex("\\H")
|
||||
val verticalWhitespaces = listOf(" ", "\t", "\u00A0", "\u1680", "\u180e", "\u202f", "\u205f", "\u3000") +
|
||||
('\u2000'..'\u200a').map(Char::toString)
|
||||
val nonVerticalWhitespaces = listOf("1", "K", "\n", "${Char.MIN_HIGH_SURROGATE}${Char.MIN_LOW_SURROGATE}")
|
||||
|
||||
// Smoke tests
|
||||
verticalWhitespaces.forEach {
|
||||
assertMatch(positiveRegex, it)
|
||||
assertNoMatch(negativeRegex, it)
|
||||
|
||||
assertFind(positiveRegex, "prefix$it", 6..6)
|
||||
assertFind(positiveRegex, "prefix${it}suffix", 6..6)
|
||||
assertFind(positiveRegex, "${it}suffix", 0..0)
|
||||
}
|
||||
|
||||
nonVerticalWhitespaces.forEach {
|
||||
assertNoMatch(positiveRegex, it)
|
||||
assertMatch(negativeRegex, it)
|
||||
|
||||
assertFind(negativeRegex, " $it", 2 until (2 + it.length))
|
||||
assertFind(negativeRegex, " $it ", 2 until (2 + it.length))
|
||||
assertFind(negativeRegex, "$it ", 0 until it.length)
|
||||
}
|
||||
|
||||
// Test that \h and \H can be a part of a more complex regex.
|
||||
val neighbors = listOf(
|
||||
// regex expression to its match.
|
||||
"x" to "x",
|
||||
" " to " ",
|
||||
"\\n" to "\n",
|
||||
"\\\\" to "\\",
|
||||
"[abc]" to "a",
|
||||
"." to "x",
|
||||
"\\d" to "5",
|
||||
"\\D" to "x",
|
||||
"\\s" to " ",
|
||||
"\\S" to "x",
|
||||
"\\w" to "x",
|
||||
"\\W" to "|",
|
||||
"\\p{Alnum}" to "x",
|
||||
"\\p{Space}" to " ",
|
||||
"\\p{Blank}" to " ",
|
||||
"\\p{Sc}" to "$"
|
||||
).flatMap { (expression, match) ->
|
||||
listOf(
|
||||
expression to match,
|
||||
"$expression+" to match.repeat(2),
|
||||
"$expression{2,4}" to match.repeat(3)
|
||||
)
|
||||
}
|
||||
|
||||
neighbors.forEach { (neighbor, match) ->
|
||||
// \h
|
||||
assertMatch(Regex("$neighbor\\h"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\h$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\h$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\h+"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\h+$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\h+$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\h{2,4}"), "$match ")
|
||||
assertMatch(Regex("$neighbor\\h{2,4}$neighbor"), "$match $match")
|
||||
assertMatch(Regex("\\h{2,4}$neighbor"), " $match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\h*"), match)
|
||||
assertMatch(Regex("$neighbor\\h*$neighbor"), "$match$match")
|
||||
assertMatch(Regex("\\h*$neighbor"), match)
|
||||
|
||||
// \H
|
||||
assertMatch(Regex("$neighbor\\H"), "$match\n")
|
||||
assertMatch(Regex("$neighbor\\H$neighbor"), "$match\n$match")
|
||||
assertMatch(Regex("\\H$neighbor"), "\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\H+"), "$match\n\n")
|
||||
assertMatch(Regex("$neighbor\\H+$neighbor"), "$match\n\n$match")
|
||||
assertMatch(Regex("\\H+$neighbor"), "\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\H{2,4}"), "$match\n\n\n")
|
||||
assertMatch(Regex("$neighbor\\H{2,4}$neighbor"), "$match\n\n\n$match")
|
||||
assertMatch(Regex("\\H{2,4}$neighbor"), "\n\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\H*"), match)
|
||||
assertMatch(Regex("$neighbor\\H*$neighbor"), "$match$match")
|
||||
assertMatch(Regex("\\H*$neighbor"), match)
|
||||
}
|
||||
|
||||
// Backrefs and `or` expression
|
||||
assertMatch(Regex("(\\h)x\\1"), " x ")
|
||||
assertMatch(Regex("\\h|x"), " ")
|
||||
assertMatch(Regex("\\h|x"), "x")
|
||||
|
||||
assertMatch(Regex("(\\H) \\1"), "x x")
|
||||
assertMatch(Regex("\\H| "), "x")
|
||||
assertMatch(Regex("\\H| "), " ")
|
||||
|
||||
// Boundaries: ^, $, \b, \B
|
||||
assertMatch(Regex("^\\hx"), " x")
|
||||
assertNoMatch(Regex("^\\hx"), "x ")
|
||||
assertMatch(Regex("x\\h$"), "x ")
|
||||
assertNoMatch(Regex("x\\h$"), " x")
|
||||
assertMatch(Regex("abc\\b\\h"), "abc ")
|
||||
assertMatch(Regex("abc\\b\\h"), "abc ")
|
||||
|
||||
assertMatch(Regex("^\\H "), "x ")
|
||||
assertNoMatch(Regex("^\\H "), " x")
|
||||
assertMatch(Regex(" \\H$"), " x")
|
||||
assertNoMatch(Regex(" \\H$"), "x ")
|
||||
assertMatch(Regex("abc\\B\\H"), "abcd")
|
||||
assertMatch(Regex("abc\\B\\H"), "abcd")
|
||||
}
|
||||
|
||||
@Test fun testUnicodeLinebreakChar() {
|
||||
// From Java 8+ `Pattern` doc:
|
||||
// \R - Any Unicode linebreak sequence, is equivalent to \u000D\u000A|[\u000A\u000B\u000C\u000D\u0085\u2028\u2029]
|
||||
|
||||
val regex = Regex("\\R")
|
||||
val linebreaks = listOf("\u000D\u000A", "\u000A", "\u000B", "\u000C", "\u000D", "\u0085", "\u2028", "\u2029")
|
||||
val nonLinebreaks = listOf("1", "K", " ", "${Char.MIN_HIGH_SURROGATE}${Char.MIN_LOW_SURROGATE}")
|
||||
|
||||
// Smoke tests
|
||||
linebreaks.forEach {
|
||||
assertMatch(regex, it)
|
||||
|
||||
assertFind(regex, "prefix$it", 6 until (6 + it.length))
|
||||
assertFind(regex, "prefix${it}suffix", 6 until (6 + it.length))
|
||||
assertFind(regex, "${it}suffix", 0 until it.length)
|
||||
}
|
||||
|
||||
nonLinebreaks.forEach {
|
||||
assertNoMatch(regex, it)
|
||||
}
|
||||
|
||||
// Test that \r\n matches both \R and \R\R
|
||||
assertMatch(Regex("\\R\\R\\R"), "\r\r\n\n")
|
||||
assertMatch(Regex("\\R\\R\\R\\R"), "\r\r\n\n")
|
||||
|
||||
// Test that \R can be a part of a more complex regex.
|
||||
val neighbors = listOf(
|
||||
// regex expression to its match.
|
||||
"x" to "x",
|
||||
" " to " ",
|
||||
"\\n" to "\n",
|
||||
"\\\\" to "\\",
|
||||
"[abc]" to "a",
|
||||
"." to "x",
|
||||
"\\d" to "5",
|
||||
"\\D" to "x",
|
||||
"\\s" to " ",
|
||||
"\\S" to "x",
|
||||
"\\w" to "x",
|
||||
"\\W" to "|",
|
||||
"\\p{Alnum}" to "x",
|
||||
"\\p{Space}" to " ",
|
||||
"\\p{Blank}" to " ",
|
||||
"\\p{Sc}" to "$"
|
||||
).flatMap { (expression, match) ->
|
||||
listOf(
|
||||
expression to match,
|
||||
"$expression+" to match.repeat(2),
|
||||
"$expression{2,4}" to match.repeat(3)
|
||||
)
|
||||
}
|
||||
|
||||
neighbors.forEach { (neighbor, match) ->
|
||||
assertMatch(Regex("$neighbor\\R"), "$match\n")
|
||||
assertMatch(Regex("$neighbor\\R$neighbor"), "$match\n$match")
|
||||
assertMatch(Regex("\\R$neighbor"), "\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\R+"), "$match\n\n")
|
||||
assertMatch(Regex("$neighbor\\R+$neighbor"), "$match\n\n$match")
|
||||
assertMatch(Regex("\\R+$neighbor"), "\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\R{2,4}"), "$match\n\n\n")
|
||||
assertMatch(Regex("$neighbor\\R{2,4}$neighbor"), "$match\n\n\n$match")
|
||||
assertMatch(Regex("\\R{2,4}$neighbor"), "\n\n\n$match")
|
||||
|
||||
assertMatch(Regex("$neighbor\\R*"), match)
|
||||
assertMatch(Regex("$neighbor\\R*$neighbor"), "$match$match")
|
||||
assertMatch(Regex("\\R*$neighbor"), match)
|
||||
}
|
||||
|
||||
// Backrefs and `or` expression
|
||||
assertMatch(Regex("(\\R)x\\1"), "\nx\n")
|
||||
assertMatch(Regex("\\R|x"), "\n")
|
||||
assertMatch(Regex("\\R|x"), "x")
|
||||
|
||||
// Boundaries: ^, $, \b, \B
|
||||
assertMatch(Regex("^\\Rx"), "\nx")
|
||||
assertNoMatch(Regex("^\\Rx"), "x\n")
|
||||
assertMatch(Regex("x\\R$"), "x\n")
|
||||
assertNoMatch(Regex("x\\R$"), "\nx")
|
||||
assertMatch(Regex("abc\\b\\R"), "abc\n")
|
||||
assertMatch(Regex("abc\\b\\R"), "abc\n")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user