Skip to content

Commit

Permalink
feat(linter): unicorn/no-anonymous-default-export (#3220)
Browse files Browse the repository at this point in the history
Refer to
[eslint-plugin-unicorn/no-anonymous-default-export](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/v52.0.0/docs/rules/no-anonymous-default-export.md)

link: #684

---------

Co-authored-by: Boshen <boshenc@gmail.com>
  • Loading branch information
1zumii and Boshen authored May 12, 2024
1 parent faa2e4b commit d45b28a
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
166 changes: 166 additions & 0 deletions crates/oxc_linter/src/rules/unicorn/no_anonymous_default_export.rs
Original file line number Diff line number Diff line change
@@ -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();
}
59 changes: 59 additions & 0 deletions crates/oxc_linter/src/snapshots/no_anonymous_default_export.snap
Original file line number Diff line number Diff line change
@@ -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]
1export 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.

0 comments on commit d45b28a

Please sign in to comment.