Skip to content

Commit adf8f61

Browse files
committed
fix(native-preview): preserve template surrogate escapes
1 parent c3690db commit adf8f61

5 files changed

Lines changed: 109 additions & 6 deletions

File tree

_packages/native-preview/test/async/api.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,38 @@ test("unicode escapes", async () => {
234234
}
235235
});
236236

237+
test("template unicode escapes", async () => {
238+
const api = spawnAPI({
239+
"/tsconfig.json": "{}",
240+
"/src/index.ts": "`\\ud800${0}\\udc00`",
241+
});
242+
try {
243+
const snapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
244+
const project = snapshot.getProject("/tsconfig.json")!;
245+
const sourceFile = await project.program.getSourceFile("/src/index.ts");
246+
assert.ok(sourceFile);
247+
248+
let sawHead = false;
249+
let sawTail = false;
250+
sourceFile.forEachChild(function visit(node) {
251+
if (isTemplateHead(node)) {
252+
assert.equal(node.text, "\ud800");
253+
sawHead = true;
254+
}
255+
else if (isTemplateTail(node)) {
256+
assert.equal(node.text, "\udc00");
257+
sawTail = true;
258+
}
259+
node.forEachChild(visit);
260+
});
261+
assert.ok(sawHead);
262+
assert.ok(sawTail);
263+
}
264+
finally {
265+
await api.close();
266+
}
267+
});
268+
237269
test("Object equality", async () => {
238270
const api = spawnAPI();
239271
try {

_packages/native-preview/test/sync/api.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,38 @@ test("unicode escapes", () => {
242242
}
243243
});
244244

245+
test("template unicode escapes", () => {
246+
const api = spawnAPI({
247+
"/tsconfig.json": "{}",
248+
"/src/index.ts": "`\\ud800${0}\\udc00`",
249+
});
250+
try {
251+
const snapshot = api.updateSnapshot({ openProject: "/tsconfig.json" });
252+
const project = snapshot.getProject("/tsconfig.json")!;
253+
const sourceFile = project.program.getSourceFile("/src/index.ts");
254+
assert.ok(sourceFile);
255+
256+
let sawHead = false;
257+
let sawTail = false;
258+
sourceFile.forEachChild(function visit(node) {
259+
if (isTemplateHead(node)) {
260+
assert.equal(node.text, "\ud800");
261+
sawHead = true;
262+
}
263+
else if (isTemplateTail(node)) {
264+
assert.equal(node.text, "\udc00");
265+
sawTail = true;
266+
}
267+
node.forEachChild(visit);
268+
});
269+
assert.ok(sawHead);
270+
assert.ok(sawTail);
271+
}
272+
finally {
273+
api.close();
274+
}
275+
});
276+
245277
test("Object equality", () => {
246278
const api = spawnAPI();
247279
try {

internal/api/encoder/encoder.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -547,21 +547,21 @@ func recordExtendedData_SourceFile(node *ast.Node, strs *stringTable, positionMa
547547

548548
func recordExtendedData_TemplateHead(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
549549
n := node.AsTemplateHead()
550-
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
550+
textIndex := strs.add(encodeTemplateTextForJS(n.Text, n.RawText), node.Kind, node.Pos(), node.End())
551551
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
552552
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
553553
}
554554

555555
func recordExtendedData_TemplateMiddle(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
556556
n := node.AsTemplateMiddle()
557-
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
557+
textIndex := strs.add(encodeTemplateTextForJS(n.Text, n.RawText), node.Kind, node.Pos(), node.End())
558558
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
559559
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
560560
}
561561

562562
func recordExtendedData_TemplateTail(node *ast.Node, strs *stringTable, positionMap *ast.PositionMap, extendedData *[]byte, structuredData *[]byte) {
563563
n := node.AsTemplateTail()
564-
textIndex := strs.add(n.Text, node.Kind, node.Pos(), node.End())
564+
textIndex := strs.add(encodeTemplateTextForJS(n.Text, n.RawText), node.Kind, node.Pos(), node.End())
565565
rawTextIndex := strs.add(n.RawText, node.Kind, node.Pos(), node.End())
566566
*extendedData = appendUint32s(*extendedData, textIndex, rawTextIndex, uint32(n.TemplateFlags))
567567
}

internal/api/encoder/encoder_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,25 @@ func TestEncodeSourceFilePreservesSurrogateEscapes(t *testing.T) {
7575
})
7676
}
7777

78+
func TestEncodeSourceFilePreservesTemplateSurrogateEscapes(t *testing.T) {
79+
t.Parallel()
80+
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
81+
FileName: "/test.ts",
82+
Path: "/test.ts",
83+
}, "let s = `\\uD800${1}\\uDC00`;", core.ScriptKindTS)
84+
85+
buf, err := encoder.EncodeSourceFile(sourceFile)
86+
assert.NilError(t, err)
87+
88+
headText, ok := findExtendedNodeText(buf, ast.KindTemplateHead)
89+
assert.Assert(t, ok)
90+
assert.DeepEqual(t, headText, []byte{0xed, 0xa0, 0x80})
91+
92+
tailText, ok := findExtendedNodeText(buf, ast.KindTemplateTail)
93+
assert.Assert(t, ok)
94+
assert.DeepEqual(t, tailText, []byte{0xed, 0xb0, 0x80})
95+
}
96+
7897
func TestEncodeSourceFileFallsBackForUnterminatedSurrogateEscape(t *testing.T) {
7998
t.Parallel()
8099
sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{

internal/api/encoder/literal_text.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,40 @@ func decodeQuotedLiteralText(raw string) (text string, hasSurrogate bool, ok boo
5050
if len(raw) < 2 {
5151
return "", false, false
5252
}
53+
return decodeEscapedLiteralText(raw[1:len(raw)-1], false)
54+
}
55+
56+
func encodeTemplateTextForJS(text string, rawText string) string {
57+
decoded, hasSurrogate, ok := decodeEscapedLiteralText(rawText, true)
58+
if !ok || !hasSurrogate {
59+
return text
60+
}
61+
return decoded
62+
}
63+
64+
func decodeEscapedLiteralText(raw string, normalizeTemplateLineEndings bool) (text string, hasSurrogate bool, ok bool) {
5365
var out strings.Builder
54-
for i := 1; i < len(raw)-1; {
66+
for i := 0; i < len(raw); {
5567
if raw[i] != '\\' {
68+
if normalizeTemplateLineEndings && raw[i] == '\r' {
69+
out.WriteByte('\n')
70+
i++
71+
if i < len(raw) && raw[i] == '\n' {
72+
i++
73+
}
74+
continue
75+
}
5676
out.WriteByte(raw[i])
5777
i++
5878
continue
5979
}
60-
ch, next, ok := decodeEscape(raw, i, len(raw)-1)
80+
ch, next, ok := decodeEscape(raw, i, len(raw))
6181
if !ok {
6282
return "", false, false
6383
}
6484
if codePointIsHighSurrogate(ch) {
6585
hasSurrogate = true
66-
if nextCh, nextNext, ok := decodeUnicodeEscape(raw, next, len(raw)-1); ok && codePointIsLowSurrogate(nextCh) {
86+
if nextCh, nextNext, ok := decodeUnicodeEscape(raw, next, len(raw)); ok && codePointIsLowSurrogate(nextCh) {
6787
out.WriteRune(surrogatePairToCodepoint(ch, nextCh))
6888
i = nextNext
6989
continue

0 commit comments

Comments
 (0)