astro-ghostcms/.pnpm-store/v3/files/8a/67742b89f3ac19d46eb94eb2123...

160 lines
5.8 KiB
Plaintext

/**
* @fileoverview Rule to disallow use of Object.prototype builtins on objects
* @author Andrew Levine
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const astUtils = require("./utils/ast-utils");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Returns true if the node or any of the objects
* to the left of it in the member/call chain is optional.
*
* e.g. `a?.b`, `a?.b.c`, `a?.()`, `a()?.()`
* @param {ASTNode} node The expression to check
* @returns {boolean} `true` if there is a short-circuiting optional `?.`
* in the same option chain to the left of this call or member expression,
* or the node itself is an optional call or member `?.`.
*/
function isAfterOptional(node) {
let leftNode;
if (node.type === "MemberExpression") {
leftNode = node.object;
} else if (node.type === "CallExpression") {
leftNode = node.callee;
} else {
return false;
}
if (node.optional) {
return true;
}
return isAfterOptional(leftNode);
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow calling some `Object.prototype` methods directly on objects",
recommended: true,
url: "https://eslint.org/docs/latest/rules/no-prototype-builtins"
},
hasSuggestions: true,
schema: [],
messages: {
prototypeBuildIn: "Do not access Object.prototype method '{{prop}}' from target object.",
callObjectPrototype: "Call Object.prototype.{{prop}} explicitly."
}
},
create(context) {
const DISALLOWED_PROPS = new Set([
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable"
]);
/**
* Reports if a disallowed property is used in a CallExpression
* @param {ASTNode} node The CallExpression node.
* @returns {void}
*/
function disallowBuiltIns(node) {
const callee = astUtils.skipChainExpression(node.callee);
if (callee.type !== "MemberExpression") {
return;
}
const propName = astUtils.getStaticPropertyName(callee);
if (propName !== null && DISALLOWED_PROPS.has(propName)) {
context.report({
messageId: "prototypeBuildIn",
loc: callee.property.loc,
data: { prop: propName },
node,
suggest: [
{
messageId: "callObjectPrototype",
data: { prop: propName },
fix(fixer) {
const sourceCode = context.sourceCode;
/*
* A call after an optional chain (e.g. a?.b.hasOwnProperty(c))
* must be fixed manually because the call can be short-circuited
*/
if (isAfterOptional(node)) {
return null;
}
/*
* A call on a ChainExpression (e.g. (a?.hasOwnProperty)(c)) will trigger
* no-unsafe-optional-chaining which should be fixed before this suggestion
*/
if (node.callee.type === "ChainExpression") {
return null;
}
const objectVariable = astUtils.getVariableByName(sourceCode.getScope(node), "Object");
/*
* We can't use Object if the global Object was shadowed,
* or Object does not exist in the global scope for some reason
*/
if (!objectVariable || objectVariable.scope.type !== "global" || objectVariable.defs.length > 0) {
return null;
}
let objectText = sourceCode.getText(callee.object);
if (astUtils.getPrecedence(callee.object) <= astUtils.getPrecedence({ type: "SequenceExpression" })) {
objectText = `(${objectText})`;
}
const openParenToken = sourceCode.getTokenAfter(
node.callee,
astUtils.isOpeningParenToken
);
const isEmptyParameters = node.arguments.length === 0;
const delim = isEmptyParameters ? "" : ", ";
const fixes = [
fixer.replaceText(callee, `Object.prototype.${propName}.call`),
fixer.insertTextAfter(openParenToken, objectText + delim)
];
return fixes;
}
}
]
});
}
}
return {
CallExpression: disallowBuiltIns
};
}
};