diff --git a/rules/prefer-event-target.js b/rules/prefer-event-target.js index d30ffb1331..9d4373d4d0 100644 --- a/rules/prefer-event-target.js +++ b/rules/prefer-event-target.js @@ -1,12 +1,82 @@ 'use strict'; +const {findVariable} = require('@eslint-community/eslint-utils'); +const {getAncestor} = require('./utils/index.js'); +const {isStaticRequire, isStringLiteral, isMemberExpression} = require('./ast/index.js'); const MESSAGE_ID = 'prefer-event-target'; const messages = { [MESSAGE_ID]: 'Prefer `EventTarget` over `EventEmitter`.', }; +const packagesShouldBeIgnored = new Set([ + '@angular/core', + 'eventemitter3', +]); + +const isConstVariableDeclarationId = node => + node.parent.type === 'VariableDeclarator' + && node.parent.id === node + && node.parent.parent.type === 'VariableDeclaration' + && node.parent.parent.kind === 'const' + && node.parent.parent.declarations.includes(node.parent); + +function isAwaitImportOrRequireFromIgnoredPackages(node) { + if (!node) { + return false; + } + + let source; + if (isStaticRequire(node)) { + [source] = node.arguments; + } else if (node.type === 'AwaitExpression' && node.argument.type === 'ImportExpression') { + ({source} = node.argument); + } + + if (isStringLiteral(source) && packagesShouldBeIgnored.has(source.value)) { + return true; + } + + return false; +} + +function isFromIgnoredPackage(node) { + if (!node) { + return false; + } + + const importDeclaration = getAncestor(node, 'ImportDeclaration'); + if (packagesShouldBeIgnored.has(importDeclaration?.source.value)) { + return true; + } + + // `const {EventEmitter} = ...` + if ( + node.parent.type === 'Property' + && node.parent.value === node + && node.parent.key.type === 'Identifier' + && node.parent.key.name === 'EventEmitter' + && node.parent.parent.type === 'ObjectPattern' + && node.parent.parent.properties.includes(node.parent) + && isConstVariableDeclarationId(node.parent.parent) + && isAwaitImportOrRequireFromIgnoredPackages(node.parent.parent.parent.init) + ) { + return true; + } + + // `const EventEmitter = (...).EventEmitter` + if ( + isConstVariableDeclarationId(node) + && isMemberExpression(node.parent.init, {property: 'EventEmitter', optional: false, computed: false}) + && isAwaitImportOrRequireFromIgnoredPackages(node.parent.init.object) + ) { + return true; + } + + return false; +} + /** @param {import('eslint').Rule.RuleContext} context */ -const create = () => ({ +const create = context => ({ Identifier(node) { if (!( node.name === 'EventEmitter' @@ -21,6 +91,12 @@ const create = () => ({ return; } + const scope = context.sourceCode.getScope(node); + const variableNode = findVariable(scope, node)?.defs[0]?.name; + if (isFromIgnoredPackage(variableNode)) { + return; + } + return { node, messageId: MESSAGE_ID, diff --git a/rules/utils/get-ancestor.js b/rules/utils/get-ancestor.js new file mode 100644 index 0000000000..fe2fb243a4 --- /dev/null +++ b/rules/utils/get-ancestor.js @@ -0,0 +1,20 @@ +'use strict'; + +// TODO: Support more types +function getPredicate(options) { + if (typeof options === 'string') { + return node => node.type === options; + } +} + +function getAncestor(node, options) { + const predicate = getPredicate(options); + + for (;node.parent; node = node.parent) { + if (predicate(node)) { + return node; + } + } +} + +module.exports = getAncestor; diff --git a/rules/utils/index.js b/rules/utils/index.js index d4d28d3132..dfcafa4ecc 100644 --- a/rules/utils/index.js +++ b/rules/utils/index.js @@ -48,5 +48,6 @@ module.exports = { shouldAddParenthesesToSpreadElementArgument: require('./should-add-parentheses-to-spread-element-argument.js'), singular: require('./singular.js'), toLocation: require('./to-location.js'), + getAncestor: require('./get-ancestor.js'), }; diff --git a/test/prefer-event-target.mjs b/test/prefer-event-target.mjs index dce958a4f4..fdb0f0a3df 100644 --- a/test/prefer-event-target.mjs +++ b/test/prefer-event-target.mjs @@ -17,6 +17,24 @@ test.snapshot({ 'const Foo = class EventEmitter extends Foo {}', 'new Foo(EventEmitter)', 'new foo.EventEmitter()', + ...[ + 'import {EventEmitter} from "@angular/core";', + 'const {EventEmitter} = require("@angular/core");', + 'const EventEmitter = require("@angular/core").EventEmitter;', + 'import {EventEmitter} from "eventemitter3";', + 'const {EventEmitter} = await import("eventemitter3");', + 'const EventEmitter = (await import("eventemitter3")).EventEmitter;', + ].map(code => outdent` + ${code} + class Foo extends EventEmitter {} + `), + 'EventTarget()', + 'new EventTarget', + 'const target = new EventTarget;', + 'const target = EventTarget()', + 'const target = new Foo(EventEmitter);', + 'EventEmitter()', + 'const emitter = EventEmitter()', ], invalid: [ 'class Foo extends EventEmitter {}', @@ -28,21 +46,10 @@ test.snapshot({ removeListener() {} } `, - ], -}); - -test.snapshot({ - valid: [ - 'EventTarget()', - 'new EventTarget', - 'const target = new EventTarget;', - 'const target = EventTarget()', - 'const target = new Foo(EventEmitter);', - 'EventEmitter()', - 'const emitter = EventEmitter()', - ], - invalid: [ 'new EventEmitter', 'const emitter = new EventEmitter;', + // For coverage + 'for (const {EventEmitter} of []) {new EventEmitter}', + 'for (const EventEmitter of []) {new EventEmitter}', ], }); diff --git a/test/snapshots/prefer-event-target.mjs.md b/test/snapshots/prefer-event-target.mjs.md index d527bbc0f1..393c136709 100644 --- a/test/snapshots/prefer-event-target.mjs.md +++ b/test/snapshots/prefer-event-target.mjs.md @@ -50,7 +50,7 @@ Generated by [AVA](https://avajs.dev). 4 | }␊ ` -## Invalid #1 +## Invalid #5 1 | new EventEmitter > Error 1/1 @@ -60,7 +60,7 @@ Generated by [AVA](https://avajs.dev). | ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊ ` -## Invalid #2 +## Invalid #6 1 | const emitter = new EventEmitter; > Error 1/1 @@ -69,3 +69,23 @@ Generated by [AVA](https://avajs.dev). > 1 | const emitter = new EventEmitter;␊ | ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊ ` + +## Invalid #7 + 1 | for (const {EventEmitter} of []) {new EventEmitter} + +> Error 1/1 + + `␊ + > 1 | for (const {EventEmitter} of []) {new EventEmitter}␊ + | ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊ + ` + +## Invalid #8 + 1 | for (const EventEmitter of []) {new EventEmitter} + +> Error 1/1 + + `␊ + > 1 | for (const EventEmitter of []) {new EventEmitter}␊ + | ^^^^^^^^^^^^ Prefer \`EventTarget\` over \`EventEmitter\`.␊ + ` diff --git a/test/snapshots/prefer-event-target.mjs.snap b/test/snapshots/prefer-event-target.mjs.snap index 347f062ac3..c731c01013 100644 Binary files a/test/snapshots/prefer-event-target.mjs.snap and b/test/snapshots/prefer-event-target.mjs.snap differ