diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 396e9aa59..5678b8b2a 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -13,6 +13,7 @@ mod import { pub mod namespace; pub mod no_amd; pub mod no_cycle; + pub mod no_default_export; pub mod no_deprecated; pub mod no_duplicates; pub mod no_named_as_default; @@ -582,6 +583,7 @@ oxc_macros::declare_all_lint_rules! { import::no_unresolved, import::no_unused_modules, import::no_duplicates, + import::no_default_export, jsx_a11y::alt_text, jsx_a11y::anchor_has_content, jsx_a11y::anchor_is_valid, diff --git a/crates/oxc_linter/src/rules/import/no_default_export.rs b/crates/oxc_linter/src/rules/import/no_default_export.rs new file mode 100644 index 000000000..73d033e99 --- /dev/null +++ b/crates/oxc_linter/src/rules/import/no_default_export.rs @@ -0,0 +1,101 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-import(no-default-export): Prefer named exports")] +#[diagnostic(severity(warning))] +struct NoDefaultExportDiagnostic(#[label] Span); + +#[derive(Debug, Default, Clone)] +pub struct NoDefaultExport; + +declare_oxc_lint!( + /// ### What it does + /// + /// Forbid a module to have a default exports. This help your editor to provide better auto imports. + /// + /// ### Examples + /// + /// ```javascript + /// // bad1.js + /// + /// // There is a default export. + /// export const foo = 'foo'; + /// const bar = 'bar'; + /// export default 'bar'; + /// ``` + /// + /// ```javascript + /// // bad2.js + /// + /// // There is a default export. + /// const foo = 'foo'; + /// export { foo as default } + /// ``` + /// + NoDefaultExport, + nursery +); + +impl Rule for NoDefaultExport { + fn run_once(&self, ctx: &LintContext<'_>) { + let module_record = ctx.semantic().module_record(); + write_diagnostic_optional(ctx, module_record.export_default); + module_record.export_default_duplicated.iter().for_each(|it| write_diagnostic(ctx, *it)); + write_diagnostic_optional(ctx, module_record.exported_bindings.get("default").copied()); + } +} + +fn write_diagnostic(ctx: &LintContext<'_>, span: Span) { + ctx.diagnostic(NoDefaultExportDiagnostic(span)); +} +fn write_diagnostic_optional(ctx: &LintContext<'_>, span_option: Option) { + if let Some(span) = span_option { + write_diagnostic(ctx, span); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + "export const foo = 'foo'; export const bar = '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'", + "import * as foo from './foo';", + "import foo from './foo';", + "import {default as foo} from './foo';", + "export type UserId = number;", + ]; + let fail = vec![ + "export default function bar() {};", + "export const foo = 'foo';\nexport default bar;", + "export default class Bar {};", + "export default function() {};", + "export default class {};", + "let foo; export { foo as default }", + // "export default from \"foo.js\"", + ]; + + Tester::new(NoDefaultExport::NAME, pass, fail) + .with_import_plugin(true) + .change_rule_path("index.ts") + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_default_export.snap b/crates/oxc_linter/src/snapshots/no_default_export.snap new file mode 100644 index 000000000..1242100ea --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_default_export.snap @@ -0,0 +1,40 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_default_export +--- + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:1:8] + 1 │ export default function bar() {}; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:2:8] + 1 │ export const foo = 'foo'; + 2 │ export default bar; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:1:8] + 1 │ export default class Bar {}; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:1:8] + 1 │ export default function() {}; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:1:8] + 1 │ export default class {}; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-default-export): Prefer named exports + ╭─[index.ts:1:26] + 1 │ let foo; export { foo as default } + · ─────── + ╰────