UNPKG

eslint-plugin-listeners

Version:

ESLint plugin for preventing memory leaks around event listeners

183 lines (182 loc) 8.16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createRule = void 0; const utils_1 = require("../utils"); var ListenerType; (function (ListenerType) { ListenerType["ADD_EVENT_LISTENER"] = "addEventListener"; ListenerType["REMOVE_EVENT_LISTENER"] = "removeEventListener"; ListenerType["ADD_LISTENER"] = "addListener"; ListenerType["REMOVE_LISTENER"] = "removeListener"; ListenerType["REMOVE_ALL_LISTENERS"] = "removeAllListeners"; ListenerType["ON_LISTENER"] = "on"; ListenerType["OFF_LISTENER"] = "off"; })(ListenerType || (ListenerType = {})); const ADD_LISTENERS = [ListenerType.ADD_EVENT_LISTENER, ListenerType.ADD_LISTENER, ListenerType.ON_LISTENER]; const REMOVE_LISTENERS = [ListenerType.REMOVE_EVENT_LISTENER, ListenerType.REMOVE_LISTENER, ListenerType.OFF_LISTENER]; const PLAIN_FUNCTION = 'plain function'; const ARROW_FUNCTION = 'arrow function'; const isProhibitedHandler = (type) => type === PLAIN_FUNCTION || type === ARROW_FUNCTION; const reportMissingListener = (context, element, eventName, loc) => { context.report({ loc, messageId: 'missingRemoveEventListener', data: { eventName, element, }, }); }; const reportListenersDoNoMatch = (context, element, eventName, add, remove, loc, isUseCapture) => { if (isUseCapture) { return context.report({ loc, messageId: 'listenersDoNotMatchUseCapture', data: { remove, element, eventName, }, }); } context.report({ loc, messageId: 'listenersDoNotMatch', data: { eventName, element, add, remove, }, }); }; const reportProhibitedListener = (context, element, eventName, type, loc) => { context.report({ loc, messageId: 'prohibitedListener', data: { eventName, element, type, }, }); }; const callExpressionListener = (listeners) => (node) => { var _a, _b, _c, _d, _e; if ((0, utils_1.isNodeMemberExpression)(node.callee)) { const callee = node.callee; const listenerType = (_a = callee.property) === null || _a === void 0 ? void 0 : _a.name; if ([...ADD_LISTENERS, ...REMOVE_LISTENERS, ListenerType.REMOVE_ALL_LISTENERS].includes(listenerType)) { const element = (0, utils_1.parseMemberExpression)(callee); if (!element) { return; } const eventName = ListenerType.REMOVE_ALL_LISTENERS !== listenerType ? (_c = (_b = node.arguments) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.value : '_all_'; const handler = node.arguments[1]; if (listenerType === ListenerType.ADD_EVENT_LISTENER) { const params = (_e = (_d = node.arguments) === null || _d === void 0 ? void 0 : _d[2]) === null || _e === void 0 ? void 0 : _e.properties; if (params && params.length) { const isOnce = params.some((param) => param.key.name === 'once' && param.value.value); if (isOnce) { return; } } } if (!eventName) { return; } let func; if ((0, utils_1.isNodeFunctionExpression)(handler)) { func = PLAIN_FUNCTION; } else if ((0, utils_1.isNodeArrowFunctionExpression)(handler)) { func = ARROW_FUNCTION; } else if ((0, utils_1.isNodeIdentifier)(handler)) { func = handler.name; } else if (handler) { func = (0, utils_1.parseMemberExpression)(handler); } const currentTypeListeners = listeners[listenerType] || {}; listeners[listenerType] = Object.assign(Object.assign({}, currentTypeListeners), { [element]: Object.assign(Object.assign({}, currentTypeListeners[element]), { [eventName]: { func, loc: node.loc, hasUseCapture: (0, utils_1.isArgumentLiteral)(node.arguments[2]) || (0, utils_1.isArgumentIdentifier)(node.arguments[2]), } }) }); } } }; const programListener = (ruleName, listeners, context) => () => { var _a; const addListenersPre = ADD_LISTENERS.map((listenerName) => { var _a; return (_a = listeners[listenerName]) !== null && _a !== void 0 ? _a : {}; }); const addListeners = Object.assign({}, ...addListenersPre); const removeListenersPre = REMOVE_LISTENERS.map((listerName) => { var _a; return (_a = listeners[listerName]) !== null && _a !== void 0 ? _a : {}; }); const removeListeners = Object.assign({}, ...removeListenersPre); const removeAllListeners = (_a = listeners[ListenerType.REMOVE_ALL_LISTENERS]) !== null && _a !== void 0 ? _a : {}; Object.keys(addListeners).forEach((element) => { const addEvents = addListeners[element]; const removeEvents = removeListeners[element]; const removeAllEvents = removeAllListeners[element]; Object.entries(addEvents).forEach(([eventName, { func, loc, hasUseCapture }]) => { var _a; const event = (_a = removeEvents === null || removeEvents === void 0 ? void 0 : removeEvents[eventName]) !== null && _a !== void 0 ? _a : removeAllEvents; switch (ruleName) { case utils_1.RuleType.MissingRemoveEventListener: if (!event) { reportMissingListener(context, element, eventName, loc); } break; case utils_1.RuleType.InlineFunctionEventListener: if (isProhibitedHandler(func) && (!event || event.func !== undefined)) { reportProhibitedListener(context, element, eventName, func, loc); } break; case utils_1.RuleType.MatchingRemoveEventListener: if (func !== undefined && event && event.func !== func && event.func !== undefined && typeof event.func === 'string') { reportListenersDoNoMatch(context, element, eventName, func, event.func, loc, false); } if (func !== undefined && event && event.func === func && hasUseCapture && !event.hasUseCapture && event.func !== undefined) { reportListenersDoNoMatch(context, element, eventName, func, event.func, loc, hasUseCapture); } break; } }); }); }; const createRule = (ruleName) => ({ meta: { type: 'problem', docs: { description: (0, utils_1.getDescription)(ruleName), url: 'https://github.com/foad/eslint-plugin-listeners', }, messages: { missingRemoveEventListener: '{{eventName}} on {{element}} does not have a corresponding removeEventListener', listenersDoNotMatch: '{{add}} and {{remove}} on {{element}} for {{eventName}} do not match', listenersDoNotMatchUseCapture: 'removeEventListener {{remove}} on {{element}} for {{eventName}} is missing useCapture parameter', prohibitedListener: 'event handler for {{eventName}} on {{element}} is {{type}}, {{type}}s are prohibited as event handlers', }, schema: [], }, create: (context) => { const listeners = {}; return { 'CallExpression:exit': callExpressionListener(listeners), 'Program:exit': programListener(ruleName, listeners, context), }; }, defaultOptions: [], }); exports.createRule = createRule;