Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/oxc_parser/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1268,3 +1268,19 @@ pub fn only_default_import_allowed_in_source_phase(span: Span) -> OxcDiagnostic
OxcDiagnostic::error("Only a single default import is allowed in a source phase import.")
.with_label(span)
}

#[cold]
pub fn ts_import_type_options_expected_with(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Expected 'with' in import type options").with_label(span)
}

#[cold]
pub fn ts_import_type_options_invalid_key(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Import attributes keys must be identifier or string literal.")
.with_label(span)
}

#[cold]
pub fn ts_import_type_options_no_spread(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Spread elements are not allowed in import type options.").with_label(span)
}
142 changes: 141 additions & 1 deletion crates/oxc_parser/src/ts/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,7 +1075,7 @@ impl<'a> ParserImpl<'a> {
};

let options =
if self.eat(Kind::Comma) { Some(self.parse_object_expression()) } else { None };
if self.eat(Kind::Comma) { Some(self.parse_ts_import_type_options()) } else { None };
self.expect(Kind::RParen);
let qualifier =
if self.eat(Kind::Dot) { Some(self.parse_ts_import_type_qualifier()) } else { None };
Expand Down Expand Up @@ -1103,6 +1103,146 @@ impl<'a> ParserImpl<'a> {
left
}

/// Parse TypeScript import type options: `{ with: { type: "json" } }` or `{ assert: { ... } }`
///
/// The options must have a property with key `with` or `assert` (as identifier, not string).
/// If the value is an object literal, it must have only static key-value pairs
/// (no computed keys, no spread elements).
fn parse_ts_import_type_options(&mut self) -> Box<'a, ObjectExpression<'a>> {
let span = self.start_span();
self.expect(Kind::LCurly);

// Expect `with` or `assert` as identifier (not string, not escaped)
// TypeScript supports both: `with` is the current standard, `assert` is the older syntax
let key_span = self.cur_token().span();
let is_with = self.at(Kind::With);
let is_assert = self.at(Kind::Assert);
if (!is_with && !is_assert) || self.cur_token().escaped() {
self.error(diagnostics::ts_import_type_options_expected_with(key_span));
}
// Use the actual string from the source (not a static string) to ensure it's in the arena
let key_name = self.cur_string();
let with_key_span = self.start_span();
self.bump_any();
let with_key = self.ast.identifier_name(self.end_span(with_key_span), key_name);

self.expect(Kind::Colon);

// Parse the value - if it's an object literal, validate it
let value = if self.at(Kind::LCurly) {
let inner_object = self.parse_ts_import_type_attributes();
Expression::ObjectExpression(self.alloc(inner_object))
} else {
// Allow any expression (e.g., super.foo)
self.parse_assignment_expression_or_higher()
};

// Create the outer `with: { ... }` property
let with_property = self.ast.alloc_object_property(
self.end_span(with_key_span),
PropertyKind::Init,
PropertyKey::StaticIdentifier(self.alloc(with_key)),
value,
false,
false,
false,
);

let outer_properties = self.ast.vec1(ObjectPropertyKind::ObjectProperty(with_property));

// Allow optional trailing comma: `{ with: { type: "json" }, }`
let _ = self.eat(Kind::Comma);

self.expect(Kind::RCurly);
self.ast.alloc_object_expression(self.end_span(span), outer_properties)
}

/// Parse TypeScript import type attributes object: `{ type: "json" }`
/// Only allows static key-value pairs (no computed keys, no spread elements).
fn parse_ts_import_type_attributes(&mut self) -> ObjectExpression<'a> {
let span = self.start_span();
self.expect(Kind::LCurly);

let mut properties = self.ast.vec();
let mut first = true;
while !self.at(Kind::RCurly) && !self.at(Kind::Eof) {
if first {
first = false;
} else {
self.expect(Kind::Comma);
if self.at(Kind::RCurly) {
break;
}
}

// Check for spread element
if self.at(Kind::Dot3) {
let spread_span = self.cur_token().span();
self.error(diagnostics::ts_import_type_options_no_spread(spread_span));
// Skip the spread and parse the expression to recover
self.bump_any();
self.parse_assignment_expression_or_higher();
continue;
}

let prop_span = self.start_span();

// Check for computed property
if self.at(Kind::LBrack) {
let bracket_span = self.cur_token().span();
self.error(diagnostics::ts_import_type_options_invalid_key(bracket_span));
// Parse as computed to recover
self.bump_any();
self.parse_assignment_expression_or_higher();
self.expect(Kind::RBrack);
self.expect(Kind::Colon);
let value = self.parse_assignment_expression_or_higher();
let key = PropertyKey::StringLiteral(self.alloc(self.ast.string_literal(
bracket_span,
"",
None,
)));
properties.push(ObjectPropertyKind::ObjectProperty(
self.ast.alloc_object_property(
self.end_span(prop_span),
PropertyKind::Init,
key,
value,
false,
false,
true, // computed
),
));
continue;
}

// Parse identifier or string key
let key = if self.at(Kind::Str) {
let string_literal = self.parse_literal_string();
PropertyKey::StringLiteral(self.alloc(string_literal))
} else {
let ident = self.parse_identifier_name();
PropertyKey::StaticIdentifier(self.alloc(ident))
};

self.expect(Kind::Colon);
let value = self.parse_assignment_expression_or_higher();

properties.push(ObjectPropertyKind::ObjectProperty(self.ast.alloc_object_property(
self.end_span(prop_span),
PropertyKind::Init,
key,
value,
false,
false,
false,
)));
}

