forked from mirrors/gecko-dev
This gets the ESLint plugin code in a state where it can work with the old APIs or the newer ESLint v9 ones. The testing system will be updated to ESLint v9 when we do the main upgrade. However, in the meantime, this allows consumers to start testing out v9 implementations. Differential Revision: https://phabricator.services.mozilla.com/D206914
156 lines
4.6 KiB
JavaScript
156 lines
4.6 KiB
JavaScript
/**
|
|
* @fileoverview Reject use of instanceof against DOM interfaces
|
|
*
|
|
* 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";
|
|
|
|
const fs = require("fs");
|
|
|
|
const { maybeGetMemberPropertyName } = require("../helpers");
|
|
const helpers = require("../helpers");
|
|
|
|
const privilegedGlobals = Object.keys(
|
|
require("../environments/privileged.js").globals
|
|
);
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Rule Definition
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Whether an identifier is defined by eslint configuration.
|
|
* `env: { browser: true }` or `globals: []` for example.
|
|
* @param {import("eslint-scope").Scope} currentScope
|
|
* @param {import("estree").Identifier} id
|
|
*/
|
|
function refersToEnvironmentGlobals(currentScope, id) {
|
|
const reference = currentScope.references.find(ref => ref.identifier === id);
|
|
const { resolved } = reference || {};
|
|
if (!resolved) {
|
|
return false;
|
|
}
|
|
|
|
// No definition in script files; defined via .eslintrc
|
|
return resolved.scope.type === "global" && resolved.defs.length === 0;
|
|
}
|
|
|
|
/**
|
|
* Whether a node points to a DOM interface.
|
|
* Includes direct references to interfaces objects and also indirect references
|
|
* via property access.
|
|
* OS.File and lazy.(Foo) are explicitly excluded.
|
|
*
|
|
* @example HTMLElement
|
|
* @example win.HTMLElement
|
|
* @example iframe.contentWindow.HTMLElement
|
|
* @example foo.HTMLElement
|
|
*
|
|
* @param {import("eslint-scope").Scope} currentScope
|
|
* @param {import("estree").Node} node
|
|
*/
|
|
function pointsToDOMInterface(currentScope, node) {
|
|
if (node.type === "MemberExpression") {
|
|
const objectName = maybeGetMemberPropertyName(node.object);
|
|
if (objectName === "lazy") {
|
|
// lazy.Foo is probably a non-IDL import.
|
|
return false;
|
|
}
|
|
if (objectName === "OS" && node.property.name === "File") {
|
|
// OS.File is an exception that is not a Web IDL interface
|
|
return false;
|
|
}
|
|
// For `win.Foo`, `iframe.contentWindow.Foo`, or such.
|
|
return privilegedGlobals.includes(node.property.name);
|
|
}
|
|
|
|
if (
|
|
node.type === "Identifier" &&
|
|
refersToEnvironmentGlobals(currentScope, node)
|
|
) {
|
|
return privilegedGlobals.includes(node.name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param {import("eslint").Rule.RuleContext} context
|
|
*/
|
|
function isChromeContext(context) {
|
|
const filename = context.getFilename();
|
|
const isChromeFileName =
|
|
filename.endsWith(".sys.mjs") || filename.endsWith(".jsm");
|
|
if (isChromeFileName) {
|
|
return true;
|
|
}
|
|
|
|
if (filename.endsWith(".xhtml")) {
|
|
// Treat scripts in XUL files as chrome scripts
|
|
// Note: readFile is needed as getSourceCode() only gives JS blocks
|
|
return fs.readFileSync(filename).includes("there.is.only.xul");
|
|
}
|
|
|
|
// Treat scripts as chrome privileged when using:
|
|
// 1. ChromeUtils, but not SpecialPowers.ChromeUtils
|
|
// 2. BrowserTestUtils, PlacesUtils
|
|
// 3. document.createXULElement
|
|
// 4. loader.lazyRequireGetter
|
|
// 5. Services.foo, but not SpecialPowers.Services.foo
|
|
// 6. evalInSandbox
|
|
const source = context.getSourceCode().text;
|
|
return !!source.match(
|
|
/(^|\s)ChromeUtils|BrowserTestUtils|PlacesUtils|createXULElement|lazyRequireGetter|(^|\s)Services\.|evalInSandbox/
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
meta: {
|
|
docs: {
|
|
url: "https://firefox-source-docs.mozilla.org/code-quality/lint/linters/eslint-plugin-mozilla/rules/use-isInstance.html",
|
|
},
|
|
fixable: "code",
|
|
messages: {
|
|
preferIsInstance:
|
|
"Please prefer .isInstance() in chrome scripts over the standard instanceof operator for DOM interfaces, " +
|
|
"since the latter will return false when the object is created from a different context.",
|
|
},
|
|
schema: [],
|
|
type: "problem",
|
|
},
|
|
/**
|
|
* @param {import("eslint").Rule.RuleContext} context
|
|
*/
|
|
create(context) {
|
|
if (!isChromeContext(context)) {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
BinaryExpression(node) {
|
|
const { operator, right } = node;
|
|
if (
|
|
operator === "instanceof" &&
|
|
pointsToDOMInterface(helpers.getScope(context, node), right)
|
|
) {
|
|
context.report({
|
|
node,
|
|
messageId: "preferIsInstance",
|
|
fix(fixer) {
|
|
const sourceCode = context.getSourceCode();
|
|
return fixer.replaceText(
|
|
node,
|
|
`${sourceCode.getText(right)}.isInstance(${sourceCode.getText(
|
|
node.left
|
|
)})`
|
|
);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|