diff options
Diffstat (limited to 'tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js')
-rw-r--r-- | tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js | 119 |
1 files changed, 119 insertions, 0 deletions
diff --git a/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js new file mode 100644 index 0000000000..f714b31cda --- /dev/null +++ b/tools/lint/eslint/eslint-plugin-mozilla/lib/rules/balanced-observers.js @@ -0,0 +1,119 @@ +/** + * @fileoverview Check that there's a Services.(prefs|obs).removeObserver for + * each addObserver. + * + * 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"; + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = function(context) { + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + var addedObservers = []; + var removedObservers = []; + + function getObserverAPI(node) { + const object = node.callee.object; + if ( + object.type == "MemberExpression" && + object.property.type == "Identifier" + ) { + return object.property.name; + } + return null; + } + + function isServicesObserver(api) { + return api == "obs" || api == "prefs"; + } + + function getObservableName(node, api) { + if (api === "obs") { + return node.arguments[1].value; + } + return node.arguments[0].value; + } + + function addAddedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + addedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + node: node.callee.property, + }); + } + + function addRemovedObserver(node) { + const api = getObserverAPI(node); + if (!isServicesObserver(api)) { + return; + } + + removedObservers.push({ + functionName: node.callee.property.name, + observable: getObservableName(node, api), + }); + } + + function getUnbalancedObservers() { + const unbalanced = addedObservers.filter( + observer => !hasRemovedObserver(observer) + ); + addedObservers = removedObservers = []; + + return unbalanced; + } + + function hasRemovedObserver(addedObserver) { + return removedObservers.some( + observer => addedObserver.observable === observer.observable + ); + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + return { + CallExpression(node) { + if (node.arguments.length === 0) { + return; + } + + if (node.callee.type === "MemberExpression") { + var methodName = node.callee.property.name; + + if (methodName === "addObserver") { + addAddedObserver(node); + } else if (methodName === "removeObserver") { + addRemovedObserver(node); + } + } + }, + + "Program:exit": function() { + getUnbalancedObservers().forEach(function(observer) { + context.report( + observer.node, + "No corresponding 'removeObserver(\"{{observable}}\")' was found.", + { + observable: observer.observable, + } + ); + }); + }, + }; +}; |