diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index f53365f6b7112..3fedb7b91b0cb 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -843,6 +843,13 @@ impl RuleRunner for crate::rules::import::no_named_default::NoNamedDefault { const NODE_TYPES: Option<&AstTypesBitset> = None; } +impl RuleRunner for crate::rules::import::no_named_export::NoNamedExport { + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::ExportAllDeclaration, + AstType::ExportNamedDeclaration, + ])); +} + impl RuleRunner for crate::rules::import::no_namespace::NoNamespace { const NODE_TYPES: Option<&AstTypesBitset> = None; } diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index db40fd67e4ea9..395434860752e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -30,6 +30,7 @@ pub(crate) mod import { pub mod no_named_as_default; pub mod no_named_as_default_member; pub mod no_named_default; + pub mod no_named_export; pub mod no_namespace; pub mod no_self_import; pub mod no_unassigned_import; @@ -802,6 +803,7 @@ oxc_macros::declare_all_lint_rules! { import::extensions, import::first, import::group_exports, + import::no_named_export, import::no_unassigned_import, import::no_empty_named_blocks, import::no_anonymous_default_export, diff --git a/crates/oxc_linter/src/rules/import/no_named_export.rs b/crates/oxc_linter/src/rules/import/no_named_export.rs new file mode 100644 index 0000000000000..919b3e3fa5174 --- /dev/null +++ b/crates/oxc_linter/src/rules/import/no_named_export.rs @@ -0,0 +1,120 @@ +use oxc_ast::AstKind; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule}; + +fn no_named_export_diagnostic(span: Span) -> OxcDiagnostic { + OxcDiagnostic::warn("Named exports are not allowed.") + .with_help("Replace named exports with a single export default to ensure a consistent module entry point.") + .with_label(span) +} + +#[derive(Debug, Default, Clone)] +pub struct NoNamedExport; + +// See for documentation details. +declare_oxc_lint!( + /// ### What it does + /// + /// Prohibit named exports. + /// + /// ### Why is this bad? + /// + /// Named exports require strict identifier matching and can lead to fragile imports, + /// while default exports enforce a single, consistent module entry point. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// export const foo = 'foo'; + /// + /// const bar = 'bar'; + /// export { bar } + /// + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// export default 'bar'; + /// + /// const foo = 'foo'; + /// export { foo as default } + /// ``` + NoNamedExport, + import, + style +); + +impl Rule for NoNamedExport { + fn run<'a>(&self, node: &oxc_semantic::AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::ExportAllDeclaration(all_decl) => { + ctx.diagnostic(no_named_export_diagnostic(all_decl.span)); + } + AstKind::ExportNamedDeclaration(named_decl) => { + let specifiers = &named_decl.specifiers; + if specifiers.is_empty() { + ctx.diagnostic(no_named_export_diagnostic(named_decl.span)); + } + if specifiers.iter().any(|specifier| specifier.exported.name() != "default") { + ctx.diagnostic(no_named_export_diagnostic(named_decl.span)); + } + } + _ => {} + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "module.export.foo = function () {}", + "module.export.foo = function () {}", + "export default function bar() {};", + "let foo; export { foo as default }", + "import * as foo from './foo';", + "import foo from './foo';", + "import {default as foo} from './foo';", + "let foo; export { foo as \"default\" }", + ]; + + let fail = vec![ + "export const foo = 'foo';", + " + export const foo = 'foo'; + export default bar; + ", + " + export const foo = 'foo'; + export function bar() {}; + ", + "export const foo = 'foo';", + " + const foo = 'foo'; + export { foo }; + ", + "let foo, bar; export { foo, bar }", + "export const { foo, bar } = item;", + "export const { foo, bar: baz } = item;", + "export const { foo: { bar, baz } } = item;", + " + let item; + export const foo = item; + export { item }; + ", + "export * from './foo';", + "export const { foo } = { foo: 'bar' };", + "export const { foo: { bar } } = { foo: { bar: 'baz' } };", + "export { a, b } from 'foo.js'", + "export type UserId = number;", + "export foo from 'foo.js'", + "export Memory, { MemoryValue } from './Memory'", + ]; + + Tester::new(NoNamedExport::NAME, NoNamedExport::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/import_no_named_export.snap b/crates/oxc_linter/src/snapshots/import_no_named_export.snap new file mode 100644 index 0000000000000..3e043fd4f3fbf --- /dev/null +++ b/crates/oxc_linter/src/snapshots/import_no_named_export.snap @@ -0,0 +1,145 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const foo = 'foo'; + · ───────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:2:13] + 1 │ + 2 │ export const foo = 'foo'; + · ───────────────────────── + 3 │ export default bar; + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:2:13] + 1 │ + 2 │ export const foo = 'foo'; + · ───────────────────────── + 3 │ export function bar() {}; + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:3:13] + 2 │ export const foo = 'foo'; + 3 │ export function bar() {}; + · ──────────────────────── + 4 │ + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const foo = 'foo'; + · ───────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:3:13] + 2 │ const foo = 'foo'; + 3 │ export { foo }; + · ─────────────── + 4 │ + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:15] + 1 │ let foo, bar; export { foo, bar } + · ─────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const { foo, bar } = item; + · ───────────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const { foo, bar: baz } = item; + · ────────────────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const { foo: { bar, baz } } = item; + · ────────────────────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:3:13] + 2 │ let item; + 3 │ export const foo = item; + · ──────────────────────── + 4 │ export { item }; + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:4:13] + 3 │ export const foo = item; + 4 │ export { item }; + · ──────────────── + 5 │ + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export * from './foo'; + · ────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const { foo } = { foo: 'bar' }; + · ────────────────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export const { foo: { bar } } = { foo: { bar: 'baz' } }; + · ──────────────────────────────────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export { a, b } from 'foo.js' + · ───────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + ⚠ eslint-plugin-import(no-named-export): Named exports are not allowed. + ╭─[no_named_export.tsx:1:1] + 1 │ export type UserId = number; + · ──────────────────────────── + ╰──── + help: Replace named exports with a single export default to ensure a consistent module entry point. + + × Unexpected token + ╭─[no_named_export.tsx:1:8] + 1 │ export foo from 'foo.js' + · ─── + ╰──── + + × Unexpected token + ╭─[no_named_export.tsx:1:8] + 1 │ export Memory, { MemoryValue } from './Memory' + · ────── + ╰────