| 71 | * Uses RE2 for linear-time matching, providing ReDoS protection. |
| 72 | */ |
| 73 | export class UserRegex implements RegexLike { |
| 74 | private readonly _re2: RE2JS; |
| 75 | private readonly _pattern: string; |
| 76 | private readonly _flags: string; |
| 77 | private readonly _global: boolean; |
| 78 | private readonly _ignoreCase: boolean; |
| 79 | private readonly _multiline: boolean; |
| 80 | private _lastIndex = 0; |
| 81 | // Cache native RegExp for compatibility - created lazily |
| 82 | private _nativeRegex: RegExp | null = null; |
| 83 | // Reusable RE2 Matcher to avoid per-call allocation in tight grep loops. |
| 84 | // Matcher allocation dominates regex.test/exec cost when called once per line |
| 85 | // across thousands of lines. We mutate charSequence in-place (not resetMatcherInput, |
| 86 | // which is broken in re2js 1.2.1 — see acquireMatcher). |
| 87 | private _matcher: ReturnType<RE2JS["matcher"]> | null = null; |
| 88 | private _matcherInput: string | null = null; |
| 89 | |
| 90 | private acquireMatcher(input: string): ReturnType<RE2JS["matcher"]> { |
| 91 | if (this._matcher === null) { |
| 92 | this._matcher = this._re2.matcher(input); |
| 93 | this._matcherInput = input; |
| 94 | return this._matcher; |
| 95 | } |
| 96 | if (this._matcherInput !== input) { |
| 97 | // Swap the cached Utf16MatcherInput's charSequence in-place to avoid |
| 98 | // allocating a new Matcher per call. RE2JS's resetMatcherInput is not |
| 99 | // safe with raw strings (the constructor wraps strings via |
| 100 | // MatcherInput.utf16, but resetMatcherInput assigns its argument |
| 101 | // directly and then calls .length() as a method, which throws on a |
| 102 | // raw string). MatcherInput is not exported, so we mutate the existing |
| 103 | // wrapper's charSequence field — Matcher.reset() reads matcherInput.length() |
| 104 | // afterwards, so the new length is picked up correctly. |
| 105 | // biome-ignore lint/suspicious/noExplicitAny: reaching into re2js internals |
| 106 | (this._matcher as any).matcherInput.charSequence = input; |
| 107 | this._matcherInput = input; |
| 108 | } |
| 109 | this._matcher.reset(); |
| 110 | return this._matcher; |
| 111 | } |
| 112 | |
| 113 | constructor(pattern: string, flags = "") { |
| 114 | this._pattern = pattern; |
| 115 | this._flags = flags; |
| 116 | this._global = flags.includes("g"); |
| 117 | this._ignoreCase = flags.includes("i"); |
| 118 | this._multiline = flags.includes("m"); |
| 119 | |
| 120 | try { |
| 121 | const translatedPattern = translatePattern(pattern); |
| 122 | const re2Flags = convertFlags(flags); |
| 123 | this._re2 = RE2JS.compile(translatedPattern, re2Flags); |
| 124 | } catch (e) { |
| 125 | if (e instanceof RE2JSSyntaxException) { |
| 126 | // Provide helpful error messages for unsupported RE2 features |
| 127 | const msg = e.message || ""; |
| 128 | let explanation = ""; |
| 129 | |
| 130 | if ( |
nothing calls this directly
no outgoing calls
no test coverage detected
searching dependent graphs…