/** * @author Sosuke Suzuki * See LICENSE file in root directory for full license. */ "use strict" const { CONSTRUCT, READ, ReferenceTracker, getPropertyName, } = require("@eslint-community/eslint-utils") const { getSourceCode } = require("eslint-compat-utils") /** * @typedef {import("estree").Node} Node * @typedef {import("estree").ClassExpression | import("estree").ClassDeclaration} ClassNode * @typedef {import("estree").CallExpression} CallExpression */ const errorConstructorNames = [ "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "AggregateError", ] const errorsTraceMap = {} for (const errorConstructorName of errorConstructorNames) { errorsTraceMap[errorConstructorName] = { [CONSTRUCT]: true, [READ]: true } } /** * @param {Node} node * @returns {boolean} */ function isSuperCall(node) { return node.type === "CallExpression" && node.callee.type === "Super" } /** * @param {Node|undefined} node * @returns {boolean} */ function isSpreadElement(node) { return node && node.type === "SpreadElement" } /** * @param {Node} node * @returns {ClassNode | null} */ function findClassFromAncestors(node) { if (node.type !== "ClassExpression" && node.type !== "ClassDeclaration") { return findClassFromAncestors(node.parent) } if (!node) { return null } return node } module.exports = { meta: { docs: { description: "disallow Error Cause.", category: "ES2022", recommended: false, url: "http://eslint-community.github.io/eslint-plugin-es-x/rules/no-error-cause.html", }, fixable: null, messages: { forbidden: "ES2022 Error Cause is forbidden.", }, schema: [], type: "problem", }, create(context) { const sourceCode = getSourceCode(context) /** @type {Array<{ classNode: ClassNode, superCallNode: CallExpression }>} */ const maybeErrorSubclasses = [] /** @type {Array<{ classNode: ClassNode, superCallNode: CallExpression }>} */ const maybeAggregateErrorSubclasses = [] /** * Checks if the received node is a constructor call with cause option. * e.g. `new Error("message", { cause: foo })`, `super("message", { cause: foo })` * * @param {Node} node * @param {boolean} isAggregateError * @returns {boolean} */ function isConstructCallWithCauseOption(node, isAggregateError) { if (node.type !== "NewExpression" && !isSuperCall(node)) { return false } const optionsArgIndex = isAggregateError ? 2 : 1 for (let index = 0; index < optionsArgIndex; index++) { if (isSpreadElement(node.arguments[index])) { return false } } const optionsArg = node.arguments[optionsArgIndex] if (!optionsArg || optionsArg.type !== "ObjectExpression") { return false } return optionsArg.properties.some((property) => { if (property.type !== "Property") { return false } // new Error("msg", { cause: foo }) return ( getPropertyName(property, sourceCode.getScope(node)) === "cause" ) }) } /** * @param {Node} node * @param {isAggregateError} boolean * @return {Node | null} */ function getReportedNode(node, isAggregateError) { const errorSubclasses = isAggregateError ? maybeAggregateErrorSubclasses : maybeErrorSubclasses if (errorSubclasses.length > 0) { for (const { classNode, superCallNode } of errorSubclasses) { if (classNode.superClass === node) { return superCallNode } } } if (isConstructCallWithCauseOption(node, isAggregateError)) { return node } return null } return { Super(node) { const superCallNode = node.parent function findErrorSubclasses(isAggregateError) { const errorSubclasses = isAggregateError ? maybeAggregateErrorSubclasses : maybeErrorSubclasses if ( isConstructCallWithCauseOption( superCallNode, isAggregateError, ) ) { const classNode = findClassFromAncestors(superCallNode) if (classNode && classNode.superClass) { errorSubclasses.push({ classNode, superCallNode }) } } } findErrorSubclasses(/* isAggregateError */ false) findErrorSubclasses(/* isAggregateError */ true) }, "Program:exit"(program) { const tracker = new ReferenceTracker( sourceCode.getScope(program), ) for (const { node, path } of tracker.iterateGlobalReferences( errorsTraceMap, )) { const reportedNode = getReportedNode( node, path.join(",") === "AggregateError", ) if (reportedNode) { context.report({ node: reportedNode, messageId: "forbidden", }) } } }, } }, }