Skip to content

Commit 43f2f00

Browse files
committed
Fix non-thread safe regex matching in the JVM implementation
Close #39
1 parent 52064dc commit 43f2f00

File tree

1 file changed

+40
-24
lines changed
  • src/jvmMain/kotlin/com/github/h0tk3y/betterParse/lexer

1 file changed

+40
-24
lines changed

src/jvmMain/kotlin/com/github/h0tk3y/betterParse/lexer/RegexToken.kt

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,54 @@ package com.github.h0tk3y.betterParse.lexer
33
import java.util.*
44
import java.util.regex.Matcher
55

6-
public actual class RegexToken : Token {
7-
private val pattern: String
6+
public actual class RegexToken private constructor(
7+
name: String?,
8+
ignored: Boolean,
9+
private val pattern: String,
810
private val regex: Regex
9-
private val matcher: Matcher
11+
) : Token(name, ignored) {
1012

11-
private companion object {
12-
const val inputStartPrefix = "\\A"
13+
private val threadLocalMatcher = object : ThreadLocal<Matcher>() {
14+
override fun initialValue() = regex.toPattern().matcher("")
1315
}
1416

15-
private fun prependPatternWithInputStart(patternString: String, options: Set<RegexOption>) =
16-
if (patternString.startsWith(inputStartPrefix))
17-
patternString.toRegex(options)
18-
else {
19-
val newlineAfterComments = if (RegexOption.COMMENTS in options) "\n" else ""
20-
val patternToEmbed = if (RegexOption.LITERAL in options) Regex.escape(patternString) else patternString
21-
("$inputStartPrefix(?:$patternToEmbed$newlineAfterComments)").toRegex(options - RegexOption.LITERAL)
22-
}
17+
private val matcher: Matcher get() = threadLocalMatcher.get()
18+
19+
private companion object {
20+
private const val inputStartPrefix = "\\A"
2321

24-
public actual constructor(name: String?, @Language("RegExp", "", "") patternString: String, ignored: Boolean)
25-
: super(name, ignored) {
26-
pattern = patternString
27-
regex = prependPatternWithInputStart(patternString, emptySet())
22+
private fun prependPatternWithInputStart(patternString: String, options: Set<RegexOption>) =
23+
if (patternString.startsWith(Companion.inputStartPrefix))
24+
patternString.toRegex(options)
25+
else {
26+
val newlineAfterComments = if (RegexOption.COMMENTS in options) "\n" else ""
27+
val patternToEmbed = if (RegexOption.LITERAL in options) Regex.escape(patternString) else patternString
28+
("${inputStartPrefix}(?:$patternToEmbed$newlineAfterComments)").toRegex(options - RegexOption.LITERAL)
29+
}
2830

29-
matcher = regex.toPattern().matcher("")
3031
}
3132

32-
public actual constructor(name: String?, regex: Regex, ignored: Boolean)
33-
: super(name, ignored) {
34-
pattern = regex.pattern
35-
this.regex = prependPatternWithInputStart(pattern, regex.options)
36-
matcher = this.regex.toPattern().matcher("")
37-
}
33+
public actual constructor(
34+
name: String?,
35+
@Language("RegExp", "", "") patternString: String,
36+
ignored: Boolean
37+
) : this(
38+
name,
39+
ignored,
40+
patternString,
41+
prependPatternWithInputStart(patternString, emptySet())
42+
)
43+
44+
public actual constructor(
45+
name: String?,
46+
regex: Regex,
47+
ignored: Boolean
48+
) : this(
49+
name,
50+
ignored,
51+
regex.pattern,
52+
prependPatternWithInputStart(regex.pattern, regex.options)
53+
)
3854

3955
override fun match(input: CharSequence, fromIndex: Int): Int {
4056
matcher.reset(input).region(fromIndex, input.length)

0 commit comments

Comments
 (0)