self.expect(Kind::RCurly);
self.ast.object_expression(self.end_span(span), properties)
}

fn try_parse_constraint_of_infer_type(&mut self) -> Option<TSType<'a>> {
if self.eat(Kind::Extends) {
let constraint =
Expand Down
40 changes: 31 additions & 9 deletions tasks/coverage/snapshots/parser_babel.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ commit: 92c052dc
parser_babel Summary:
AST Parsed : 2224/2224 (100.00%)
Positive Passed: 2211/2224 (99.42%)
Negative Passed: 1656/1689 (98.05%)
Negative Passed: 1660/1689 (98.28%)
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2026/explicit-resource-management/invalid-for-using-of-no-initializer/input.js

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/cast/unparenthesized-assert-and-assign/input.ts
Expand Down Expand Up @@ -62,14 +62,6 @@ Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/ty

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/const-type-parameters-invalid/input.ts

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-string-with/input.ts

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-computed-properties/input.ts

Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-spread-element/input.ts

Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/estree/class-private-method/typescript-invalid-abstract/input.ts

Γ— TS(18019): 'abstract' modifier cannot be used with a private identifier.
Expand Down Expand Up @@ -14204,13 +14196,43 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
Β· ╰── `{` expected
╰────

Γ— Expected 'with' in import type options
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts:1:36]
1 β”‚ let x: typeof import("foo.json", { w\u0069th: { type: "json" } });
Β· ─────────
╰────

Γ— Keywords cannot contain escape characters
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-escaped-with/input.ts:1:36]
1 β”‚ let x: typeof import("foo.json", { w\u0069th: { type: "json" } });
Β· ─────────
╰────

Γ— Expected 'with' in import type options
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-string-with/input.ts:1:36]
1 β”‚ let x: typeof import("foo.json", { "with": { type: "json" } });
Β· ──────
╰────

Γ— Expected `)` but found `,`
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-trailing-comma/input.ts:1:60]
1 β”‚ let x: typeof import("foo.json", { with: { type: "json" } }, )
Β· ┬
Β· ╰── `)` expected
╰────

Γ— Import attributes keys must be identifier or string literal.
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-computed-properties/input.ts:1:44]
1 β”‚ let x: typeof import("foo.json", { with: { ["type"]: "json" } });
Β· ─
╰────

Γ— Spread elements are not allowed in import type options.
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-options-with-spread-element/input.ts:1:58]
1 β”‚ let x: typeof import("foo.json", { with: { type: "json", ...attributes } });
Β· ───
╰────

Γ— Expected `{` but found `)`
╭─[babel/packages/babel-parser/test/fixtures/typescript/types/invalid-import-type-trailing-comma/input.ts:1:34]
1 β”‚ let x: typeof import("foo.json", )
Expand Down
32 changes: 32 additions & 0 deletions tasks/coverage/snapshots/parser_misc.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3534,6 +3534,38 @@ Negative Passed: 134/134 (100.00%)
Β· ─────────
╰────

Γ— Expected 'with' in import type options
╭─[misc/fail/oxc-2394.ts:20:22]
19 β”‚ export type LocalInterface =
20 β”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface
Β· ─────────────────
21 β”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
╰────

Γ— Expected 'with' in import type options
╭─[misc/fail/oxc-2394.ts:21:22]
20 β”‚ & import("pkg", {"resolution-mode": "require"}).RequireInterface
21 β”‚ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
Β· ─────────────────
22 β”‚
╰────

Γ— Expected 'with' in import type options
╭─[misc/fail/oxc-2394.ts:23:49]
22 β”‚
23 β”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
Β· ─────────────────
24 β”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
╰────

Γ— Expected 'with' in import type options
╭─[misc/fail/oxc-2394.ts:24:49]
23 β”‚ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
24 β”‚ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
Β· ─────────────────
25 β”‚
╰────

Γ— Expected `{` but found `[`
╭─[misc/fail/oxc-2394.ts:38:21]
37 β”‚ export type LocalInterface =
Expand Down
Loading
Loading