balanced-observers.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. /**
  2. * @fileoverview Check that there's a Services.(prefs|obs).removeObserver for
  3. * each addObserver.
  4. *
  5. * This Source Code Form is subject to the terms of the Mozilla Public
  6. * License, v. 2.0. If a copy of the MPL was not distributed with this
  7. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  8. */
  9. "use strict";
  10. module.exports = {
  11. meta: {
  12. docs: {
  13. url:
  14. "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/balanced-observers.html",
  15. },
  16. type: "problem",
  17. },
  18. create(context) {
  19. var addedObservers = [];
  20. var removedObservers = [];
  21. function getObserverAPI(node) {
  22. const object = node.callee.object;
  23. if (
  24. object.type == "MemberExpression" &&
  25. object.property.type == "Identifier"
  26. ) {
  27. return object.property.name;
  28. }
  29. return null;
  30. }
  31. function isServicesObserver(api) {
  32. return api == "obs" || api == "prefs";
  33. }
  34. function getObservableName(node, api) {
  35. if (api === "obs") {
  36. return node.arguments[1].value;
  37. }
  38. return node.arguments[0].value;
  39. }
  40. function addAddedObserver(node) {
  41. const api = getObserverAPI(node);
  42. if (!isServicesObserver(api)) {
  43. return;
  44. }
  45. addedObservers.push({
  46. functionName: node.callee.property.name,
  47. observable: getObservableName(node, api),
  48. node: node.callee.property,
  49. });
  50. }
  51. function addRemovedObserver(node) {
  52. const api = getObserverAPI(node);
  53. if (!isServicesObserver(api)) {
  54. return;
  55. }
  56. removedObservers.push({
  57. functionName: node.callee.property.name,
  58. observable: getObservableName(node, api),
  59. });
  60. }
  61. function getUnbalancedObservers() {
  62. const unbalanced = addedObservers.filter(
  63. observer => !hasRemovedObserver(observer)
  64. );
  65. addedObservers = removedObservers = [];
  66. return unbalanced;
  67. }
  68. function hasRemovedObserver(addedObserver) {
  69. return removedObservers.some(
  70. observer => addedObserver.observable === observer.observable
  71. );
  72. }
  73. return {
  74. CallExpression(node) {
  75. if (node.arguments.length === 0) {
  76. return;
  77. }
  78. if (node.callee.type === "MemberExpression") {
  79. var methodName = node.callee.property.name;
  80. if (methodName === "addObserver") {
  81. addAddedObserver(node);
  82. } else if (methodName === "removeObserver") {
  83. addRemovedObserver(node);
  84. }
  85. }
  86. },
  87. "Program:exit": function() {
  88. getUnbalancedObservers().forEach(function(observer) {
  89. context.report({
  90. node: observer.node,
  91. message:
  92. "No corresponding 'removeObserver(\"{{observable}}\")' was found.",
  93. data: {
  94. observable: observer.observable,
  95. },
  96. });
  97. });
  98. },
  99. };
  100. },
  101. };