-
-
Notifications
You must be signed in to change notification settings - Fork 451
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
Showing
3 changed files
with
227 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
crates/oxc_linter/src/rules/unicorn/no_anonymous_default_export.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
59
crates/oxc_linter/src/snapshots/no_anonymous_default_export.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
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. |