Skip to content

Commit b662df4

Browse files
committed
refactor(ast/estree): alter Program start span with converter (#10195)
#10134 fixed the start span of `Program` in TS AST to align with TS-ESLint. Change the method this is achieved by - use a custom converter instead of mutating the AST in `to_estree_ts_json` and `to_pretty_estree_ts_json`. This method is more convoluted, but I think it's preferable if serialization does not have side effect of mutating the AST.
1 parent abc9ea4 commit b662df4

File tree

5 files changed

+73
-36
lines changed

5 files changed

+73
-36
lines changed

crates/oxc_ast/src/ast/js.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ use super::{macros::inherit_variants, *};
3030
)]
3131
#[derive(Debug)]
3232
#[generate_derive(CloneIn, Dummy, TakeIn, GetSpan, GetSpanMut, ContentEq, ESTree)]
33-
#[estree(field_order(span, directives, source_type, hashbang))]
33+
#[estree(field_order(span, directives, source_type, hashbang), via = ProgramConverter)]
3434
pub struct Program<'a> {
3535
pub span: Span,
3636
pub source_type: SourceType,

crates/oxc_ast/src/generated/derive_estree.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,7 @@ use crate::ast::ts::*;
1616

1717
impl ESTree for Program<'_> {
1818
fn serialize<S: Serializer>(&self, serializer: S) {
19-
let mut state = serializer.serialize_struct();
20-
state.serialize_field("type", &JsonSafeString("Program"));
21-
state.serialize_field("start", &self.span.start);
22-
state.serialize_field("end", &self.span.end);
23-
state.serialize_field(
24-
"body",
25-
&AppendToConcat { array: &self.directives, after: &self.body },
26-
);
27-
self.source_type.serialize(FlatStructSerializer(&mut state));
28-
state.serialize_field("hashbang", &self.hashbang);
29-
state.end();
19+
crate::serialize::ProgramConverter(self).serialize(serializer)
3020
}
3121
}
3222

crates/oxc_ast/src/serialize.rs

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use oxc_ast_macros::ast_meta;
55
use oxc_estree::{
66
CompactJSSerializer, CompactTSSerializer, ESTree, JsonSafeString, LoneSurrogatesString,
77
PrettyJSSerializer, PrettyTSSerializer, SequenceSerializer, Serializer, StructSerializer,
8+
ser::AppendToConcat,
89
};
910
use oxc_span::GetSpan;
1011

@@ -37,16 +38,7 @@ const JSON_CAPACITY_RATIO_PRETTY: usize = 80;
3738

