Repository navigation
fix(type): preserve escaped backslash in regex string literals - #1634
Conversation
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the fix ensures /regex/ string literals preserve escaped backslashes so the runtime and type-level parsers agree with the RegExp-instance form.
- Add
escapeEscapeparameter toScanner.shiftUntilEscapableto opt into preserving\\as a literal backslash. - Wire regex literals (
/andx/) in both runtime and type-levelparseEnclosedto passBackslash. - Leave quoted-string and date literal escaping behavior unchanged.
- Add regression tests for escaped-backslash parity, literal backslash, lone
\\, single-backslash classes, and escaped terminators.
Kimi K2 (free via Pullfrog for OSS) | 𝕏
A regex written as a string literal was parsed with the string-escape
rule, which collapses `\\` into a single `\`. That is correct for a JS
string but corrupts a regex source, where `\\` is a literal backslash
and `\d`/`\b`/`\w` are metacharacters. So `type("/^\\\\d$/")` silently
became the digit-class regex `/^\d$/`, accepting "5" and rejecting "\d",
disagreeing with the equivalent RegExp-instance form.
Add an optional `escapeEscape` argument to `shiftUntilEscapable` and pass
a backslash for regex tokens in both the runtime and type-level parsers,
so a doubled backslash is preserved in the pattern source. Quoted-string
and date literals keep collapsing `\\` as before; escaped terminators
`\/` and single-backslash classes `\d`/`\w`/`\t` are unaffected.
510ca5d to
1672bfd
Compare
ssalbdivad
left a comment
There was a problem hiding this comment.
Verified the three failing cases on main and the two controls. Rebased onto latest.
|
Thanks @spokodev, great catch. The lone case is the one that sold me — This also completes a convention rather than introducing one: |
Bumps all publishable packages and documents the changes merged since 2.2.4: - preserve escaped backslashes in regex literals (#1634, @spokodev) - fix recursive discriminated unions referenced from a Record (#1641, @xianjianlf2) - prevent a crash validating unions of object arrays (#1638, @xianjianlf2) - merge index-derived props with a declared key of the same name (#1659) - preserve parameter labels when a type.fn implementation annotates an optional param (#1615, @aswinsvijay) #1634 changes behavior for regex literals that contain `\\`: `"/\\\\d/"` previously compiled to the digit class `\d` and now matches a literal backslash followed by "d", matching the equivalent RegExp instance. The changelog calls this out with a migration note. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

A regex written as a string literal loses escaped backslashes, so a pattern that should match a literal backslash is compiled as a metacharacter class and validates the wrong values. The string-literal form and the equivalent
RegExp-instance form disagree, even though the docs treat them as equivalent.Repro
A regex matching a literal backslash followed by
dis/^\\d$/as aRegExp. Written as a string literal the backslashes double again, so it becomes"/^\\\\d$/".FromStringis compiled as the digit class\dinstead of a literal backslash plusd, so it rejects the value it should accept and accepts the value it should reject.FromInstance, which does not go through the string scanner, is correct, so the two forms diverge for the same intended pattern.Root cause
Both forms end in
new RegExp(source), but the string-literal path reads the source between the slashes withScanner.shiftUntilEscapable, which collapses every\\into a single\. That rule is correct for a JS string literal, where\\denotes one backslash. It is wrong for a regex source: there\\is a literal backslash while\d,\b,\ware metacharacters, so collapsing turns a literal backslash into a metacharacter and changes what the pattern accepts.Fix
Add an optional
escapeEscapeargument toshiftUntilEscapable. For regex tokens the parser emits\\instead of collapsing it, keeping the pattern source equal to theRegExp-instance form. The change is applied in both the runtime parser and its type-level twin, so runtime validation and inferred types stay in agreement. Quoted-string and date literals are unchanged and still collapse\\; escaped terminators (\/) and single-backslash classes (\d,\w,\t) are unaffected.Authority
A regex literal's content is a regex source, not a JS string, so
\\in it means one literal backslash. arktype's ownRegExp-instance path (type(/^\\d$/)) already preserves this, and the docs treattype("/regex/")as equivalent to the correspondingRegExp, so the string form must produce the same pattern. The string scanner instead applied JS-string un-escaping to the regex source, which is the divergence this fixes.Tests
Added cases in
ark/type/__tests__/regex.test.tscovering string-literal vsRegExp-instance parity, literal-backslash acceptance, a lone escaped backslash, and regression guards for single-backslash classes and escaped terminators. Verified red on the current base (the parity and backslash assertions fail without the parser change) and green with it. Full package suite passes (1774 passing; the 2 failingsnapPopulationcases are pre-existing under--skipTypesand unrelated to this change).