193 lines
5.9 KiB
JavaScript
193 lines
5.9 KiB
JavaScript
/**
|
|
* @author Sosuke Suzuki <https://github.com/sosukesuzuki>
|
|
* 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",
|
|
})
|
|
}
|
|
}
|
|
},
|
|
}
|
|
},
|
|
}
|