diff --git a/crates/oxc_linter/src/rules/import/export.rs b/crates/oxc_linter/src/rules/import/export.rs index f7886628f..da72b4408 100644 --- a/crates/oxc_linter/src/rules/import/export.rs +++ b/crates/oxc_linter/src/rules/import/export.rs @@ -1,8 +1,9 @@ use std::path::PathBuf; use oxc_diagnostics::{ - miette::{self, Diagnostic}, + miette::{self, miette, Diagnostic, LabeledSpan}, thiserror::{self, Error}, + Severity, }; use oxc_macros::declare_oxc_lint; use oxc_semantic::ModuleRecord; @@ -13,9 +14,6 @@ use crate::{context::LintContext, rule::Rule}; #[derive(Debug, Error, Diagnostic)] enum ExportDiagnostic { - #[error("eslint-plugin-import(export): Multiple exports of name '{1}'.")] - #[diagnostic(severity(warning))] - MultipleNamedExport(#[label] Span, CompactStr), #[error("eslint-plugin-import(export): No named exports found in module '{1}'")] #[diagnostic(severity(warning))] NoNamedExport(#[label] Span, CompactStr), @@ -43,55 +41,79 @@ impl Rule for Export { fn run_once(&self, ctx: &LintContext<'_>) { let module_record = ctx.semantic().module_record(); let named_export = &module_record.exported_bindings; - let mut duplicated_named_export = FxHashMap::default(); - for export_entry in &module_record.star_export_entries { - let Some(module_request) = &export_entry.module_request else { - continue; - }; + + let mut all_export_names = FxHashMap::default(); + let mut visited = FxHashSet::default(); + + module_record.star_export_entries.iter().for_each(|star_export_entry| { + let mut export_names = FxHashSet::default(); + + let Some(module_request) = &star_export_entry.module_request else { return }; let Some(remote_module_record_ref) = module_record.loaded_modules.get(module_request.name()) else { - continue; + return; }; - let remote_module_record = remote_module_record_ref.value(); - let mut all_export_names = FxHashSet::default(); - let mut visited = FxHashSet::default(); - walk_exported_recursive(remote_module_record, &mut all_export_names, &mut visited); - if all_export_names.is_empty() { + walk_exported_recursive( + remote_module_record_ref.value(), + &mut export_names, + &mut visited, + ); + + if export_names.is_empty() { ctx.diagnostic(ExportDiagnostic::NoNamedExport( module_request.span(), module_request.name().clone(), )); - continue; + } else { + all_export_names.insert(star_export_entry.span, export_names); } + }); - for name in &all_export_names { - if let Some(span) = named_export.get(name) { - duplicated_named_export.entry(*span).or_insert_with(|| name.clone()); + for (name, span) in named_export { + let mut spans = all_export_names + .iter() + .filter_map(|(star_export_entry_span, export_names)| { + if export_names.contains(name) { + Some(*star_export_entry_span) + } else { + None + } + }) + .collect::>(); + + for name_span in &module_record.exported_bindings_duplicated { + if name == name_span.name() { + spans.push(name_span.span()); } } - } - for (span, name) in duplicated_named_export { - ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(span, name)); - } + if !spans.is_empty() { + spans.push(*span); + let labels = spans.into_iter().map(LabeledSpan::underline).collect::>(); - for name_span in &module_record.exported_bindings_duplicated { - let name = name_span.name().clone(); - if let Some(span) = module_record.exported_bindings.get(&name) { - ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(*span, name.clone())); + ctx.diagnostic(miette!( + severity = Severity::Warning, + labels = labels, + "eslint-plugin-import(export): Multiple exports of name '{name}'." + )); } - ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(name_span.span(), name)); } + if !module_record.export_default_duplicated.is_empty() { + let mut spans = module_record.export_default_duplicated.clone(); if let Some(span) = module_record.export_default { - ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(span, "default".into())); + spans.push(span); + let labels = spans.into_iter().map(LabeledSpan::underline).collect::>(); + + ctx.diagnostic(miette!( + severity = Severity::Warning, + labels = labels, + "eslint-plugin-import(export): Multiple default exports." + )); } } - for span in &module_record.export_default_duplicated { - ctx.diagnostic(ExportDiagnostic::MultipleNamedExport(*span, "default".into())); - } } } diff --git a/crates/oxc_linter/src/snapshots/export.snap b/crates/oxc_linter/src/snapshots/export.snap index aa6e2b873..92f04810a 100644 --- a/crates/oxc_linter/src/snapshots/export.snap +++ b/crates/oxc_linter/src/snapshots/export.snap @@ -5,13 +5,13 @@ expression: export ⚠ eslint-plugin-import(export): Multiple exports of name 'foo'. ╭─[index.ts:1:19] 1 │ let foo; export { foo }; export * from "./export-all" - · ─── + · ─── ──────────────────────────── ╰──── ⚠ eslint-plugin-import(export): Multiple exports of name 'foo'. ╭─[index.ts:1:26] 1 │ let foo; export { foo as "foo" }; export * from "./export-all" - · ───── + · ───── ──────────────────────────── ╰──── × Identifier `Foo` has already been declared @@ -99,17 +99,12 @@ expression: export ╰──── ⚠ eslint-plugin-import(export): Multiple exports of name 'default'. - ╭─[index.ts:9:32] + ╭─[index.ts:7:32] + 6 │ const Bar = 2; + 7 │ export {Bar as default}; + · ─────── 8 │ const Baz = 3; 9 │ export {Baz as default}; · ─────── 10 │ ╰──── - - ⚠ eslint-plugin-import(export): Multiple exports of name 'default'. - ╭─[index.ts:7:32] - 6 │ const Bar = 2; - 7 │ export {Bar as default}; - · ─────── - 8 │ const Baz = 3; - ╰────