diff options
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js')
-rw-r--r-- | tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js | 149 |
1 files changed, 149 insertions, 0 deletions
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js new file mode 100644 index 0000000000..d571918909 --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js @@ -0,0 +1,149 @@ +/** + * @fileoverview Check that there's a removeEventListener for each + * addEventListener and an off for each on. + * Note that for now, this rule is rather simple in that it only checks that + * for each event name there is both an add and remove listener. It doesn't + * check that these are called on the right objects or with the same callback. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +module.exports = { + meta: { + docs: { + url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/balanced-listeners.html", + }, + messages: { + noCorresponding: + "No corresponding '{{functionName}}({{type}})' was found.", + }, + schema: [], + type: "problem", + }, + + create(context) { + var DICTIONARY = { + addEventListener: "removeEventListener", + on: "off", + }; + // Invert this dictionary to make it easy later. + var INVERTED_DICTIONARY = {}; + for (var i in DICTIONARY) { + INVERTED_DICTIONARY[DICTIONARY[i]] = i; + } + + // Collect the add/remove listeners in these 2 arrays. + var addedListeners = []; + var removedListeners = []; + + function addAddedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + if ( + options.properties.some( + p => p.key.name == "once" && p.value.value === true + ) + ) { + // No point in adding listeners using the 'once' option. + return; + } + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + addedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + node: node.callee.property, + useCapture: capture, + }); + } + + function addRemovedListener(node) { + var capture = false; + let options = node.arguments[2]; + if (options) { + if (options.type == "ObjectExpression") { + capture = options.properties.some( + p => p.key.name == "capture" && p.value.value === true + ); + } else { + capture = options.value; + } + } + removedListeners.push({ + functionName: node.callee.property.name, + type: node.arguments[0].value, + useCapture: capture, + }); + } + + function getUnbalancedListeners() { + var unbalanced = []; + + for (var j = 0; j < addedListeners.length; j++) { + if (!hasRemovedListener(addedListeners[j])) { + unbalanced.push(addedListeners[j]); + } + } + addedListeners = removedListeners = []; + + return unbalanced; + } + + function hasRemovedListener(addedListener) { + for (var k = 0; k < removedListeners.length; k++) { + var listener = removedListeners[k]; + if ( + DICTIONARY[addedListener.functionName] === listener.functionName && + addedListener.type === listener.type && + addedListener.useCapture === listener.useCapture + ) { + return true; + } + } + + return false; + } + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var listenerMethodName = node.callee.property.name; + + if (DICTIONARY.hasOwnProperty(listenerMethodName)) { + addAddedListener(node); + } else if (INVERTED_DICTIONARY.hasOwnProperty(listenerMethodName)) { + addRemovedListener(node); + } + } + }, + + "Program:exit": function () { + getUnbalancedListeners().forEach(function (listener) { + context.report({ + node: listener.node, + messageId: "noCorresponding", + data: { + functionName: DICTIONARY[listener.functionName], + type: listener.type, + }, + }); + }); + }, + }; + }, +}; |