fix(parser): validate TypeScript import type options (#18889)

## Summary

- Add validation for TypeScript import type options (`typeof import("...", { with: {...} })`)
- Require `with` or `assert` keyword as identifier (not string, not escaped)
- Reject computed property keys in attributes object
- Reject spread elements in attributes object

This fixes 4 Babel conformance tests:
- `invalid-import-type-options-escaped-with`
- `invalid-import-type-options-string-with`
- `invalid-import-type-options-with-computed-properties`
- `invalid-import-type-options-with-spread-element`

**parser_babel Negative Passed**: 1655/1689 → 1659/1689 (+4)

🤖 Generated with [Claude Code](https://claude.ai/code)
This commit is contained in:
Boshen committed 2026-02-03 14:47:05 +00:00
1 parent f609cb6819
commit 1f6b1931c2
5 files changed
+285 -32

No files matched your search

+16
View File
@@ -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)
}
+141 -1
View File
@@ -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 };
@@ -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 =
+31 -9
View File
@@ -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
@@ -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.
@@ -14204,6 +14196,24 @@ 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" } }, )
@@ -14211,6 +14221,18 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
· ╰── `)` 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", )
+32
View File
@@ -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 =
+65 -22
View File
@@ -9189,12 +9189,11 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va
· ─
╰────
× Expected `:` but found `,`
╭─[typescript/tests/cases/compiler/parseAssertEntriesError.ts:2:36]
× Unexpected token
╭─[typescript/tests/cases/compiler/parseAssertEntriesError.ts:2:32]
1 │ export type LocalInterface =
2 │ & import("pkg", { assert: {1234, "resolution-mode": "require"} }).RequireInterface
· ┬
· ╰── `:` expected
· ────
3 │ & import("pkg", { assert: {1234, "resolution-mode": "import"} }).ImportInterface;
╰────
@@ -9236,12 +9235,11 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va
3 │ }
╰────
× Expected `:` but found `,`
╭─[typescript/tests/cases/compiler/parseImportAttributesError.ts:2:34]
× Unexpected token
╭─[typescript/tests/cases/compiler/parseImportAttributesError.ts:2:30]
1 │ export type LocalInterface =
2 │ & import("pkg", { with: {1234, "resolution-mode": "require"} }).RequireInterface
· ┬
· ╰── `:` expected
· ────
3 │ & import("pkg", { with: {1234, "resolution-mode": "import"} }).ImportInterface;
╰────
@@ -20199,11 +20197,12 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va
9 │ const g = import('./0', {}, {})
╰────
× Unexpected token
× Expected `}` but found `,`
╭─[typescript/tests/cases/conformance/importAttributes/importAttributes10.ts:22:5]
21 │ type: "json"
22 │ },,
· ─
· ┬
· ╰── `}` expected
23 │ });
╰────
@@ -21116,22 +21115,66 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va
╰────
help: TypeScript transforms 'import ... =' to 'const ... ='
× Expected `{` but found `[`
╭─[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:3:21]
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:3:22]
2 │ export type LocalInterface =
3 │ & import("pkg", [ {"resolution-mode": "require"} ]).RequireInterface
· ┬
· ╰── `{` expected
4 │ & import("pkg", [ {"resolution-mode": "import"} ]).ImportInterface;
3 │ & import("pkg", {"resolution-mode": "require"}).RequireInterface
· ─────────────────
4 │ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
╰────
× Expected `{` but found `[`
╭─[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:3:21]
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:4:22]
3 │ & import("pkg", {"resolution-mode": "require"}).RequireInterface
4 │ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
· ─────────────────
5 │
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:6:49]
5 │
6 │ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
· ─────────────────
7 │ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:7:49]
6 │ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
7 │ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
· ─────────────────
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:3:22]
2 │ export type LocalInterface =
3 │ & import("pkg", [ {"resolution-mode": "require"} ]).RequireInterface
· ┬
· ╰── `{` expected
4 │ & import("pkg", [ {"resolution-mode": "import"} ]).ImportInterface;
3 │ & import("pkg", {"resolution-mode": "require"}).RequireInterface
· ─────────────────
4 │ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:4:22]
3 │ & import("pkg", {"resolution-mode": "require"}).RequireInterface
4 │ & import("pkg", {"resolution-mode": "import"}).ImportInterface;
· ─────────────────
5 │
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:6:49]
5 │
6 │ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
· ─────────────────
7 │ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
╰────
× Expected 'with' in import type options
╭─[typescript/tests/cases/conformance/node/nodeModulesImportTypeModeDeclarationEmitErrors1.ts:7:49]
6 │ export const a = (null as any as import("pkg", {"resolution-mode": "require"}).RequireInterface);
7 │ export const b = (null as any as import("pkg", {"resolution-mode": "import"}).ImportInterface);
· ─────────────────
╰────
× TS(1030): 'override' modifier already seen.