Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer-event-target rule #1792

Merged
merged 14 commits into from May 8, 2022
1 change: 1 addition & 0 deletions configs/recommended.js
Expand Up @@ -79,6 +79,7 @@ module.exports = {
'unicorn/prefer-dom-node-dataset': 'error',
'unicorn/prefer-dom-node-remove': 'error',
'unicorn/prefer-dom-node-text-content': 'error',
'unicorn/prefer-event-target': 'error',
'unicorn/prefer-export-from': 'error',
'unicorn/prefer-includes': 'error',
'unicorn/prefer-json-parse-buffer': 'off',
Expand Down
39 changes: 39 additions & 0 deletions docs/rules/prefer-event-target.md
@@ -0,0 +1,39 @@
# Prefer `EventTarget` over `EventEmitter`

<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
<!-- RULE_NOTICE -->
✅ *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*

<!-- /RULE_NOTICE -->

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.

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`.

## Fail

```js
import EventEmitter from 'node:event';

class Foo extends EventEmitter {

}
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
```

```js
const emitter = new EventEmitter;
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
```

## Pass

```js
class Foo extends EventTarget {

}
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
```

```js
const target = new EventTarget;
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
```
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -119,6 +119,7 @@ Each rule has emojis denoting:
| [prefer-dom-node-dataset](docs/rules/prefer-dom-node-dataset.md) | Prefer using `.dataset` on DOM elements over calling attribute methods. | ✅ | 🔧 | |
| [prefer-dom-node-remove](docs/rules/prefer-dom-node-remove.md) | Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`. | ✅ | 🔧 | 💡 |
| [prefer-dom-node-text-content](docs/rules/prefer-dom-node-text-content.md) | Prefer `.textContent` over `.innerText`. | ✅ | | 💡 |
| [prefer-event-target](docs/rules/prefer-event-target.md) | Prefer EventTarget instead of EventEmitter | ✅ | | |
| [prefer-export-from](docs/rules/prefer-export-from.md) | Prefer `export…from` when re-exporting. | ✅ | 🔧 | 💡 |
| [prefer-includes](docs/rules/prefer-includes.md) | Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence. | ✅ | 🔧 | 💡 |
| [prefer-json-parse-buffer](docs/rules/prefer-json-parse-buffer.md) | Prefer reading a JSON file as a buffer. | | 🔧 | |
Expand Down
49 changes: 49 additions & 0 deletions rules/prefer-event-target.js
@@ -0,0 +1,49 @@
'use strict';
const {matches} = require('./selectors/index.js');

const MESSAGE_ID = 'prefer-event-target';
const messages = {
[MESSAGE_ID]: 'Prefer `EventTarget` over `EventEmitter`.',
};

const eventEmitterSuperClassSelector = [
':matches(ClassDeclaration, ClassExpression)',
'[superClass.name="EventEmitter"]',
'[body.type="ClassBody"]',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added this body type check, but not tests covers this, is it possible to have different body here? Maybe in TypeScript? Please test it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added this body type check, but not tests covers this, is it possible to have different body here? Maybe in TypeScript? Please test it.

I'm sorry, but I think I actually don't understand this feedback.

Is it possible to handle typescript with this lib?
I tried to insert a simple typescript statement (let a: number = 3;) and tested, but it seems like parsing error occurs.

I think class declaration's with other body type seems not possible, according to this ECMA document in Javascript,
In case of Typescript, this specification's 117 page

And FYI, when I added this selector, I referenced to /rules/no-static-only-class.js's selector.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if it's possible to have different body in TS, I guess it's fine to keep this just in case.

If you find anything want test, you can use

test.typescript();
// OR
test.snapshot({
	valid: [
		{
			code: '',
			parser: parsers.typescript,
		}
	],
});

There are examples in other rules.

' > ',
'[name="EventEmitter"]',
].join('');
jopemachine marked this conversation as resolved.
Show resolved Hide resolved

const newEventEmitterSelector = [
'NewExpression',
'[callee.name="EventEmitter"]',
' > ',
'[name="EventEmitter"]',
].join('');
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
jopemachine marked this conversation as resolved.
Show resolved Hide resolved

const selector = matches([
eventEmitterSuperClassSelector,
newEventEmitterSelector,
]);

/** @param {import('eslint').Rule.RuleContext} context */
const create = () => ({
[selector](node) {
return {
node,
messageId: MESSAGE_ID,
};
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `EventTarget` over `EventEmitter`.',
},
messages,
},
};
32 changes: 32 additions & 0 deletions test/prefer-event-target.mjs
@@ -0,0 +1,32 @@
import outdent from 'outdent';
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);

test.snapshot({
valid: [
'class Foo {}',
'class Foo extends OtherClass {}',
'class Foo extends EventTarget {}',
],
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
invalid: [
'class Foo extends EventEmitter {}',
'class Foo extends EventEmitter { someMethod() {} }',
outdent`
class Foo extends EventEmitter {
addListener() {}
removeListener() {}
}`,
],
fisker marked this conversation as resolved.
Show resolved Hide resolved
});

test.snapshot({
valid: [
'new EventTarget',
'const target = new EventTarget;',
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
],
jopemachine marked this conversation as resolved.
Show resolved Hide resolved
invalid: [
'new EventEmitter',
'const emitter = new EventEmitter;',
],
});
61 changes: 61 additions & 0 deletions test/snapshots/prefer-event-target.mjs.md
@@ -0,0 +1,61 @@
# Snapshot report for `test/prefer-event-target.mjs`

The actual snapshot is saved in `prefer-event-target.mjs.snap`.

Generated by [AVA](https://avajs.dev).

## Invalid #1
1 | class Foo extends EventEmitter {}

> Error 1/1

`␊
> 1 | class Foo extends EventEmitter {}␊
| ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊
`

## Invalid #2
1 | class Foo extends EventEmitter { someMethod() {} }

> Error 1/1

`␊
> 1 | class Foo extends EventEmitter { someMethod() {} }␊
| ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊
`

## Invalid #3
1 | class Foo extends EventEmitter {
2 | addListener() {}
3 | removeListener() {}
4 | }

> Error 1/1

`␊
> 1 | class Foo extends EventEmitter {␊
| ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊
2 | addListener() {}␊
3 | removeListener() {}␊
4 | }␊
`

## Invalid #1
1 | new EventEmitter

> Error 1/1

`␊
> 1 | new EventEmitter␊
| ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊
`

## Invalid #2
1 | const emitter = new EventEmitter;

> Error 1/1

`␊
> 1 | const emitter = new EventEmitter;␊
| ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊
`
Binary file added test/snapshots/prefer-event-target.mjs.snap
Binary file not shown.