Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
120 changes: 120 additions & 0 deletions crates/oxc_linter/src/rules/import/no_named_export.rs
Original file line number Diff line number Diff line change
@@ -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 <https://github.com/oxc-project/oxc/issues/6050> 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();
}
145 changes: 145 additions & 0 deletions crates/oxc_linter/src/snapshots/import_no_named_export.snap
Original file line number Diff line number Diff line change
@@ -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'
· ──────
╰────
Loading