eslint
Version:
An AST-based pattern checker for JavaScript.
182 lines (156 loc) • 4.72 kB
JavaScript
/**
* @fileoverview Rule to disallow use of Object.prototype builtins on objects
* @author Andrew Levine
*/
;
//------------------------------------------------------------------------------
// 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,
};
},
};