3839
impl Program<'_> {
3940
/// Serialize AST to ESTree JSON, including TypeScript fields.
40-
pub fn to_estree_ts_json(&mut self) -> String {
41-
// Set start span to first token of first directive or statement. This is required because
42-
// unlike Acorn, TS-ESLint excludes whitespace and comments from the `Program` start span.
43-
// See https://github.com/oxc-project/oxc/pull/10134 for more info.
44-
if let Some(first_directive) = self.directives.first() {
45-
self.span.start = first_directive.span.start;
46-
} else if let Some(first_stmt) = self.body.first() {
47-
self.span.start = first_stmt.span().start;
48-
}
49-
41+
pub fn to_estree_ts_json(&self) -> String {
5042
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_COMPACT;
5143
let mut serializer = CompactTSSerializer::with_capacity(capacity);
5244
self.serialize(&mut serializer);
@@ -62,16 +54,7 @@ impl Program<'_> {
6254
}
6355

6456
/// Serialize AST to pretty-printed ESTree JSON, including TypeScript fields.
65-
pub fn to_pretty_estree_ts_json(&mut self) -> String {
66-
// Set start span to first token of first directive or statement. This is required because
67-
// unlike Acorn, TS-ESLint excludes whitespace and comments from the `Program` start span.
68-
// See https://github.com/oxc-project/oxc/pull/10134 for more info.
69-
if let Some(first_directive) = self.directives.first() {
70-
self.span.start = first_directive.span.start;
71-
} else if let Some(first_stmt) = self.body.first() {
72-
self.span.start = first_stmt.span().start;
73-
}
74-
57+
pub fn to_pretty_estree_ts_json(&self) -> String {
7558
let capacity = self.source_text.len() * JSON_CAPACITY_RATIO_PRETTY;
7659
let mut serializer = PrettyTSSerializer::with_capacity(capacity);
7760
self.serialize(&mut serializer);
@@ -87,6 +70,65 @@ impl Program<'_> {
8770
}
8871
}
8972

73+
// --------------------
74+
// Program
75+
// --------------------
76+
77+
/// Serializer for `Program`.
78+
///
79+
/// In TS AST, set start span to start of first directive or statement.
80+
/// This is required because unlike Acorn, TS-ESLint excludes whitespace and comments
81+
/// from the `Program` start span.
82+
/// See <https://github.com/oxc-project/oxc/pull/10134> for more info.
83+
#[ast_meta]
84+
#[estree(raw_deser = "
85+
const body = DESER[Vec<Directive>](POS_OFFSET.directives);
86+
body.push(...DESER[Vec<Statement>](POS_OFFSET.body));
87+
let start = DESER[u32](POS_OFFSET.span.start);
88+
/* IF_TS */
89+
if (body.length > 0) start = body[0].start;
90+
/* END_IF_TS */
91+
const program = {
92+
type: 'Program',
93+
start,
94+
end: DESER[u32](POS_OFFSET.span.end),
95+
body,
96+
sourceType: DESER[ModuleKind](POS_OFFSET.source_type.module_kind),
97+
hashbang: DESER[Option<Hashbang>](POS_OFFSET.hashbang),
98+
};
99+
program
100+
")]
101+
pub struct ProgramConverter<'a, 'b>(pub &'b Program<'a>);
102+
103+
impl ESTree for ProgramConverter<'_, '_> {
104+
fn serialize<S: Serializer>(&self, serializer: S) {
105+
let program = self.0;
106+
let span_start = if S::INCLUDE_TS_FIELDS {
107+
if let Some(first_directive) = program.directives.first() {
108+
first_directive.span.start
109+
} else if let Some(first_stmt) = program.body.first() {
110+
first_stmt.span().start
111+
} else {
112+
program.span.start
113+
}
114+
} else {
115+
program.span.start
116+
};
117+
118+
let mut state = serializer.serialize_struct();
119+
state.serialize_field("type", &JsonSafeString("Program"));
120+
state.serialize_field("start", &span_start);
121+
state.serialize_field("end", &program.span.end);
122+
state.serialize_field(
123+
"body",
124+
&AppendToConcat { array: &program.directives, after: &program.body },
125+
);
126+
state.serialize_field("sourceType", &program.source_type.module_kind());
127+
state.serialize_field("hashbang", &program.hashbang);
128+
state.end();
129+
}
130+
}
131+
90132
// --------------------
91133
// Basic types
92134
// --------------------

napi/parser/deserialize-js.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ function deserialize(buffer, sourceTextInput, sourceLenInput) {
3737
function deserializeProgram(pos) {
3838
const body = deserializeVecDirective(pos + 88);
3939
body.push(...deserializeVecStatement(pos + 120));
40-
return {
40+
let start = deserializeU32(pos);
41+
const program = {
4142
type: 'Program',
42-
start: deserializeU32(pos),
43+
start,
4344
end: deserializeU32(pos + 4),
4445
body,
4546
sourceType: deserializeModuleKind(pos + 9),
4647
hashbang: deserializeOptionHashbang(pos + 64),
4748
};
49+
return program;
4850
}
4951

5052
function deserializeIdentifierName(pos) {

napi/parser/deserialize-ts.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,17 @@ function deserialize(buffer, sourceTextInput, sourceLenInput) {
3737
function deserializeProgram(pos) {
3838
const body = deserializeVecDirective(pos + 88);
3939
body.push(...deserializeVecStatement(pos + 120));
40-
return {
40+
let start = deserializeU32(pos);
41+
if (body.length > 0) start = body[0].start;
42+
const program = {
4143
type: 'Program',
42-
start: deserializeU32(pos),
44+
start,
4345
end: deserializeU32(pos + 4),
4446
body,
4547
sourceType: deserializeModuleKind(pos + 9),
4648
hashbang: deserializeOptionHashbang(pos + 64),
4749
};
50+
return program;
4851
}
4952

5053
function deserializeIdentifierName(pos) {

0 commit comments

Comments
 (0)