fix(ast/estree): include comments in TS Program span (#28326)

- Align TS ESTree `Program` spans with `@typescript-eslint/parser` v8.72.0, which includes leading and trailing comments/whitespace.
- Remove the TS-only `Program.start` adjustment from the Rust serializer and raw-transfer deserializers.
- Update affected coverage and oxlint fixture snapshots.
This commit is contained in:
camc314 committed 2026-10-28 19:08:37 +00:00
1 parent 9510eac186
commit ed7df7de92
12 files changed
+63 -4763

No files matched your search

+6 -25
View File
@@ -64,7 +64,8 @@ export function resetBuffer() {
}
function deserializeProgram(pos) {
let end = deserializeI32(pos + 4),
let start = deserializeI32(pos),
end = deserializeI32(pos + 4),
program = (parent = {
__proto__: NodeProto,
type: "Program",
@@ -78,32 +79,12 @@ function deserializeProgram(pos) {
tokens === null && initTokens();
return tokens;
},
start: 0,
start,
end,
range: [0, end],
range: [start, end],
parent: null,
}),
body = (program.body = deserializeVecDirective(pos + 88));
body.push(...deserializeVecStatement(pos + 112));
{
let start;
if (body.length > 0) {
let first = body[0];
start = first.start;
if (first.type === "ExportNamedDeclaration" || first.type === "ExportDefaultDeclaration") {
let { declaration } = first;
if (
declaration !== null
&& declaration.type === "ClassDeclaration"
&& declaration.decorators.length > 0
) {
let decoratorStart = declaration.decorators[0].start;
decoratorStart < start && (start = decoratorStart);
}
}
} else start = end;
program.start = program.range[0] = start;
}
});
(program.body = deserializeVecDirective(pos + 88)).push(...deserializeVecStatement(pos + 112));
parent = null;
return program;
}
+1 -4
View File
@@ -323,8 +323,7 @@ const LOC_DESCRIPTOR: PropertyDescriptor = {
*/
export function computeLoc(start: number, end: number): Location {
// All AST nodes, tokens and comments have `start < end`, with only one exception:
// `Program` node can have `start === end` if it has no directives or statements - either 0-length file,
// or purely comments and/or whitespace and/or hashbang. But `start > end` is impossible.
// `Program` node has `start === end` for a 0-length file. But `start > end` is impossible.
debugAssert(start <= end, "`start` must be <= `end`");
if (lines.length === 0) initLines();
@@ -418,8 +417,6 @@ export function getNodeByRangeIndex(offset: number): ESTreeNode | null {
debugAssertIsNonNull(ast);
// If index is outside of `Program`, return `null`
// TODO: Once `Program`'s span covers the entire file (as per ESLint v10), `index < ast.start` check can be removed
// (or changed to `index < 0` if we want to check for negative indices)
if (offset < ast.start || offset >= ast.end) return null;
// Search for the node containing the index
+4 -4
View File
@@ -53,9 +53,9 @@
x bom-plugin(bom):
| hasBOM: true
| sourceText: "/*x*/'é';/*y*/ऊ=1;debugger;\n// 😀🤪😆😎🤮\ndebugger;\ndebugger;"
| Program span: 5-61
| Program span: 0-61
| comments: [{"start":0,"end":5,"text":"/*x*/"},{"start":9,"end":14,"text":"/*y*/"},{"start":28,"end":41,"text":"// 😀🤪😆😎🤮"}]
,-[files/bom_unicode.js:1:9]
,-[files/bom_unicode.js:1:4]
1 | ,-> /*x*/'é';/*y*/ऊ=1;debugger;
2 | | // 😀🤪😆😎🤮
3 | | debugger;
@@ -134,9 +134,9 @@
x bom-plugin(bom):
| hasBOM: false
| sourceText: "/*x*/'é';/*y*/ऊ=1;debugger;\n// 😀🤪😆😎🤮\ndebugger;\ndebugger;"
| Program span: 5-61
| Program span: 0-61
| comments: [{"start":0,"end":5,"text":"/*x*/"},{"start":9,"end":14,"text":"/*y*/"},{"start":28,"end":41,"text":"// 😀🤪😆😎🤮"}]
,-[files/no_bom_unicode.js:1:6]
,-[files/no_bom_unicode.js:1:1]
1 | ,-> /*x*/'é';/*y*/ऊ=1;debugger;
2 | | // 😀🤪😆😎🤮
3 | | debugger;
+14 -12
View File
@@ -283,6 +283,20 @@
2 | // Line comment after hashbang
`----
x test-comments(test-comments): getAllComments: 3 comments
| [0] Shebang: "/usr/bin/env node" at [0, 19]
| [1] Line: " Line comment after hashbang" at [20, 50]
| [2] Block: " Block comment after hashbang " at [51, 85]
,-[files/hashbang.js:1:1]
1 | ,-> #!/usr/bin/env node
2 | | // Line comment after hashbang
3 | | /* Block comment after hashbang */
4 | | const topLevelVariable1 = 1;
5 | | const topLevelVariable2 = 2;
6 | |
7 | `-> export function topLevelFunction() {}
`----
x test-comments(test-comments): VariableDeclaration(topLevelVariable1):
| getCommentsBefore: 3 comments
| [0] Shebang: "/usr/bin/env node" at [0, 19]
@@ -298,18 +312,6 @@
5 | const topLevelVariable2 = 2;
`----
x test-comments(test-comments): getAllComments: 3 comments
| [0] Shebang: "/usr/bin/env node" at [0, 19]
| [1] Line: " Line comment after hashbang" at [20, 50]
| [2] Block: " Block comment after hashbang " at [51, 85]
,-[files/hashbang.js:4:1]
3 | /* Block comment after hashbang */
4 | ,-> const topLevelVariable1 = 1;
5 | | const topLevelVariable2 = 2;
6 | |
7 | `-> export function topLevelFunction() {}
`----
x test-comments(test-comments): VariableDeclaration(topLevelVariable2):
| getCommentsBefore: 0 comments
| getCommentsInside: 0 comments
+13 -9
View File
@@ -33,9 +33,11 @@
| * TSUnionType:exit: (types: TSStringKeyword, TSNumberKeyword)
| * TSTypeAliasDeclaration:exit: (typeAnnotation: TSUnionType)
| * Program:exit
,-[files/index.ts:4:1]
3 | // All `Identifier`s
4 | ,-> let a = { x: y };
,-[files/index.ts:1:1]
1 | ,-> // @ts-nocheck
2 | |
3 | | // All `Identifier`s
4 | | let a = { x: y };
5 | |
6 | | // No `ParenthesizedExpression`s in AST
7 | | // prettier-ignore
@@ -50,12 +52,14 @@
`----
x estree-check(check): program:
| start/end: [37,281]
| range: [37,281]
| loc: [{"start":{"line":4,"column":0},"end":{"line":16,"column":0}}]
,-[files/index.ts:4:1]
3 | // All `Identifier`s
4 | ,-> let a = { x: y };
| start/end: [0,281]
| range: [0,281]
| loc: [{"start":{"line":1,"column":0},"end":{"line":16,"column":0}}]
,-[files/index.ts:1:1]
1 | ,-> // @ts-nocheck
2 | |
3 | | // All `Identifier`s
4 | | let a = { x: y };
5 | |
6 | | // No `ParenthesizedExpression`s in AST
7 | | // prettier-ignore
@@ -3,14 +3,14 @@
# stdout
```
x getNode-plugin(getNode): type: null
x getNode-plugin(getNode): type: Program
,-[files/index.ts:1:1]
1 | // Comment
: ^
2 |
`----
x getNode-plugin(getNode): type: null
x getNode-plugin(getNode): type: Program
,-[files/index.ts:2:1]
1 | // Comment
2 |
+4 -78
View File
@@ -1,11 +1,8 @@
use std::cmp;
use oxc_ast_macros::ast_meta;
use oxc_estree::{
CompactFixesSerializer, CompactSerializer, Concat2, ESTree, JsonSafeString,
PrettyFixesSerializer, PrettySerializer, Serializer, StructSerializer,
};
use oxc_span::GetSpan;
use crate::ast::*;
@@ -81,19 +78,11 @@ impl Program<'_> {
/// Serializer for `Program`.
///
/// In TS AST, set start span to start of first directive or statement.
/// This is required because unlike Acorn, TS-ESLint excludes whitespace and comments
/// from the `Program` start span.
/// See <https://github.com/oxc-project/oxc/pull/10134> for more info.
///
/// Special case where first statement is an `ExportDeclaration` or `ExportDefaultDeclaration`
/// exporting a class with decorators, where one of the decorators is before `export`.
/// In these cases, the span of the statement starts after the span of the decorators.
/// e.g. `@dec export class C {}` - `ExportDeclaration` span start is 5, `Decorator` span start is 0.
/// `Program` span start is 0 (not 5).
/// Serializes directives and statements together as `body`.
/// The span covers the entire source text, including leading and trailing comments and whitespace.
#[ast_meta]
#[estree(raw_deser = "
const start = IS_TS ? 0 : DESER[i32](POS_OFFSET.span.start),
const start = DESER[i32](POS_OFFSET.span.start),
end = DESER[i32](POS_OFFSET.span.end);
const program = parent = {
@@ -124,32 +113,6 @@ impl Program<'_> {
const body = program.body = DESER[Vec<Directive>](POS_OFFSET.directives);
body.push(...DESER[Vec<Statement>](POS_OFFSET.body));
if (IS_TS) {
let start;
if (body.length > 0) {
const first = body[0];
start = first.start;
if (first.type === 'ExportNamedDeclaration' || first.type === 'ExportDefaultDeclaration') {
const { declaration } = first;
if (
declaration !== null && declaration.type === 'ClassDeclaration'
&& declaration.decorators.length > 0
) {
const decoratorStart = declaration.decorators[0].start;
if (decoratorStart < start) start = decoratorStart;
}
}
} else {
start = end;
}
if (RANGE) {
program.start = program.range[0] = start;
} else {
program.start = start;
}
}
if (PARENT) parent = null;
program
@@ -165,49 +128,12 @@ impl ESTree for ProgramConverter<'_, '_> {
state.serialize_field("body", &Concat2(&program.directives, &program.body));
state.serialize_field("sourceType", &program.source_type.module_kind());
state.serialize_field("hashbang", &program.hashbang);
let span = if state.include_ts_fields() {
Span::new(get_ts_start_span(program), program.span.end)
} else {
program.span
};
state.serialize_span(span);
state.serialize_span(program.span);
state.end();
}
}
fn get_ts_start_span(program: &Program<'_>) -> u32 {
if let Some(first_directive) = program.directives.first() {
return first_directive.span.start;
}
let Some(first_stmt) = program.body.first() else {
// Program contains no statements or directives. Span start = span end.
return program.span.end;
};
let start = first_stmt.span().start;
match first_stmt {
Statement::ExportDeclaration(decl) => {
if let Declaration::ClassDeclaration(class) = &decl.declaration
&& let Some(decorator) = class.decorators.first()
{
return cmp::min(start, decorator.span.start);
}
}
Statement::ExportDefaultDeclaration(decl) => {
if let ExportDefaultDeclarationKind::ClassDeclaration(class) = &decl.declaration
&& let Some(decorator) = class.decorators.first()
{
return cmp::min(start, decorator.span.start);
}
}
_ => {}
}
start
}
/// Serialize `value` field of `Comment`.
///
/// This serializer does not work for JSON serializer, because there's no access to source text
+4 -23
View File
@@ -55,37 +55,18 @@ export function resetBuffer() {
}
function deserializeProgram(pos) {
let end = deserializeI32(pos + 4),
let start = deserializeI32(pos),
end = deserializeI32(pos + 4),
program = {
type: "Program",
body: null,
sourceType: deserializeModuleKind(pos + 137),
hashbang: null,
start: 0,
start,
end,
};
program.hashbang = deserializeOptionHashbang(pos + 56);
let body = (program.body = deserializeVecDirective(pos + 88));
body.push(...deserializeVecStatement(pos + 112));
{
let start;
if (body.length > 0) {
let first = body[0];
start = first.start;
if (first.type === "ExportNamedDeclaration" || first.type === "ExportDefaultDeclaration") {
let { declaration } = first;
if (
declaration !== null
&& declaration.type === "ClassDeclaration"
&& declaration.decorators.length > 0
) {
let decoratorStart = declaration.decorators[0].start;
decoratorStart < start && (start = decoratorStart);
}
}
} else start = end;
program.start = start;
}
(program.body = deserializeVecDirective(pos + 88)).push(...deserializeVecStatement(pos + 112));
return program;
}
@@ -56,38 +56,19 @@ export function resetBuffer() {
}
function deserializeProgram(pos) {
let end = deserializeI32(pos + 4),
let start = deserializeI32(pos),
end = deserializeI32(pos + 4),
program = (parent = {
type: "Program",
body: null,
sourceType: deserializeModuleKind(pos + 137),
hashbang: null,
start: 0,
start,
end,
parent: null,
});
program.hashbang = deserializeOptionHashbang(pos + 56);
let body = (program.body = deserializeVecDirective(pos + 88));
body.push(...deserializeVecStatement(pos + 112));
{
let start;
if (body.length > 0) {
let first = body[0];
start = first.start;
if (first.type === "ExportNamedDeclaration" || first.type === "ExportDefaultDeclaration") {
let { declaration } = first;
if (
declaration !== null
&& declaration.type === "ClassDeclaration"
&& declaration.decorators.length > 0
) {
let decoratorStart = declaration.decorators[0].start;
decoratorStart < start && (start = decoratorStart);
}
}
} else start = end;
program.start = start;
}
(program.body = deserializeVecDirective(pos + 88)).push(...deserializeVecStatement(pos + 112));
parent = null;
return program;
}
@@ -55,38 +55,19 @@ export function resetBuffer() {
}
function deserializeProgram(pos) {
let end = deserializeI32(pos + 4),
let start = deserializeI32(pos),
end = deserializeI32(pos + 4),
program = {
type: "Program",
body: null,
sourceType: deserializeModuleKind(pos + 137),
hashbang: null,
start: 0,
start,
end,
range: [0, end],
range: [start, end],
};
program.hashbang = deserializeOptionHashbang(pos + 56);
let body = (program.body = deserializeVecDirective(pos + 88));
body.push(...deserializeVecStatement(pos + 112));
{
let start;
if (body.length > 0) {
let first = body[0];
start = first.start;
if (first.type === "ExportNamedDeclaration" || first.type === "ExportDefaultDeclaration") {
let { declaration } = first;
if (
declaration !== null
&& declaration.type === "ClassDeclaration"
&& declaration.decorators.length > 0
) {
let decoratorStart = declaration.decorators[0].start;
decoratorStart < start && (start = decoratorStart);
}
}
} else start = end;
program.start = program.range[0] = start;
}
(program.body = deserializeVecDirective(pos + 88)).push(...deserializeVecStatement(pos + 112));
return program;
}
@@ -56,39 +56,20 @@ export function resetBuffer() {
}
function deserializeProgram(pos) {
let end = deserializeI32(pos + 4),
let start = deserializeI32(pos),
end = deserializeI32(pos + 4),
program = (parent = {
type: "Program",
body: null,
sourceType: deserializeModuleKind(pos + 137),
hashbang: null,
start: 0,
start,
end,
range: [0, end],
range: [start, end],
parent: null,
});
program.hashbang = deserializeOptionHashbang(pos + 56);
let body = (program.body = deserializeVecDirective(pos + 88));
body.push(...deserializeVecStatement(pos + 112));
{
let start;
if (body.length > 0) {
let first = body[0];
start = first.start;
if (first.type === "ExportNamedDeclaration" || first.type === "ExportDefaultDeclaration") {
let { declaration } = first;
if (
declaration !== null
&& declaration.type === "ClassDeclaration"
&& declaration.decorators.length > 0
) {
let decoratorStart = declaration.decorators[0].start;
decoratorStart < start && (start = decoratorStart);
}
}
} else start = end;
program.start = program.range[0] = start;
}
(program.body = deserializeVecDirective(pos + 88)).push(...deserializeVecStatement(pos + 112));
parent = null;
return program;
}
File diff suppressed because it is too large. Load diff