diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 1f1f4dd18..4c3b65301 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -180,6 +180,7 @@ mod unicorn { pub mod prefer_date_now; pub mod prefer_dom_node_append; pub mod prefer_dom_node_dataset; + pub mod prefer_event_target; pub mod prefer_logical_operator_over_ternary; pub mod prefer_optional_catch_binding; pub mod prefer_query_selector; @@ -343,6 +344,7 @@ oxc_macros::declare_all_lint_rules! { unicorn::prefer_date_now, unicorn::prefer_dom_node_append, unicorn::prefer_dom_node_dataset, + unicorn::prefer_event_target, unicorn::prefer_logical_operator_over_ternary, unicorn::prefer_optional_catch_binding, unicorn::prefer_query_selector, diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_event_target.rs b/crates/oxc_linter/src/rules/unicorn/prefer_event_target.rs new file mode 100644 index 000000000..4c0b1af31 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_event_target.rs @@ -0,0 +1,111 @@ +use oxc_ast::{ast::Expression, AstKind}; +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule, AstNode}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter`")] +#[diagnostic(severity(warning), help("Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers."))] +struct PreferEventTargetDiagnostic(#[label] pub Span); + +#[derive(Debug, Default, Clone)] +pub struct PreferEventTarget; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefers `EventTarget` over `EventEmitter`. + /// + /// This rule reduces the bundle size and makes your code more cross-platform friendly. + /// + /// See the [differences](https://nodejs.org/api/events.html#eventtarget-and-event-api) between `EventEmitter` and `EventTarget`. + /// + /// ### Why is this bad? + /// + /// While [`EventEmitter`](https://nodejs.org/api/events.html#class-eventemitter) is only available in Node.js, [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) is also available in _Deno_ and browsers. + /// + /// ### Example + /// ```javascript + /// // Bad + /// class Foo extends EventEmitter {} + /// + /// // Good + /// class Foo extends OtherClass {} + /// ``` + PreferEventTarget, + pedantic +); + +impl Rule for PreferEventTarget { + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::IdentifierReference(ident) = node.kind() else { return }; + + if ident.name.as_str() != "EventEmitter" { + return; + } + + let Some(parent) = ctx.nodes().parent_node(node.id()) else { + return; + }; + + match parent.kind() { + AstKind::ClassHeritage(_) => {} + AstKind::NewExpression(new_expr) => { + let Expression::Identifier(callee_ident) = &new_expr.callee else { + return; + }; + + if ident as *const _ != callee_ident.0 as *const _ { + return; + } + } + _ => return, + }; + + ctx.diagnostic(PreferEventTargetDiagnostic(ident.span)); + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + r"class Foo {}", + r"class Foo extends OtherClass {}", + r"class Foo extends EventTarget {}", + r"const Foo = class extends EventTarget {}", + r"const Foo = class extends foo.EventTarget {}", + r"const Foo = class extends foo.bar.EventTarget {}", + r"class Foo extends foo.EventEmitter {}", + r"class Foo extends foo.bar.EventEmitter {}", + r"class EventEmitter extends Foo {}", + r"const Foo = class EventEmitter extends Foo {}", + r"new Foo(EventEmitter)", + r"new foo.EventEmitter()", + r"EventTarget()", + r"new EventTarget", + r"const target = new EventTarget;", + r"const target = EventTarget()", + r"const target = new Foo(EventEmitter);", + r"EventEmitter()", + r"const emitter = EventEmitter()", + ]; + + let fail = vec![ + r"class Foo extends EventEmitter {}", + r"class Foo extends EventEmitter { someMethod() {} }", + r"const Foo = class extends EventEmitter {}", + r"new EventEmitter", + r"const emitter = new EventEmitter;", + r"for (const {EventEmitter} of []) {new EventEmitter}", + r"for (const EventEmitter of []) {new EventEmitter}", + ]; + + Tester::new_without_config(PreferEventTarget::NAME, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/prefer_event_target.snap b/crates/oxc_linter/src/snapshots/prefer_event_target.snap new file mode 100644 index 000000000..4d83594db --- /dev/null +++ b/crates/oxc_linter/src/snapshots/prefer_event_target.snap @@ -0,0 +1,54 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: prefer_event_target +--- + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ class Foo extends EventEmitter {} + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ class Foo extends EventEmitter { someMethod() {} } + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ const Foo = class extends EventEmitter {} + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ new EventEmitter + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ const emitter = new EventEmitter; + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ for (const {EventEmitter} of []) {new EventEmitter} + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + + ⚠ eslint-plugin-unicorn(prefer-event-target): Prefer `EventTarget` over `EventEmitter` + ╭─[prefer_event_target.tsx:1:1] + 1 │ for (const EventEmitter of []) {new EventEmitter} + · ──────────── + ╰──── + help: Change `EventEmitter` to `EventTarget`. EventEmitters are only available in Node.js, while EventTargets are also available in browsers. + +