use-isInstance.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. /**
  2. * @fileoverview Reject use of instanceof against DOM interfaces
  3. *
  4. * This Source Code Form is subject to the terms of the Mozilla Public
  5. * License, v. 2.0. If a copy of the MPL was not distributed with this
  6. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7. */
  8. "use strict";
  9. const fs = require("fs");
  10. const { maybeGetMemberPropertyName } = require("../helpers");
  11. const privilegedGlobals = Object.keys(
  12. require("../environments/privileged.js").globals
  13. );
  14. // -----------------------------------------------------------------------------
  15. // Rule Definition
  16. // -----------------------------------------------------------------------------
  17. /**
  18. * Whether an identifier is defined by eslint configuration.
  19. * `env: { browser: true }` or `globals: []` for example.
  20. * @param {import("eslint-scope").Scope} currentScope
  21. * @param {import("estree").Identifier} id
  22. */
  23. function refersToEnvironmentGlobals(currentScope, id) {
  24. const reference = currentScope.references.find(ref => ref.identifier === id);
  25. const { resolved } = reference || {};
  26. if (!resolved) {
  27. return false;
  28. }
  29. // No definition in script files; defined via .eslintrc
  30. return resolved.scope.type === "global" && resolved.defs.length === 0;
  31. }
  32. /**
  33. * Whether a node points to a DOM interface.
  34. * Includes direct references to interfaces objects and also indirect references
  35. * via property access.
  36. * OS.File and lazy.(Foo) are explicitly excluded.
  37. *
  38. * @example HTMLElement
  39. * @example win.HTMLElement
  40. * @example iframe.contentWindow.HTMLElement
  41. * @example foo.HTMLElement
  42. *
  43. * @param {import("eslint-scope").Scope} currentScope
  44. * @param {import("estree").Node} node
  45. */
  46. function pointsToDOMInterface(currentScope, node) {
  47. if (node.type === "MemberExpression") {
  48. const objectName = maybeGetMemberPropertyName(node.object);
  49. if (objectName === "lazy") {
  50. // lazy.Foo is probably a non-IDL import.
  51. return false;
  52. }
  53. if (objectName === "OS" && node.property.name === "File") {
  54. // OS.File is an exception that is not a Web IDL interface
  55. return false;
  56. }
  57. // For `win.Foo`, `iframe.contentWindow.Foo`, or such.
  58. return privilegedGlobals.includes(node.property.name);
  59. }
  60. if (
  61. node.type === "Identifier" &&
  62. refersToEnvironmentGlobals(currentScope, node)
  63. ) {
  64. return privilegedGlobals.includes(node.name);
  65. }
  66. return false;
  67. }
  68. /**
  69. * @param {import("eslint").Rule.RuleContext} context
  70. */
  71. function isChromeContext(context) {
  72. const filename = context.getFilename();
  73. const isChromeFileName =
  74. filename.endsWith(".sys.mjs") ||
  75. filename.endsWith(".jsm") ||
  76. filename.endsWith(".jsm.js");
  77. if (isChromeFileName) {
  78. return true;
  79. }
  80. if (filename.endsWith(".xhtml")) {
  81. // Treat scripts in XUL files as chrome scripts
  82. // Note: readFile is needed as getSourceCode() only gives JS blocks
  83. return fs.readFileSync(filename).includes("there.is.only.xul");
  84. }
  85. // Treat scripts as chrome privileged when using:
  86. // 1. ChromeUtils, but not SpecialPowers.ChromeUtils
  87. // 2. BrowserTestUtils, PlacesUtils
  88. // 3. document.createXULElement
  89. // 4. loader.lazyRequireGetter
  90. // 5. Services.foo, but not SpecialPowers.Services.foo
  91. // 6. evalInSandbox
  92. const source = context.getSourceCode().text;
  93. return !!source.match(
  94. /(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/
  95. );
  96. }
  97. module.exports = {
  98. meta: {
  99. docs: {
  100. url:
  101. "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/use-isInstance.html",
  102. },
  103. fixable: "code",
  104. schema: [],
  105. type: "problem",
  106. },
  107. /**
  108. * @param {import("eslint").Rule.RuleContext} context
  109. */
  110. create(context) {
  111. if (!isChromeContext(context)) {
  112. return {};
  113. }
  114. return {
  115. BinaryExpression(node) {
  116. const { operator, right } = node;
  117. if (
  118. operator === "instanceof" &&
  119. pointsToDOMInterface(context.getScope(), right)
  120. ) {
  121. context.report({
  122. node,
  123. message:
  124. "Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " +
  125. "since the latter will return false when the object is created from a different context.",
  126. fix(fixer) {
  127. const sourceCode = context.getSourceCode();
  128. return fixer.replaceText(
  129. node,
  130. `${sourceCode.getText(right)}.isInstance(${sourceCode.getText(
  131. node.left
  132. )})`
  133. );
  134. },
  135. });
  136. }
  137. },
  138. };
  139. },
  140. };