diff --git a/crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs b/crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs index 64320840dadbb5..c32d706170d9a6 100644 --- a/crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs +++ b/crates/oxc_linter/src/rules/eslint/no_duplicate_imports.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use oxc_ast::{ - ast::{ImportDeclaration, ImportDeclarationSpecifier}, + ast::{ + ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration, ImportDeclarationSpecifier, + }, AstKind, }; use oxc_diagnostics::OxcDiagnostic; @@ -16,8 +18,16 @@ fn no_duplicate_imports_diagnostic(module_name: &str, span: Span) -> OxcDiagnost .with_label(span) } +fn no_duplicate_exports_diagnostic(module_name: &str, span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn(format!("'{}' export is duplicated", module_name)) + .with_help("Merge the duplicated exports into a single export statement") + .with_label(span) +} + #[derive(Debug, Default, Clone)] -pub struct NoDuplicateImports {} +pub struct NoDuplicateImports { + include_exports: bool, +} declare_oxc_lint!( /// ### What it does @@ -41,20 +51,40 @@ declare_oxc_lint!( /// import something from 'another-module'; /// ``` NoDuplicateImports, - style, + nursery, pending); #[derive(Debug, Clone)] enum DeclarationType { Import, + Export, +} + +#[derive(Debug, Clone)] +enum Specifier { + Named, + Default, + Namespace, + All, } #[derive(Debug, Clone)] struct ModuleEntry { + specifier: Specifier, declaration_type: DeclarationType, } impl Rule for NoDuplicateImports { + fn from_configuration(value: serde_json::Value) -> Self { + let Some(value) = value.get(0) else { return Self { include_exports: false } }; + Self { + include_exports: value + .get("includeExports") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + } + } + fn run_once(&self, ctx: &LintContext) { let semantic = ctx.semantic(); let nodes = semantic.nodes(); @@ -66,6 +96,12 @@ impl Rule for NoDuplicateImports { AstKind::ImportDeclaration(import_decl) => { handle_import(import_decl, &mut modules, ctx); } + AstKind::ExportNamedDeclaration(export_decl) if self.include_exports => { + handle_export(export_decl, &mut modules, ctx); + } + AstKind::ExportAllDeclaration(export_decl) if self.include_exports => { + handle_export_all(export_decl, &mut modules, ctx); + } _ => {} } } @@ -79,30 +115,120 @@ fn handle_import( ) { let source = &import_decl.source; let module_name = source.value.to_string(); + let mut specifier = Specifier::All; + if let Some(specifiers) = &import_decl.specifiers { let has_namespace = specifiers.iter().any(|s| match s { ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => false, ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => true, _ => false, }); + + specifier = if specifiers.iter().any(|s| match s { + ImportDeclarationSpecifier::ImportDefaultSpecifier(_) => true, + _ => false, + }) { + Specifier::Default + } else if specifiers.iter().any(|s| match s { + ImportDeclarationSpecifier::ImportNamespaceSpecifier(_) => true, + _ => false, + }) { + Specifier::Namespace + } else { + Specifier::Named + }; + if has_namespace { return; } } + if let Some(existing_modules) = modules.get(&module_name) { - if existing_modules - .iter() - .any(|entry| matches!(entry.declaration_type, DeclarationType::Import)) - { + if existing_modules.iter().any(|entry| { + matches!(entry.declaration_type, DeclarationType::Import) + || matches!( + (entry.declaration_type.clone(), entry.specifier.clone()), + (DeclarationType::Export, Specifier::All) + ) + }) { ctx.diagnostic(no_duplicate_imports_diagnostic(&module_name, import_decl.span)); return; } } - let entry = ModuleEntry { declaration_type: DeclarationType::Import }; + let entry = ModuleEntry { declaration_type: DeclarationType::Import, specifier }; modules.entry(module_name.clone()).or_default().push(entry); } +fn handle_export( + export_decl: &ExportNamedDeclaration, + modules: &mut HashMap>, + ctx: &LintContext, +) { + let source = match &export_decl.source { + Some(source) => source, + None => return, + }; + let module_name = source.value.to_string(); + + if let Some(existing_modules) = modules.get(&module_name) { + if existing_modules.iter().any(|entry| { + matches!(entry.declaration_type, DeclarationType::Export) + || matches!(entry.declaration_type, DeclarationType::Import) + }) { + ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span)); + } + } + + modules.entry(module_name).or_default().push(ModuleEntry { + declaration_type: DeclarationType::Export, + specifier: Specifier::Named, + }); +} + +fn handle_export_all( + export_decl: &ExportAllDeclaration, + modules: &mut HashMap>, + ctx: &LintContext, +) { + let source = &export_decl.source; + let module_name = source.value.to_string(); + + let exported_name = export_decl.exported.clone(); + + if let Some(existing_modules) = modules.get(&module_name) { + if existing_modules.iter().any(|entry| { + matches!( + (&entry.declaration_type, &entry.specifier), + (DeclarationType::Import, Specifier::All) + ) || matches!( + (&entry.declaration_type, &entry.specifier), + (DeclarationType::Export, Specifier::All) + ) + }) { + ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span)); + } + + if exported_name.is_none() { + return; + } + + if existing_modules.iter().any(|entry| { + matches!( + (&entry.declaration_type, &entry.specifier), + (DeclarationType::Import, Specifier::Default) + ) + }) { + ctx.diagnostic(no_duplicate_exports_diagnostic(&module_name, export_decl.span)); + } + } + + modules + .entry(module_name) + .or_default() + .push(ModuleEntry { declaration_type: DeclarationType::Export, specifier: Specifier::All }); +} + #[test] fn test() { use crate::tester::Tester; @@ -192,72 +318,72 @@ fn test() { let fail = vec![ ( r#"import "fs"; - import "fs""#, + import "fs""#, None, ), ( r#"import { merge } from "lodash-es"; - import { find } from "lodash-es";"#, + import { find } from "lodash-es";"#, None, ), ( r#"import { merge } from "lodash-es"; - import _ from "lodash-es";"#, + import _ from "lodash-es";"#, None, ), ( r#"import os from "os"; - import { something } from "os"; - import * as foobar from "os";"#, + import { something } from "os"; + import * as foobar from "os";"#, None, ), ( r#"import * as modns from "lodash-es"; - import { merge } from "lodash-es"; - import { baz } from "lodash-es";"#, + import { merge } from "lodash-es"; + import { baz } from "lodash-es";"#, None, ), + ( + r#"export { os } from "os"; + export { something } from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), + ( + r#"import os from "os"; + export { os as foobar } from "os"; + export { something } from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), + ( + r#"import os from "os"; + export { something } from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), + ( + r#"import os from "os"; + export * as os from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), + ( + r#"export * as os from "os"; + import os from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), // ( - // r#"export { os } from "os"; - // export { something } from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"import os from "os"; - // export { os as foobar } from "os"; - // export { something } from "os";"#, + // r#"import * as modns from "mod"; + // export * as modns from "mod";"#, // Some(serde_json::json!([{ "includeExports": true }])), // ), - // ( - // r#"import os from "os"; - // export { something } from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"import os from "os"; - // export * as os from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"export * as os from "os"; - // import os from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"import * as modns from "mod"; - // export * as modns from "mod";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"export * from "os"; - // export * from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), - // ( - // r#"import "os"; - // export * from "os";"#, - // Some(serde_json::json!([{ "includeExports": true }])), - // ), + ( + r#"export * from "os"; + export * from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), + ( + r#"import "os"; + export * from "os";"#, + Some(serde_json::json!([{ "includeExports": true }])), + ), ]; Tester::new(NoDuplicateImports::NAME, pass, fail).test_and_snapshot(); diff --git a/crates/oxc_linter/src/snapshots/no_duplicate_imports.snap b/crates/oxc_linter/src/snapshots/no_duplicate_imports.snap index 6885974c83c47d..6eae291c8d1166 100644 --- a/crates/oxc_linter/src/snapshots/no_duplicate_imports.snap +++ b/crates/oxc_linter/src/snapshots/no_duplicate_imports.snap @@ -3,42 +3,107 @@ source: crates/oxc_linter/src/tester.rs snapshot_kind: text --- ⚠ eslint(no-duplicate-imports): 'fs' import is duplicated - ╭─[no_duplicate_imports.tsx:2:7] + ╭─[no_duplicate_imports.tsx:2:9] 1 │ import "fs"; - 2 │ import "fs" - · ─────────── + 2 │ import "fs" + · ─────────── ╰──── help: Merge the duplicated import into a single import statement ⚠ eslint(no-duplicate-imports): 'lodash-es' import is duplicated - ╭─[no_duplicate_imports.tsx:2:7] + ╭─[no_duplicate_imports.tsx:2:9] 1 │ import { merge } from "lodash-es"; - 2 │ import { find } from "lodash-es"; - · ───────────────────────────────── + 2 │ import { find } from "lodash-es"; + · ───────────────────────────────── ╰──── help: Merge the duplicated import into a single import statement ⚠ eslint(no-duplicate-imports): 'lodash-es' import is duplicated - ╭─[no_duplicate_imports.tsx:2:9] + ╭─[no_duplicate_imports.tsx:2:11] 1 │ import { merge } from "lodash-es"; - 2 │ import _ from "lodash-es"; - · ────────────────────────── + 2 │ import _ from "lodash-es"; + · ────────────────────────── ╰──── help: Merge the duplicated import into a single import statement ⚠ eslint(no-duplicate-imports): 'os' import is duplicated - ╭─[no_duplicate_imports.tsx:2:9] + ╭─[no_duplicate_imports.tsx:2:11] 1 │ import os from "os"; - 2 │ import { something } from "os"; - · ─────────────────────────────── - 3 │ import * as foobar from "os"; + 2 │ import { something } from "os"; + · ─────────────────────────────── + 3 │ import * as foobar from "os"; ╰──── help: Merge the duplicated import into a single import statement ⚠ eslint(no-duplicate-imports): 'lodash-es' import is duplicated - ╭─[no_duplicate_imports.tsx:3:9] - 2 │ import { merge } from "lodash-es"; - 3 │ import { baz } from "lodash-es"; - · ──────────────────────────────── + ╭─[no_duplicate_imports.tsx:3:11] + 2 │ import { merge } from "lodash-es"; + 3 │ import { baz } from "lodash-es"; + · ──────────────────────────────── ╰──── help: Merge the duplicated import into a single import statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:11] + 1 │ export { os } from "os"; + 2 │ export { something } from "os"; + · ─────────────────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:11] + 1 │ import os from "os"; + 2 │ export { os as foobar } from "os"; + · ────────────────────────────────── + 3 │ export { something } from "os"; + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:3:11] + 2 │ export { os as foobar } from "os"; + 3 │ export { something } from "os"; + · ─────────────────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:11] + 1 │ import os from "os"; + 2 │ export { something } from "os"; + · ─────────────────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:9] + 1 │ import os from "os"; + 2 │ export * as os from "os"; + · ───────────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' import is duplicated + ╭─[no_duplicate_imports.tsx:2:9] + 1 │ export * as os from "os"; + 2 │ import os from "os"; + · ──────────────────── + ╰──── + help: Merge the duplicated import into a single import statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:9] + 1 │ export * from "os"; + 2 │ export * from "os"; + · ─────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement + + ⚠ eslint(no-duplicate-imports): 'os' export is duplicated + ╭─[no_duplicate_imports.tsx:2:9] + 1 │ import "os"; + 2 │ export * from "os"; + · ─────────────────── + ╰──── + help: Merge the duplicated exports into a single export statement