From 6870b6470bb1863902924172065688b303fd8efa Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:58:35 +0000 Subject: [PATCH] feat(parser): add TS1363 error code (#17609) TS Playground: https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBDAnmApnAYhCARFAzAQwFcAbGAGjgG84AhAqS+gLzgF848oIQ4AiEBAAmpFHwDcQA found in `tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/types/import-type-declaration-error/input.ts` The span is not correct, but I think this is still better then nothing :) The span should be only on the specifiers, not on the complete import declaration. --- .../src/rules/import/no_empty_named_blocks.rs | 3 +- .../typescript/no_import_type_side_effects.rs | 2 +- .../unicorn/require_module_specifiers.rs | 2 +- crates/oxc_parser/src/diagnostics.rs | 10 +++ crates/oxc_parser/src/js/module.rs | 11 --- crates/oxc_parser/src/module_record.rs | 84 +++++++++++++------ tasks/coverage/snapshots/parser_babel.snap | 10 ++- .../coverage/snapshots/parser_typescript.snap | 10 +-- .../allocs_parser.snap | 2 +- 9 files changed, 86 insertions(+), 48 deletions(-) diff --git a/crates/oxc_linter/src/rules/import/no_empty_named_blocks.rs b/crates/oxc_linter/src/rules/import/no_empty_named_blocks.rs index 4c873b2ac4f12..851b6dde80be3 100644 --- a/crates/oxc_linter/src/rules/import/no_empty_named_blocks.rs +++ b/crates/oxc_linter/src/rules/import/no_empty_named_blocks.rs @@ -98,7 +98,8 @@ fn test() { "import Default, { mod } from 'mod'", "import { Named } from 'mod'", "import type { Named } from 'mod'", - "import type Default, { Named } from 'mod'", + // "import type Default, { Named } from 'mod'", ts error 1363 + "import type Default from 'mod'", "import type * as Namespace from 'mod'", "import * as Namespace from 'mod'", r#" diff --git a/crates/oxc_linter/src/rules/typescript/no_import_type_side_effects.rs b/crates/oxc_linter/src/rules/typescript/no_import_type_side_effects.rs index 606b73fbe2c76..1d2a8b2480ffc 100644 --- a/crates/oxc_linter/src/rules/typescript/no_import_type_side_effects.rs +++ b/crates/oxc_linter/src/rules/typescript/no_import_type_side_effects.rs @@ -148,7 +148,7 @@ fn test() { "import { type T, U } from 'mod';", "import { T, type U } from 'mod';", "import type T from 'mod';", - "import type T, { U } from 'mod';", + // "import type T, { U } from 'mod';", ts error 1363 "import T, { type U } from 'mod';", "import type * as T from 'mod';", "import 'mod';", diff --git a/crates/oxc_linter/src/rules/unicorn/require_module_specifiers.rs b/crates/oxc_linter/src/rules/unicorn/require_module_specifiers.rs index b48cc250fc718..32392607f7a90 100644 --- a/crates/oxc_linter/src/rules/unicorn/require_module_specifiers.rs +++ b/crates/oxc_linter/src/rules/unicorn/require_module_specifiers.rs @@ -161,7 +161,7 @@ fn test() { r#"import {foo} from "foo""#, r#"import foo,{bar} from "foo""#, r#"import type foo from "foo""#, - r#"import type foo,{bar} from "foo""#, + // r#"import type foo,{bar} from "foo""#, ts error 1363 r#"import foo,{type bar} from "foo""#, "const foo = 1; export {foo};", diff --git a/crates/oxc_parser/src/diagnostics.rs b/crates/oxc_parser/src/diagnostics.rs index a9970d7f3306a..d9cab916b3348 100644 --- a/crates/oxc_parser/src/diagnostics.rs +++ b/crates/oxc_parser/src/diagnostics.rs @@ -343,6 +343,16 @@ pub fn implements_clause_already_seen(span: Span, seen_span: Span) -> OxcDiagnos .with_help("Merge the two 'implements' clauses into one by a ','") } +// A type-only import can specify a default import or named bindings, but not both. ts(1363) +#[cold] +pub fn type_only_import_default_and_named(specifier_span: Span) -> OxcDiagnostic { + ts_error( + "1363", + "A type-only import can specify a default import or named bindings, but not both.", + ) + .with_label(specifier_span) +} + #[cold] pub fn binding_rest_element_last(span: Span) -> OxcDiagnostic { OxcDiagnostic::error("A rest element must be last in a destructuring pattern").with_label(span) diff --git a/crates/oxc_parser/src/js/module.rs b/crates/oxc_parser/src/js/module.rs index 9c44152d74294..1d88e05d609d0 100644 --- a/crates/oxc_parser/src/js/module.rs +++ b/crates/oxc_parser/src/js/module.rs @@ -1274,17 +1274,6 @@ mod test { assert_eq!(specifiers[0].name(), "defer"); }); - let src = "import type foo, { bar } from 'bar';"; - parse_and_assert_import_declarations(src, |declarations| { - assert_eq!(declarations.len(), 1); - let decl = declarations[0]; - assert_eq!(decl.import_kind, ImportOrExportKind::Type); - let specifiers = decl.specifiers.as_ref().unwrap(); - assert_eq!(specifiers.len(), 2); - assert_eq!(specifiers[0].name(), "foo"); - assert_eq!(specifiers[1].name(), "bar"); - }); - let src = "import foo = bar"; parse_and_assert_statements(src, |statements| { if let Statement::TSImportEqualsDeclaration(decl) = statements[0] { diff --git a/crates/oxc_parser/src/module_record.rs b/crates/oxc_parser/src/module_record.rs index b08ac53f1e48b..d9b02094a5fa4 100644 --- a/crates/oxc_parser/src/module_record.rs +++ b/crates/oxc_parser/src/module_record.rs @@ -37,35 +37,71 @@ impl<'a> ModuleRecordBuilder<'a> { pub fn errors(&self) -> std::vec::Vec { let mut errors = vec![]; + let module_record = &self.module_record; + // Skip checking for exports in TypeScript if self.source_type.is_typescript() { - return errors; - } - - let module_record = &self.module_record; + // TS1363: A type-only import can specify a default import or named bindings, but not both. + // Group import entries by statement and check only those statements that are type-only imports. + if !module_record.import_entries.is_empty() { + // Build map of type-only import statement spans -> (has_default, has_named). + // `requested_modules` contains entries for both imports and exports, so filter is_import && is_type. + let mut seen: rustc_hash::FxHashMap = + rustc_hash::FxHashMap::default(); + for requests in module_record.requested_modules.values() { + for req in requests { + if req.is_import && req.is_type { + seen.entry(req.statement_span).or_insert((false, false)); + } + } + } + if !seen.is_empty() { + for entry in &module_record.import_entries { + if let Some(lookup) = seen.get_mut(&entry.statement_span) { + match &entry.import_name { + ImportImportName::Default(_) => lookup.0 = true, + ImportImportName::Name(_) | ImportImportName::NamespaceObject => { + lookup.1 = true; + } + } + } + } + for (stmt_span, (has_default, has_named)) in seen { + if has_default && has_named { + errors.push(diagnostics::type_only_import_default_and_named(stmt_span)); + } + } + } + } + } else { + // It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. + for name_span in &self.exported_bindings_duplicated { + let old_span = module_record.exported_bindings[&name_span.name]; + errors.push(diagnostics::duplicate_export( + &name_span.name, + name_span.span, + old_span, + )); + } - // It is a Syntax Error if the ExportedNames of ModuleItemList contains any duplicate entries. - for name_span in &self.exported_bindings_duplicated { - let old_span = module_record.exported_bindings[&name_span.name]; - errors.push(diagnostics::duplicate_export(&name_span.name, name_span.span, old_span)); + // Multiple default exports + // `export default foo` + // `export { default }` + let default_exports = module_record + .local_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()) + .chain( + module_record + .indirect_export_entries + .iter() + .filter_map(|export_entry| export_entry.export_name.default_export_span()), + ); + if default_exports.clone().count() > 1 { + errors.push(diagnostics::duplicate_default_export(default_exports)); + } } - // Multiple default exports - // `export default foo` - // `export { default }` - let default_exports = module_record - .local_export_entries - .iter() - .filter_map(|export_entry| export_entry.export_name.default_export_span()) - .chain( - module_record - .indirect_export_entries - .iter() - .filter_map(|export_entry| export_entry.export_name.default_export_span()), - ); - if default_exports.clone().count() > 1 { - errors.push(diagnostics::duplicate_default_export(default_exports)); - } errors } diff --git a/tasks/coverage/snapshots/parser_babel.snap b/tasks/coverage/snapshots/parser_babel.snap index 91c927a44227b..160eeea7a3ed4 100644 --- a/tasks/coverage/snapshots/parser_babel.snap +++ b/tasks/coverage/snapshots/parser_babel.snap @@ -3,7 +3,7 @@ commit: 761c2509 parser_babel Summary: AST Parsed : 2223/2235 (99.46%) Positive Passed: 2203/2235 (98.57%) -Negative Passed: 1646/1696 (97.05%) +Negative Passed: 1647/1696 (97.11%) Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2022/private-in/invalid-private-followed-by-in-2/input.js Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2026/async-explicit-resource-management/invalid-script-top-level-using-binding/input.js @@ -90,8 +90,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/import-type-declaration-error/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 @@ -14290,6 +14288,12 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc · ───── ╰──── + × TS(1363): A type-only import can specify a default import or named bindings, but not both. + ╭─[babel/packages/babel-parser/test/fixtures/typescript/types/import-type-declaration-error/input.ts:1:1] + 1 │ import type FooDefault, { Bar, Baz } from "module"; + · ─────────────────────────────────────────────────── + ╰──── + × TS(1141): String literal expected. ╭─[babel/packages/babel-parser/test/fixtures/typescript/types/import-type-dynamic-errors/input.ts:1:17] 1 │ type X = import(3); diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index 70561cab6bcda..25314c0655d8b 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -19906,12 +19906,10 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/parser/ecmasc · ╰── Opened here ╰──── - × Expected `from` but found `Identifier` - ╭─[typescript/tests/cases/conformance/externalModules/typeOnly/grammarErrors.ts:1:13] - 1 │ import type A from './a'; - · ┬ - · ╰── `from` expected - 2 │ export type { A }; + × TS(1363): A type-only import can specify a default import or named bindings, but not both. + ╭─[typescript/tests/cases/conformance/externalModules/typeOnly/grammarErrors.ts:1:1] + 1 │ import type A, { B, C } from './a'; + · ─────────────────────────────────── ╰──── × Expected `,` or `}` but found `as` diff --git a/tasks/track_memory_allocations/allocs_parser.snap b/tasks/track_memory_allocations/allocs_parser.snap index 4b031738a5bfa..26bf7a4fc795f 100644 --- a/tasks/track_memory_allocations/allocs_parser.snap +++ b/tasks/track_memory_allocations/allocs_parser.snap @@ -2,7 +2,7 @@ File | File size || Sys allocs | Sys reallocs | ------------------------------------------------------------------------------------------------------------------------------------------- checker.ts | 2.92 MB || 9672 | 21 || 267681 | 22847 -cal.com.tsx | 1.06 MB || 1083 | 49 || 138162 | 13699 +cal.com.tsx | 1.06 MB || 1091 | 49 || 138162 | 13699 RadixUIAdoptionSection.jsx | 2.52 kB || 1 | 0 || 365 | 66