Skip to content

Commit 77b6f7e

Browse files
committed
fix(ast/estree): fix start span of Program in TS-ESTree AST where first statement is @dec export class C {} (#10448)
#10438 aligned the spans of `@dec export class C {}` and `@dec export default class {}` with TS-ESLint. ```ts @dec export class C {} ^^^^^^^^^^^^^^^^^ ExportNamedDeclaration ^^^^^^^^^^ Class ^^^^ Decorator @dec export default class {} ^^^^^^^^^^^^^^^^^^^^^^^ ExportDefaultDeclaration ^^^^^^^^ Class ^^^^ Decorator ``` However, this causes a problem where one of these is the first statement in the file. In TS-ESTree, `Program` start is the start of the first token (excluding whitespace and comments). So we need to set `Program` start to the start of the decorator in these cases.
1 parent a6b2232 commit 77b6f7e

File tree

4 files changed

+96
-22
lines changed

4 files changed

+96
-22
lines changed

crates/oxc_ast/src/serialize.rs

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::cmp;
2+
13
use cow_utils::CowUtils;
24

35
use crate::ast::*;
@@ -80,18 +82,47 @@ impl Program<'_> {
8082
/// This is required because unlike Acorn, TS-ESLint excludes whitespace and comments
8183
/// from the `Program` start span.
8284
/// See <https://github.com/oxc-project/oxc/pull/10134> for more info.
85+
///
86+
/// Special case where first statement is an `ExportNamedDeclaration` or `ExportDefaultDeclaration`
87+
/// exporting a class with decorators, where one of the decorators is before `export`.
88+
/// In these cases, the span of the statement starts after the span of the decorators.
89+
/// e.g. `@dec export class C {}` - `ExportNamedDeclaration` span start is 5, `Decorator` span start is 0.
90+
/// `Program` span start is 0 (not 5).
8391
#[ast_meta]
8492
#[estree(raw_deser = "
8593
const body = DESER[Vec<Directive>](POS_OFFSET.directives);
8694
body.push(...DESER[Vec<Statement>](POS_OFFSET.body));
87-
let start = DESER[u32](POS_OFFSET.span.start);
95+
96+
/* IF_JS */
97+
const start = DESER[u32](POS_OFFSET.span.start);
98+
/* END_IF_JS */
99+
100+
const end = DESER[u32](POS_OFFSET.span.end);
101+
88102
/* IF_TS */
89-
if (body.length > 0) start = body[0].start;
103+
let start;
104+
if (body.length > 0) {
105+
const first = body[0];
106+
start = first.start;
107+
if (first.type === 'ExportNamedDeclaration' || first.type === 'ExportDefaultDeclaration') {
108+
const {declaration} = first;
109+
if (
110+
declaration !== null && declaration.type === 'ClassDeclaration'
111+
&& declaration.decorators.length > 0
112+
) {
113+
const decoratorStart = declaration.decorators[0].start;
114+
if (decoratorStart < start) start = decoratorStart;
115+
}
116+
}
117+
} else {
118+
start = end;
119+
}
90120
/* END_IF_TS */
121+
91122
const program = {
92123
type: 'Program',
93124
start,
94-
end: DESER[u32](POS_OFFSET.span.end),
125+
end,
95126
body,
96127
sourceType: DESER[ModuleKind](POS_OFFSET.source_type.module_kind),
97128
hashbang: DESER[Option<Hashbang>](POS_OFFSET.hashbang),
@@ -103,18 +134,8 @@ pub struct ProgramConverter<'a, 'b>(pub &'b Program<'a>);
103134
impl ESTree for ProgramConverter<'_, '_> {
104135
fn serialize<S: Serializer>(&self, serializer: S) {
105136
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-
// If program contains no statements or directives, span start = span end
113-
program.span.end
114-
}
115-
} else {
116-
program.span.start
117-
};
137+
let span_start =
138+
if S::INCLUDE_TS_FIELDS { get_ts_start_span(program) } else { program.span.start };
118139

119140
let mut state = serializer.serialize_struct();
120141
state.serialize_field("type", &JsonSafeString("Program"));
@@ -130,6 +151,39 @@ impl ESTree for ProgramConverter<'_, '_> {
130151
}
131152
}
132153

154+
fn get_ts_start_span(program: &Program<'_>) -> u32 {
155+
if let Some(first_directive) = program.directives.first() {
156+
return first_directive.span.start;
157+
}
158+
159+
let Some(first_stmt) = program.body.first() else {
160+
// Program contains no statements or directives. Span start = span end.
161+
return program.span.end;
162+
};
163+
164+
match first_stmt {
165+
Statement::ExportNamedDeclaration(decl) => {
166+
let start = decl.span.start;
167+
if let Some(Declaration::ClassDeclaration(class)) = &decl.declaration {
168+
if let Some(decorator) = class.decorators.first() {
169+
return cmp::min(start, decorator.span.start);
170+
}
171+
}
172+
start
173+
}
174+
Statement::ExportDefaultDeclaration(decl) => {
175+
let start = decl.span.start;
176+
if let ExportDefaultDeclarationKind::ClassDeclaration(class) = &decl.declaration {
177+
if let Some(decorator) = class.decorators.first() {
178+
return cmp::min(start, decorator.span.start);
179+
}
180+
}
181+
start
182+
}
183+
_ => first_stmt.span().start,
184+
}
185+
}
186+
133187
// --------------------
134188
// Basic types
135189
// --------------------

napi/parser/deserialize-js.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,14 @@ function deserialize(buffer, sourceTextInput, sourceLenInput) {
3737
function deserializeProgram(pos) {
3838
const body = deserializeVecDirective(pos + 88);
3939
body.push(...deserializeVecStatement(pos + 120));
40-
let start = deserializeU32(pos);
40+
41+
const start = deserializeU32(pos);
42+
const end = deserializeU32(pos + 4);
43+
4144
const program = {
4245
type: 'Program',
4346
start,
44-
end: deserializeU32(pos + 4),
47+
end,
4548
body,
4649
sourceType: deserializeModuleKind(pos + 9),
4750
hashbang: deserializeOptionHashbang(pos + 64),

napi/parser/deserialize-ts.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,30 @@ function deserialize(buffer, sourceTextInput, sourceLenInput) {
3737
function deserializeProgram(pos) {
3838
const body = deserializeVecDirective(pos + 88);
3939
body.push(...deserializeVecStatement(pos + 120));
40-
let start = deserializeU32(pos);
41-
if (body.length > 0) start = body[0].start;
40+
41+
const end = deserializeU32(pos + 4);
42+
43+
let start;
44+
if (body.length > 0) {
45+
const first = body[0];
46+
start = first.start;
47+
if (first.type === 'ExportNamedDeclaration' || first.type === 'ExportDefaultDeclaration') {
48+
const { declaration } = first;
49+
if (
50+
declaration !== null && declaration.type === 'ClassDeclaration' &&
51+
declaration.decorators.length > 0
52+
) {
53+
const decoratorStart = declaration.decorators[0].start;
54+
if (decoratorStart < start) start = decoratorStart;
55+
}
56+
}
57+
} else {
58+
start = end;
59+
}
4260
const program = {
4361
type: 'Program',
4462
start,
45-
end: deserializeU32(pos + 4),
63+
end,
4664
body,
4765
sourceType: deserializeModuleKind(pos + 9),
4866
hashbang: deserializeOptionHashbang(pos + 64),

tasks/coverage/snapshots/estree_typescript.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ commit: 15392346
22

33
estree_typescript Summary:
44
AST Parsed : 10619/10725 (99.01%)
5-
Positive Passed: 9012/10725 (84.03%)
5+
Positive Passed: 9013/10725 (84.04%)
66
Expect to Parse: tasks/coverage/typescript/tests/cases/compiler/ClassDeclarationWithInvalidConstOnPropertyDeclaration.ts
77
A class member cannot have the 'const' keyword.
88
Mismatch: tasks/coverage/typescript/tests/cases/compiler/accessOverriddenBaseClassMember1.ts
@@ -1643,7 +1643,6 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/es7/trailingC
16431643
Unexpected trailing comma after rest element
16441644
Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/es7/trailingCommasInFunctionParametersAndArguments.ts
16451645
A rest parameter must be last in a parameter list
1646-
Mismatch: tasks/coverage/typescript/tests/cases/conformance/esDecorators/classDeclaration/esDecorators-classDeclaration-commentPreservation.ts
16471646
Mismatch: tasks/coverage/typescript/tests/cases/conformance/esDecorators/classDeclaration/esDecorators-classDeclaration-parameterProperties.ts
16481647
Mismatch: tasks/coverage/typescript/tests/cases/conformance/esDecorators/classExpression/esDecorators-classExpression-missingEmitHelpers-classDecorator.17.ts
16491648
Mismatch: tasks/coverage/typescript/tests/cases/conformance/esDecorators/classExpression/esDecorators-classExpression-missingEmitHelpers-classDecorator.5.ts

0 commit comments

Comments
 (0)