Skip to content

fix(type): preserve escaped backslash in regex string literals - #1634

Merged
ssalbdivad merged 1 commit into
arktypeio:mainfrom
spokodev:fix/regex-literal-escaped-backslash
Sep 18, 2026
Merged

ssalbdivad merged 1 commit into
arktypeio:mainfrom
spokodev:fix/regex-literal-escaped-backslash

Conversation

@spokodev

@spokodev spokodev commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

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 d is /^\\d$/ as a RegExp. Written as a string literal the backslashes double again, so it becomes "/^\\\\d$/".

import { type } from "arktype"

const FromInstance = type(/^\\d$/)     // literal backslash + d
const FromString = type("/^\\\\d$/")   // same intent, written as a string

FromInstance.allows("\\d")  // true   ("\\d" is the two character string: backslash, d)
FromString.allows("\\d")    // actual false, expected true
FromString.allows("5")      // actual true,  expected false

FromString is compiled as the digit class \d instead of a literal backslash plus d, 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 with Scanner.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, \w are metacharacters, so collapsing turns a literal backslash into a metacharacter and changes what the pattern accepts.

Fix

Add an optional escapeEscape argument to shiftUntilEscapable. For regex tokens the parser emits \\ instead of collapsing it, keeping the pattern source equal to the RegExp-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 own RegExp-instance path (type(/^\\d$/)) already preserves this, and the docs treat type("/regex/") as equivalent to the corresponding RegExp, 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.ts covering string-literal vs RegExp-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 failing snapPopulation cases are pre-existing under --skipTypes and unrelated to this change).

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 escapeEscape parameter to Scanner.shiftUntilEscapable to opt into preserving \\ as a literal backslash.
  • Wire regex literals (/ and x/) in both runtime and type-level parseEnclosed to pass Backslash.
  • 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.

Pullfrog  | View workflow run | Using 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.
@ssalbdivad
ssalbdivad force-pushed the fix/regex-literal-escaped-backslash branch from 510ca5d to 1672bfd Compare September 18, 2026 17:53

@ssalbdivad ssalbdivad left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified the three failing cases on main and the two controls. Rebased onto latest.

@ssalbdivad

ssalbdivad commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Thanks @spokodev, great catch. The lone case is the one that sold me — "/^\\\\$/" was parsing to ^\$, so a pattern meant to match a backslash silently matched a literal $ instead.

This also completes a convention rather than introducing one: arkregex already passes Backslash as escapeEscape in charset.ts, and parseEnclosed was the last place hardcoding "".

@ssalbdivad
ssalbdivad merged commit 93dbc3f into arktypeio:main Sep 18, 2026
6 checks passed
@github-project-automation github-project-automation Bot moved this from To do to Done (merged or closed) in arktypeio Sep 18, 2026
@ssalbdivad ssalbdivad mentioned this pull request Sep 24, 2026
4 tasks done
ssalbdivad added a commit that referenced this pull request Sep 24, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done (merged or closed)

Development

Successfully merging this pull request may close these issues.

2 participants