diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index dbc94004dc101..03cb0c875310e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -227,6 +227,7 @@ mod unicorn { pub mod filename_case; pub mod new_for_builtins; pub mod no_abusive_eslint_disable; + pub mod no_anonymous_default_export; pub mod no_array_for_each; pub mod no_array_reduce; pub mod no_await_expression_member; @@ -545,6 +546,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::filename_case, unicorn::new_for_builtins, unicorn::no_abusive_eslint_disable, + unicorn::no_anonymous_default_export, unicorn::no_array_for_each, unicorn::no_array_reduce, unicorn::no_await_expression_member, diff --git a/crates/oxc_linter/src/rules/unicorn/no_anonymous_default_export.rs b/crates/oxc_linter/src/rules/unicorn/no_anonymous_default_export.rs new file mode 100644 index 0000000000000..806023109ac92 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/no_anonymous_default_export.rs @@ -0,0 +1,166 @@ +use oxc_ast::{ + ast::{AssignmentExpression, AssignmentTarget, ExportDefaultDeclarationKind, Expression}, + AstKind, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +fn no_anonymous_default_export_diagnostic(span0: Span, x1: &str) -> OxcDiagnostic { + OxcDiagnostic::warning("eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export") + .with_help(format!("The {x1} should be named.")) + .with_labels([span0.into()]) +} + +#[derive(Debug, Default, Clone)] +pub struct NoAnonymousDefaultExport; + +declare_oxc_lint!( + /// ### What it does + /// Disallow anonymous functions and classes as the default export + /// + /// ### Why is this bad? + /// Naming default exports improves codebase searchability by ensuring consistent identifier use for a module's default export, both where it's declared and where it's imported. + /// + /// ### Example + /// ```javascript + /// // Bad + /// export default class {} + /// export default function () {} + /// export default () => {}; + /// module.exports = class {}; + /// module.exports = function () {}; + /// module.exports = () => {}; + /// + /// // Good + /// export default class Foo {} + /// export default function foo () {} + /// + /// const foo = () => {}; + /// export default foo; + /// + /// module.exports = class Foo {}; + /// module.exports = function foo () {}; + /// + /// const foo = () => {}; + /// module.exports = foo; + /// ``` + NoAnonymousDefaultExport, + restriction, +); + +impl Rule for NoAnonymousDefaultExport { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let problem_node = match node.kind() { + // ESM: export default + AstKind::ExportDefaultDeclaration(export_decl) => match &export_decl.declaration { + ExportDefaultDeclarationKind::ClassDeclaration(class_decl) => class_decl + .id + .as_ref() + .map_or(Some((export_decl.span, ErrorNodeKind::Class)), |_| None), + ExportDefaultDeclarationKind::FunctionDeclaration(function_decl) => function_decl + .id + .as_ref() + .map_or(Some((export_decl.span, ErrorNodeKind::Function)), |_| None), + ExportDefaultDeclarationKind::ArrowFunctionExpression(_) => { + Some((export_decl.span, ErrorNodeKind::Function)) + } + ExportDefaultDeclarationKind::ParenthesizedExpression(expr) => { + let expr = expr.expression.get_inner_expression(); + match expr { + Expression::ClassExpression(class_expr) => class_expr + .id + .as_ref() + .map_or(Some((class_expr.span, ErrorNodeKind::Class)), |_| None), + Expression::FunctionExpression(func_expr) => func_expr + .id + .as_ref() + .map_or(Some((func_expr.span, ErrorNodeKind::Function)), |_| None), + _ => None, + } + } + _ => None, + }, + // CommonJS: module.exports + AstKind::AssignmentExpression(expr) if is_common_js_export(expr) => match &expr.right { + Expression::ClassExpression(class_expr) => { + class_expr.id.as_ref().map_or(Some((expr.span, ErrorNodeKind::Class)), |_| None) + } + Expression::FunctionExpression(function_expr) => function_expr + .id + .as_ref() + .map_or(Some((expr.span, ErrorNodeKind::Function)), |_| None), + Expression::ArrowFunctionExpression(_) => { + Some((expr.span, ErrorNodeKind::Function)) + } + _ => None, + }, + _ => None, + }; + + if let Some((span, error_kind)) = problem_node { + ctx.diagnostic(no_anonymous_default_export_diagnostic(span, error_kind.as_str())); + }; + } +} + +fn is_common_js_export(expr: &AssignmentExpression) -> bool { + if let AssignmentTarget::StaticMemberExpression(member_expr) = &expr.left { + if let Expression::Identifier(object_ident) = &member_expr.object { + if object_ident.name != "module" { + return false; + } + } + + if member_expr.property.name != "exports" { + return false; + } + } + + true +} + +enum ErrorNodeKind { + Function, + Class, +} + +impl ErrorNodeKind { + fn as_str(&self) -> &str { + match self { + Self::Function => "function", + Self::Class => "class", + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r"export default class Foo {}", + r"export default function foo () {}", + r"const foo = () => {}; export default foo;", + r"module.exports = class Foo {};", + r"module.exports = function foo () {};", + r"const foo = () => {}; module.exports = foo;", + // TODO: need handle this situation? + // r"module['exports'] = function foo () {};", + ]; + + let fail = vec![ + r"export default class {}", + r"export default function () {}", + r"export default () => {};", + r"module.exports = class {}", + r"module.exports = function () {}", + r"module.exports = () => {}", + "export default (async function * () {})", + "export default (class extends class {} {})", + ]; + + Tester::new(NoAnonymousDefaultExport::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_anonymous_default_export.snap b/crates/oxc_linter/src/snapshots/no_anonymous_default_export.snap new file mode 100644 index 0000000000000..b97c78d38f31b --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_anonymous_default_export.snap @@ -0,0 +1,59 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_anonymous_default_export +--- + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ export default class {} + · ─────────────────────── + ╰──── + help: The class should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ export default function () {} + · ───────────────────────────── + ╰──── + help: The function should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ export default () => {}; + · ──────────────────────── + ╰──── + help: The function should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ module.exports = class {} + · ───────────────────────── + ╰──── + help: The class should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ module.exports = function () {} + · ─────────────────────────────── + ╰──── + help: The function should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:1] + 1 │ module.exports = () => {} + · ───────────────────────── + ╰──── + help: The function should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:17] + 1 │ export default (async function * () {}) + · ────────────────────── + ╰──── + help: The function should be named. + + ⚠ eslint-plugin-unicorn(no-anonymous-default-export): Disallow anonymous functions and classes as the default export + ╭─[no_anonymous_default_export.tsx:1:17] + 1 │ export default (class extends class {} {}) + · ───────────────────────── + ╰──── + help: The class should be named.