mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
5 files changed
+285
-32
No files matched your search
@@ -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)
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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", )
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user