Skip to content

Commit 6a92674

Browse files
committed
Reject malformed/out-of-range \u and \x escapes in strings
A string escape must be well-formed: \u{cp} with cp <= 0x10FFFF, a 4-hex \uXXXX, or a 2-hex \xXX; any other \<char> (\n, \\, \q non-escape, line continuation) stays valid. A \u/\x that doesn't match its strict form makes the string match no token, so the lexer throws — TS's exact rejection. The in-range code point is encoded directly in the regex: 0* leading zeros then 1-5 hex (0-0xFFFFF) or 10+4 hex (0x100000-0x10FFFF). Rejects \u{110000}, \u{FFFFFFFF}, \u{-DDDD}, \u{r}, \u{}, \u{, \u{67 in string literals. FP 116->108 (8 unicodeExtendedEscapesInStrings cases), FN stays 0 (3376/3376), TP unchanged, highlighter 99.3% unchanged. Artifacts regenerated. Templates share the same escape fragment but tokenize via the engine — next.
1 parent 42ed333 commit 6a92674

4 files changed

Lines changed: 13 additions & 4 deletions

File tree

examples/lezer/tokens.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
// BinaryNumber: /0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/
1818
// BigInt: /[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/
1919
// Number: /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/
20-
// String: /"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/
20+
// String: /"(?:[^"\\]|\\(?:u\{0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux]))*"|'(?:[^'\\]|\\(?:u\{0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux]))*'/
2121
// Decorator: /@(?:[a-zA-Z_$][a-zA-Z0-9_$.]*)?/
2222
//
2323
// NOTE: `@lezer/generator` passes the matching term ids in; the names below must

examples/lezer/typescript.grammar

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ Program {
259259
// INCOMPLETE: BinaryNumber regex /0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
260260
// INCOMPLETE: BigInt regex /[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
261261
// INCOMPLETE: Number regex /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
262-
// INCOMPLETE: String regex /"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/ exceeds Lezer token syntax; external tokenizer.
262+
// INCOMPLETE: String regex /"(?:[^"\\]|\\(?:u\{0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux]))*"|'(?:[^'\\]|\\(?:u\{0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux]))*'/ exceeds Lezer token syntax; external tokenizer.
263263
// INCOMPLETE: Template is produced by the external tokenizer
264264
// (context-sensitive: template interpolation).
265265
// INCOMPLETE: Regex is produced by the external tokenizer

examples/tree-sitter/grammar.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/typescript.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,16 @@ const OctalNumber = token(/0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\])/,
2020
const BinaryNumber = token(/0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.binary' });
2121
const BigInt_ = token(/[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.bigint' });
2222
const Number_ = token(/[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/);
23-
const String_ = token(/"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/, {
23+
// A well-formed JS escape, used in the string-body pattern below. `\u`/`\x` must
24+
// match their strict forms — a `\u{cp}` with cp ≤ 0x10FFFF, a 4-hex `\uXXXX`, or a
25+
// 2-hex `\xXX` — while `\` + any *other* char (\n, \\, \q non-escape, line
26+
// continuation) stays valid via `[^ux]`. A malformed `\u`/`\x` (e.g. `\u{110000}`,
27+
// `\u{r}`, `\u{}`, `\u{67`) matches no escape, so the string matches no token and the
28+
// lexer throws — TS's exact rejection. The in-range codepoint is `0*` leading zeros
29+
// then 1–5 hex (0–0xFFFFF) or `10`+4 hex (0x100000–0x10FFFF).
30+
const codePoint = String.raw`0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})`;
31+
const escape = String.raw`\\(?:u\{${codePoint}\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux])`;
32+
const String_ = token(new RegExp(`"(?:[^"\\\\]|${escape})*"|'(?:[^'\\\\]|${escape})*'`), {
2433
string: true,
2534
escape: /\\(?:[nrtbfv0'"\\]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\{[0-9a-fA-F]+\})/,
2635
});

0 commit comments

Comments
 (0)