Skip to content

Commit 48ed6a1

Browse files
committed
fix(ast/estree): fix span for TemplateElement in TS AST (#10315)
Part of #9705. TS-ESLint differs from Acorn in the `span` of `TemplateElement`. TS-ESLint includes the preceding `` ` `` or `}` and following `${` or `` ` `` in the span. ```js const template = `abc${x}def${x}ghi`; // Acorn: ^^^ ^^^ ^^^ // TS-ESLint: ^^^^^^ ^^^^^^ ^^^^^ ``` Make the span follow TS-ESLint in the TS AST.
1 parent 48c711a commit 48ed6a1

File tree

6 files changed

+62
-169
lines changed

6 files changed

+62
-169
lines changed

crates/oxc_ast/src/ast/js.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,9 +462,9 @@ pub struct TaggedTemplateExpression<'a> {
462462
#[ast(visit)]
463463
#[derive(Debug, Clone)]
464464
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
465+
#[estree(via = TemplateElementConverter)]
465466
pub struct TemplateElement<'a> {
466467
pub span: Span,
467-
#[estree(via = TemplateElementValue)]
468468
pub value: TemplateElementValue<'a>,
469469
pub tail: bool,
470470
/// The template element contains lone surrogates.

crates/oxc_ast/src/generated/derive_estree.rs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -331,13 +331,7 @@ impl ESTree for TaggedTemplateExpression<'_> {
331331

332332
impl ESTree for TemplateElement<'_> {
333333
fn serialize<S: Serializer>(&self, serializer: S) {
334-
let mut state = serializer.serialize_struct();
335-
state.serialize_field("type", &JsonSafeString("TemplateElement"));
336-
state.serialize_field("start", &self.span.start);
337-
state.serialize_field("end", &self.span.end);
338-
state.serialize_field("value", &crate::serialize::TemplateElementValue(self));
339-
state.serialize_field("tail", &self.tail);
340-
state.end();
334+
crate::serialize::TemplateElementConverter(self).serialize(serializer)
341335
}
342336
}
343337

crates/oxc_ast/src/serialize.rs

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,20 +417,61 @@ impl ESTree for RegExpFlagsConverter<'_> {
417417
}
418418
}
419419

420+
/// Converter for `TemplateElement`.
421+
///
422+
/// Decode `cooked` if it contains lone surrogates.
423+
///
424+
/// Also adjust span in TS AST.
425+
/// TS-ESLint produces a different span from Acorn:
426+
/// ```js
427+
/// const template = `abc${x}def${x}ghi`;
428+
/// // Acorn: ^^^ ^^^ ^^^
429+
/// // TS-ESLint: ^^^^^^ ^^^^^^ ^^^^^
430+
/// ```
431+
// TODO: Raise an issue on TS-ESLint and see if they'll change span to match Acorn.
432+
#[ast_meta]
433+
#[estree(raw_deser = r#"
434+
const tail = DESER[bool](POS_OFFSET.tail),
435+
start = DESER[u32](POS_OFFSET.span.start) /* IF_TS */ - 1 /* END_IF_TS */,
436+
end = DESER[u32](POS_OFFSET.span.end) /* IF_TS */ + 2 - tail /* END_IF_TS */,
437+
value = DESER[TemplateElementValue](POS_OFFSET.value);
438+
if (value.cooked !== null && DESER[bool](POS_OFFSET.lone_surrogates)) {
439+
value.cooked = value.cooked
440+
.replace(/\uFFFD(.{4})/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
441+
}
442+
{ type: 'TemplateElement', start, end, value, tail }
443+
"#)]
444+
pub struct TemplateElementConverter<'a, 'b>(pub &'b TemplateElement<'a>);
445+
446+
impl ESTree for TemplateElementConverter<'_, '_> {
447+
fn serialize<S: Serializer>(&self, serializer: S) {
448+
let element = self.0;
449+
let mut state = serializer.serialize_struct();
450+
state.serialize_field("type", &JsonSafeString("TemplateElement"));
451+
452+
let mut span = element.span;
453+
if S::INCLUDE_TS_FIELDS {
454+
span.start -= 1;
455+
span.end += if element.tail { 1 } else { 2 };
456+
}
457+
state.serialize_field("start", &span.start);
458+
state.serialize_field("end", &span.end);
459+
460+
state.serialize_field("value", &TemplateElementValue(element));
461+
state.serialize_field("tail", &element.tail);
462+
state.end();
463+
}
464+
}
465+
420466
/// Serializer for `value` field of `TemplateElement`.
421467
///
422468
/// Handle when `lone_surrogates` flag is set, indicating the cooked string contains lone surrogates.
469+
///
470+
/// Implementation for `raw_deser` is included in `TemplateElementConverter` above.
423471
#[ast_meta]
424472
#[estree(
425473
ts_type = "TemplateElementValue",
426-
raw_deser = r#"
427-
let value = DESER[TemplateElementValue](POS_OFFSET.value);
428-
if (value.cooked !== null && DESER[bool](POS_OFFSET.lone_surrogates)) {
429-
value.cooked = value.cooked
430-
.replace(/\uFFFD(.{4})/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
431-
}
432-
value
433-
"#
474+
raw_deser = "(() => { throw new Error('Should not appear in deserializer code'); })()"
434475
)]
435476
pub struct TemplateElementValue<'a, 'b>(pub &'b TemplateElement<'a>);
436477

napi/parser/deserialize-js.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,18 +150,15 @@ function deserializeTaggedTemplateExpression(pos) {
150150
}
151151

152152
function deserializeTemplateElement(pos) {
153-
let value = deserializeTemplateElementValue(pos + 8);
153+
const tail = deserializeBool(pos + 40),
154+
start = deserializeU32(pos),
155+
end = deserializeU32(pos + 4),
156+
value = deserializeTemplateElementValue(pos + 8);
154157
if (value.cooked !== null && deserializeBool(pos + 41)) {
155158
value.cooked = value.cooked
156159
.replace(/\uFFFD(.{4})/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
157160
}
158-
return {
159-
type: 'TemplateElement',
160-
start: deserializeU32(pos),
161-
end: deserializeU32(pos + 4),
162-
value,
163-
tail: deserializeBool(pos + 40),
164-
};
161+
return { type: 'TemplateElement', start, end, value, tail };
165162
}
166163

167164
function deserializeTemplateElementValue(pos) {

napi/parser/deserialize-ts.js

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -165,18 +165,15 @@ function deserializeTaggedTemplateExpression(pos) {
165165
}
166166

167167
function deserializeTemplateElement(pos) {
168-
let value = deserializeTemplateElementValue(pos + 8);
168+
const tail = deserializeBool(pos + 40),
169+
start = deserializeU32(pos) - 1,
170+
end = deserializeU32(pos + 4) + 2 - tail,
171+
value = deserializeTemplateElementValue(pos + 8);
169172
if (value.cooked !== null && deserializeBool(pos + 41)) {
170173
value.cooked = value.cooked
171174
.replace(/\uFFFD(.{4})/g, (_, hex) => String.fromCodePoint(parseInt(hex, 16)));
172175
}
173-
return {
174-
type: 'TemplateElement',
175-
start: deserializeU32(pos),
176-
end: deserializeU32(pos + 4),
177-
value,
178-
tail: deserializeBool(pos + 40),
179-
};
176+
return { type: 'TemplateElement', start, end, value, tail };
180177
}
181178

182179
function deserializeTemplateElementValue(pos) {

0 commit comments

Comments
 (0)