From c4975d0ca4e127dadb3d5579405b532b28a2a4cc Mon Sep 17 00:00:00 2001 From: fisker Date: Sat, 23 Sep 2023 00:44:21 +0800 Subject: [PATCH] Ignore `EventEmitter` from `@angular/core` and `eventemitter3` --- rules/prefer-event-target.js | 77 +++++++++++++++++++- rules/utils/get-ancestor.js | 20 +++++ rules/utils/index.js | 1 + test/prefer-event-target.mjs | 35 +++++---- test/snapshots/prefer-event-target.mjs.md | 24 +++++- test/snapshots/prefer-event-target.mjs.snap | Bin 409 -> 498 bytes 6 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 rules/utils/get-ancestor.js diff --git a/rules/prefer-event-target.js b/rules/prefer-event-target.js index d30ffb1331..6b273d6b1a 100644 --- a/rules/prefer-event-target.js +++ b/rules/prefer-event-target.js @@ -1,12 +1,81 @@ '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; + } + + const source = isStaticRequire(node) + ? node.arguments[0] + : node.type === 'AwaitExpression' && node.argument.type === 'ImportExpression' + ? node.argument.source + : undefined; + + 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 +90,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..50e9054368 --- /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..f4a7dc11f4 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 347f062ac34ed430e6ffd66ced95bf1dfb50041e..c731c010134d71a0e5cefc6e019269d0dfdfaebd 100644 GIT binary patch literal 498 zcmVd&P5_8iHn_TAp^1Q@}hvOooS zivFjQ*q=)uF;lW;dH7@|BUrQri03wN$V%kS|GA{c{0(>W@?VT#QC%hmhO>na)G|)i zo-D`>FtROsznKv%dJBl(TrF3d<TNJ^&yvlKI3*~>%E_z&T3)=i|%G&U|?oo2m64Ll|hiHgMrIV!BC+_ zAvr&7JG2`B^