diff --git a/crates/oxc_linter/fixtures/import/extensionless_default_import/default_target.js b/crates/oxc_linter/fixtures/import/extensionless_default_import/default_target.js new file mode 100644 index 0000000000000..8960cfcc08eab --- /dev/null +++ b/crates/oxc_linter/fixtures/import/extensionless_default_import/default_target.js @@ -0,0 +1,3 @@ +export default function defaultTarget() { + return 'ok'; +} diff --git a/crates/oxc_linter/src/rules/import/named.rs b/crates/oxc_linter/src/rules/import/named.rs index d9f55c69fae98..71b97b337be47 100644 --- a/crates/oxc_linter/src/rules/import/named.rs +++ b/crates/oxc_linter/src/rules/import/named.rs @@ -4,7 +4,7 @@ use oxc_span::Span; use crate::{ context::LintContext, - module_record::{ExportImportName, ImportImportName}, + module_record::{ExportEntry, ExportImportName, ImportImportName, ModuleRecord, NameSpan}, rule::Rule, }; @@ -14,6 +14,37 @@ fn named_diagnostic(imported_name: &str, module_name: &str, span: Span) -> OxcDi .with_label(span) } +fn has_default_export(module_record: &ModuleRecord) -> bool { + module_record.export_default.is_some() + || module_record.exported_bindings.contains_key("default") +} + +fn is_synthesized_indirect_export_entry(export_entry: &ExportEntry) -> bool { + !export_entry.statement_span.contains_inclusive(export_entry.span) +} + +fn is_reexport_of_default_import( + module_record: &ModuleRecord, + export_entry: &ExportEntry, + import_name: &NameSpan, + remote_module_record: &ModuleRecord, +) -> bool { + if !is_synthesized_indirect_export_entry(export_entry) { + return false; + } + + let Some(module_request) = &export_entry.module_request else { + return false; + }; + + has_default_export(remote_module_record) + && module_record.import_entries.iter().any(|entry| { + entry.import_name.is_default() + && entry.module_request.name() == module_request.name() + && entry.local_name.name() == import_name.name() + }) +} + // #[derive(Debug, Default, Clone)] pub struct Named; @@ -144,7 +175,15 @@ impl Rule for Named { // Check remote bindings let name = import_name.name(); // `export { default as foo } from './source'` <> `export default xxx` - if name == "default" && remote_module_record.export_default.is_some() { + if name == "default" && has_default_export(&remote_module_record) { + continue; + } + if is_reexport_of_default_import( + module_record, + export_entry, + import_name, + &remote_module_record, + ) { continue; } if remote_module_record.exported_bindings.contains_key(name) { @@ -273,3 +312,33 @@ fn test() { .with_import_plugin(true) .test_and_snapshot(); } + +#[test] +fn regression_extensionless_default_import_barrel() { + use crate::tester::Tester; + + let pass = + vec![("import defaultTarget from './default_target';\nexport { defaultTarget };", None)]; + + Tester::new(Named::NAME, Named::PLUGIN, pass, vec![]) + .change_rule_path("extensionless_default_import/index.js") + .with_import_plugin(true) + .intentionally_allow_no_fix_tests() + .test(); +} + +#[test] +fn regression_named_reexport_is_not_default_import_barrel() { + use crate::tester::Tester; + + let fail = vec![( + "import defaultTarget from './default_target';\nexport { defaultTarget } from './default_target';", + None, + )]; + + Tester::new(Named::NAME, Named::PLUGIN, vec![], fail) + .change_rule_path("extensionless_default_import/index.js") + .with_import_plugin(true) + .intentionally_allow_no_fix_tests() + .test(); +}