mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): unicorn/no-anonymous-default-export (#3220)
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>
This commit is contained in:
parent
faa2e4b26b
commit
d45b28a3b9
3 changed files with 227 additions and 0 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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.
|
||||
Loading…
Reference in a new issue