Repository: eslint-community/eslint-plugin-es-x
Pull request #62 (2023-02-03 05:54:13)

Improve rules for checking prototype methods to reduce false positives


Patch:
diff --git a/lib/util/define-prototype-method-handler/index.js b/lib/util/define-prototype-method-handler/index.js
new file mode 100644
index 0000000..e4ce6d1
--- /dev/null
+++ b/lib/util/define-prototype-method-handler/index.js
@@ -0,0 +1,141 @@
+"use strict"
+
+const { getPropertyName } = require("@eslint-community/eslint-utils")
+const { buildObjectTypeChecker } = require("./object-type-checker")
+const { buildObjectTypeCheckerForTS } = require("./object-type-checker-for-ts")
+
+/**
+ * @typedef {object} CreateReportArgument
+ * @property {true | 'aggressive'} objectTypeResult
+ * @property {string} propertyName
+ * @property {MemberExpression} node
+ */
+/**
+ * @typedef {object} Options
+ * @property { (arg: CreateReportArgument) => ReportDescriptor } [Options.createReport]
+ */
+
+/**
+ * Define handlers to disallow prototype methods.
+ * @param {RuleContext} context The rule context.
+ * @param {Record<string, readonly string[]>} nameMap The method names to disallow. The key is class names and that value is method names.
+ * @param {Options} [options] The options.
+ * @returns {Record<string, (node: ASTNode) => void>} The defined handlers.
+ */
+function definePrototypeMethodHandler(context, nameMap, options) {
+    const aggressiveOption = getAggressiveOption(context)
+    const aggressiveResult = aggressiveOption ? "aggressive" : false
+
+    const objectTypeChecker =
+        buildObjectTypeCheckerForTS(context, aggressiveResult) ||
+        buildObjectTypeChecker(context, aggressiveResult)
+
+    /**
+     * Check if the type of the given node is one of given class or not.
+     * @param {MemberExpression} memberAccessNode The MemberExpression node.
+     * @param {string} className The class name to disallow.
+     * @returns {boolean | "aggressive"} `true` if should disallow it.
+     */
+    function checkObjectType(memberAccessNode, className) {
+        // If it's obvious, shortcut.
+        if (memberAccessNode.object.type === "ArrayExpression") {
+            return className === "Array"
+        }
+        if (
+            memberAccessNode.object.type === "Literal" &&
+            memberAccessNode.object.regex
+        ) {
+            return className === "RegExp"
+        }
+        if (
+            (memberAccessNode.object.type === "Literal" &&
+                typeof memberAccessNode.object.value === "string") ||
+            memberAccessNode.object.type === "TemplateLiteral"
+        ) {
+            return className === "String"
+        }
+        if (
+            memberAccessNode.object.type === "FunctionExpression" ||
+            memberAccessNode.object.type === "ArrowFunctionExpression"
+        ) {
+            return className === "Function"
+        }
+
+        // Test object type.
+        return objectTypeChecker(memberAccessNode, className)
+    }
+
+    // For performance
+    const nameMapEntries = Object.entries(nameMap)
+    if (nameMapEntries.length === 1) {
+        const [[className, methodNames]] = nameMapEntries
+        return {
+            MemberExpression(node) {
+                const propertyName = getPropertyName(node, context.getScope())
+                let objectTypeResult = undefined
+                if (
+                    methodNames.includes(propertyName) &&
+                    (objectTypeResult = checkObjectType(node, className))
+                ) {
+                    context.report({
+                        node,
+                        messageId: "forbidden",
+                        data: {
+                            name: `${className}.prototype.${propertyName}`,
+                        },
+                        ...((options &&
+                            options.createReport &&
+                            options.createReport({
+                                objectTypeResult,
+                                propertyName,
+                                node,
+                            })) ||
+                            {}),
+                    })
+                }
+            },
+        }
+    }
+
+    return {
+        MemberExpression(node) {
+            const propertyName = getPropertyName(node, context.getScope())
+            for (const [className, methodNames] of nameMapEntries) {
+                if (
+                    methodNames.includes(propertyName) &&
+                    checkObjectType(node, className)
+                ) {
+                    context.report({
+                        node,
+                        messageId: "forbidden",
+                        data: {
+                            name: `${className}.prototype.${propertyName}`,
+                        },
+                    })
+                    return
+                }
+            }
+        },
+    }
+}
+
+/**
+ * Get `aggressive` option value.
+ * @param {RuleContext} context The rule context.
+ * @returns {boolean} The gotten `aggressive` option value.
+ */
+function getAggressiveOption(context) {
+    const options = context.options[0]
+    const globalOptions = context.settings["es-x"]
+
+    if (options && typeof options.aggressive === "boolean") {
+        return options.aggressive
+    }
+    if (globalOptions && typeof globalOptions.aggressive === "boolean") {
+        return globalOptions.aggressive
+    }
+
+    return false
+}
+
+module.exports = { definePrototypeMethodHandler }
diff --git a/lib/util/define-prototype-method-handler.js b/lib/util/define-prototype-method-handler/object-type-checker-for-ts.js
similarity index 68%
rename from lib/util/define-prototype-method-handler.js
rename to lib/util/define-prototype-method-handler/object-type-checker-for-ts.js
index 386de32..d7a94a1 100644
--- a/lib/util/define-prototype-method-handler.js
+++ b/lib/util/define-prototype-method-handler/object-type-checker-for-ts.js
@@ -1,33 +1,22 @@
 "use strict"
 
-const { getPropertyName } = require("@eslint-community/eslint-utils")
-const { optionalRequire } = require("./optional-require")
-
+const { optionalRequire } = require("../optional-require")
 /** @type {import("typescript")} */
 const ts = optionalRequire(require, "typescript")
 
+module.exports = { buildObjectTypeCheckerForTS }
+
 /**
- * @typedef {object} CreateReportArgument
- * @property {true | 'aggressive'} objectTypeResult
- * @property {string} propertyName
- * @property {MemberExpression} node
- */
-/**
- * @typedef {object} Options
- * @property { (arg: CreateReportArgument) => ReportDescriptor } [Options.createReport]
+ * @typedef {import("eslint").Rule.RuleContext} RuleContext
+ * @typedef {import("estree").MemberExpression} MemberExpression
  */
-
 /**
- * Define handlers to disallow prototype methods.
+ * Build object type checker for TypeScript.
  * @param {RuleContext} context The rule context.
- * @param {Record<string, readonly string[]>} nameMap The method names to disallow. The key is class names and that value is method names.
- * @param {Options} [options] The options.
- * @returns {Record<string, (node: ASTNode) => void>} The defined handlers.
+ * @param {boolean} aggressiveResult The value to return if the type cannot be determined.
+ * @returns {((memberAccessNode: MemberExpression, className: string) => boolean | "aggressive") | null} Returns an object type checker. Returns null if TypeScript is not available.
  */
-function definePrototypeMethodHandler(context, nameMap, options) {
-    const aggressiveOption = getAggressiveOption(context)
-    const aggressiveResult = aggressiveOption ? "aggressive" : false
-
+function buildObjectTypeCheckerForTS(context, aggressiveResult) {
     /** @type {ReadonlyMap<any, import("typescript").Node>} */
     const tsNodeMap = context.parserServices.esTreeNodeToTSNodeMap
     /** @type {import("typescript").TypeChecker} */
@@ -36,45 +25,16 @@ function definePrototypeMethodHandler(context, nameMap, options) {
         context.parserServices.program.getTypeChecker()
 
     const isTS = Boolean(ts && tsNodeMap && checker)
-    const hasFullType =
-        isTS && context.parserServices.hasFullTypeInformation !== false
-
-    /**
-     * Check if the type of the given node is one of given class or not.
-     * @param {MemberExpression} memberAccessNode The MemberExpression node.
-     * @param {string} className The class name to disallow.
-     * @returns {boolean | "aggressive"} `true` if should disallow it.
-     */
-    function checkObjectType(memberAccessNode, className) {
-        // If it's obvious, shortcut.
-        if (memberAccessNode.object.type === "ArrayExpression") {
-            return className === "Array"
-        }
-        if (
-            memberAccessNode.object.type === "Literal" &&
-            memberAccessNode.object.regex
-        ) {
-            return className === "RegExp"
-        }
-        if (
-            (memberAccessNode.object.type === "Literal" &&
-                typeof memberAccessNode.object.value === "string") ||
-            memberAccessNode.object.type === "TemplateLiteral"
-        ) {
-            return className === "String"
-        }
-        if (
-            memberAccessNode.object.type === "FunctionExpression" ||
-            memberAccessNode.object.type === "ArrowFunctionExpression"
-        ) {
-            return className === "Function"
-        }
+    if (!isTS) {
+        return null
+    }
+    const hasFullType = context.parserServices.hasFullTypeInformation !== false
 
-        // Test object type.
-        return isTS
-            ? checkByPropertyDeclaration(memberAccessNode, className) ||
-                  checkByObjectExpressionType(memberAccessNode, className)
-            : aggressiveResult
+    return function (memberAccessNode, className) {
+        return (
+            checkByPropertyDeclaration(memberAccessNode, className) ||
+            checkByObjectExpressionType(memberAccessNode, className)
+        )
     }
 
     /**
@@ -237,78 +197,6 @@ function definePrototypeMethodHandler(context, nameMap, options) {
         }
         return undefined
     }
-
-    // For performance
-    const nameMapEntries = Object.entries(nameMap)
-    if (nameMapEntries.length === 1) {
-        const [[className, methodNames]] = nameMapEntries
-        return {
-            MemberExpression(node) {
-                const propertyName = getPropertyName(node, context.getScope())
-                let objectTypeResult = undefined
-                if (
-                    methodNames.includes(propertyName) &&
-                    (objectTypeResult = checkObjectType(node, className))
-                ) {
-                    context.report({
-                        node,
-                        messageId: "forbidden",
-                        data: {
-                            name: `${className}.prototype.${propertyName}`,
-                        },
-                        ...((options &&
-                            options.createReport &&
-                            options.createReport({
-                                objectTypeResult,
-                                propertyName,
-                                node,
-                            })) ||
-                            {}),
-                    })
-                }
-            },
-        }
-    }
-
-    return {
-        MemberExpression(node) {
-            const propertyName = getPropertyName(node, context.getScope())
-            for (const [className, methodNames] of nameMapEntries) {
-                if (
-                    methodNames.includes(propertyName) &&
-                    checkObjectType(node, className)
-                ) {
-                    context.report({
-                        node,
-                        messageId: "forbidden",
-                        data: {
-                            name: `${className}.prototype.${propertyName}`,
-                        },
-                    })
-                    return
-                }
-            }
-        },
-    }
-}
-
-/**
- * Get `aggressive` option value.
- * @param {RuleContext} context The rule context.
- * @returns {boolean} The gotten `aggressive` option value.
- */
-function getAggressiveOption(context) {
-    const options = context.options[0]
-    const globalOptions = context.settings["es-x"]
-
-    if (options && typeof options.aggressive === "boolean") {
-        return options.aggressive
-    }
-    if (globalOptions && typeof globalOptions.aggressive === "boolean") {
-        return globalOptions.aggressive
-    }
-
-    return false
 }
 
 /**
@@ -429,5 +317,3 @@ function isFunction(type) {
     const signatures = type.getCallSignatures()
     return signatures.length > 0
 }
-
-module.exports = { definePrototypeMethodHandler }
diff --git a/lib/util/define-prototype-method-handler/object-type-checker.js b/lib/util/define-prototype-method-handler/object-type-checker.js
new file mode 100644
index 0000000..f7a967d
--- /dev/null
+++ b/lib/util/define-prototype-method-handler/object-type-checker.js
@@ -0,0 +1,474 @@
+"use strict"
+
+const { findVariable } = require("@eslint-community/eslint-utils")
+
+module.exports = { buildObjectTypeChecker, buildExpressionTypeProvider }
+
+/**
+ * @typedef {import("eslint").Rule.RuleContext} RuleContext
+ * @typedef {import("estree").MemberExpression} MemberExpression
+ * @typedef {import("estree").Expression} Expression
+ */
+
+/**
+ * @typedef {"Array"
+ *         | "Date"
+ *         | "Function"
+ *         | "Intl.Collator"
+ *         | "Intl.DateTimeFormat"
+ *         | "Intl.ListFormat"
+ *         | "Intl.NumberFormat"
+ *         | "Intl.PluralRules"
+ *         | "Intl.RelativeTimeFormat"
+ *         | "Intl.Segmenter"
+ *         | "Promise"
+ *         | "RegExp"
+ *         | "String"
+ *         | "Symbol"
+ *         | "Int8Array"
+ *         | "Uint8Array"
+ *         | "Uint8ClampedArray"
+ *         | "Int16Array"
+ *         | "Uint16Array"
+ *         | "Int32Array"
+ *         | "Uint32Array"
+ *         | "Float32Array"
+ *         | "Float64Array"
+ *         | "BigInt64Array"
+ *         | "BigUint64Array"
+ *         | "Object"
+ *         | "Number"
+ *         | "Boolean"
+ *         | "BigInt"
+ *         | "null"
+ *         | "undefined"
+ *  } TypeName
+ */
+
+/**
+ * @typedef {Record<
+ *   string,
+ *     WellKnownGlobalFunction
+ *   | WellKnownGlobalObject
+ *   | { type: "undefined" }
+ *   | { type: "Number" }
+ *   | undefined>
+ * } WellKnownGlobals
+ */
+/**
+ * @typedef {object} WellKnownGlobalFunction
+ * @property {"Function"} type
+ * @property {TypeName} returnType
+ */
+/**
+ * @typedef {object} WellKnownGlobalObject
+ * @property {"Object"} type
+ * @property {WellKnownGlobals} properties
+ */
+
+/** @type {WellKnownGlobals} */
+const WELLKNOWN_GLOBALS = {
+    String: { type: "Function", returnType: "String" },
+    Number: { type: "Function", returnType: "Number" },
+    Boolean: { type: "Function", returnType: "Boolean" },
+    Symbol: { type: "Function", returnType: "Symbol" },
+    BigInt: { type: "Function", returnType: "BigInt" },
+    Object: { type: "Function", returnType: "Object" },
+    Function: { type: "Function", returnType: "Function" },
+    Array: { type: "Function", returnType: "Array" },
+    RegExp: { type: "Function", returnType: "RegExp" },
+    Date: { type: "Function", returnType: "Date" },
+    Promise: { type: "Function", returnType: "Promise" },
+    Int8Array: { type: "Function", returnType: "Int8Array" },
+    Uint8Array: { type: "Function", returnType: "Uint8Array" },
+    Uint8ClampedArray: { type: "Function", returnType: "Uint8ClampedArray" },
+    Int16Array: { type: "Function", returnType: "Int16Array" },
+    Uint16Array: { type: "Function", returnType: "Uint16Array" },
+    Int32Array: { type: "Function", returnType: "Int32Array" },
+    Uint32Array: { type: "Function", returnType: "Uint32Array" },
+    Float32Array: { type: "Function", returnType: "Float32Array" },
+    Float64Array: { type: "Function", returnType: "Float64Array" },
+    BigInt64Array: { type: "Function", returnType: "BigInt64Array" },
+    BigUint64Array: { type: "Function", returnType: "BigUint64Array" },
+    Intl: {
+        type: "Object",
+        properties: {
+            Collator: { type: "Function", returnType: "Intl.Collator" },
+            DateTimeFormat: {
+                type: "Function",
+                returnType: "Intl.DateTimeFormat",
+            },
+            ListFormat: {
+                type: "Function",
+                returnType: "Intl.ListFormat",
+            },
+            NumberFormat: {
+                type: "Function",
+                returnType: "Intl.NumberFormat",
+            },
+            PluralRules: {
+                type: "Function",
+                returnType: "Intl.PluralRules",
+            },
+            RelativeTimeFormat: {
+                type: "Function",
+                returnType: "Intl.RelativeTimeFormat",
+            },
+            Segmenter: { type: "Function", returnType: "Intl.Segmenter" },
+        },
+    },
+    undefined: { type: "undefined" },
+    NaN: { type: "Number" },
+    Infinity: { type: "Number" },
+}
+
+/**
+ * Build object type checker.
+ * @param {RuleContext} context The rule context.
+ * @param {boolean} aggressiveResult The value to return if the type cannot be determined.
+ * @returns {((memberAccessNode: MemberExpression, className: string) => boolean)} Returns an object type checker.
+ */
+function buildObjectTypeChecker(context, aggressiveResult) {
+    const getType = buildExpressionTypeProvider(context)
+    return function (memberAccessNode, className) {
+        const type = getType(memberAccessNode.object)
+        if (type == null) {
+            return aggressiveResult
+        }
+        return type === className
+    }
+}
+
+const cache = new WeakMap()
+
+/**
+ * Build expression type provider.
+ * @param {RuleContext} context The rule context.
+ * @returns {((node: Expression) => TypeName | null)} Returns an expression type provider.
+ */
+function buildExpressionTypeProvider(context) {
+    const key = context.getSourceCode().ast
+    let result = cache.get(key)
+    if (!result) {
+        cache.set(key, (result = buildExpressionTypeProviderImpl(context)))
+    }
+    return result
+}
+
+/**
+ * Build expression type provider.
+ * @param {RuleContext} context The rule context.
+ * @returns {((node: Expression) => TypeName | null)} Returns an expression type provider.
+ */
+function buildExpressionTypeProviderImpl(context) {
+    /** @type {Record<Expression['type'], (node: Expression) => TypeName | null>} */
+    const GET_TYPES = {
+        ArrayExpression: () => "Array",
+        ObjectExpression: () => "Object",
+        ArrowFunctionExpression: () => "Function",
+        FunctionExpression: () => "Function",
+        Literal: getLiteralType,
+        TemplateLiteral: () => "String",
+        Identifier: getIdentifierType,
+        /** @param {import("estree").BinaryExpression} node */
+        BinaryExpression: (node) =>
+            getOperatorType(node.operator, node.left, node.right),
+        /** @param {import("estree").LogicalExpression} node */
+        LogicalExpression: (node) =>
+            getOperatorType(node.operator, node.left, node.right),
+        /** @param {import("estree").AssignmentExpression} node */
+        AssignmentExpression: (node) =>
+            getOperatorType(node.operator, node.left, node.right),
+        UnaryExpression: getUnaryExpressionType,
+        UpdateExpression: () => "Number",
+        ClassExpression: () => "Function",
+        ChainExpression: (node) => getType(node.expression),
+        /** @param {import("estree").SequenceExpression} node */
+        SequenceExpression: (node) =>
+            getType(node.expressions[node.expressions.length - 1]),
+        CallExpression: getCallExpressionType,
+        NewExpression: getCallExpressionType,
+        TaggedTemplateExpression: getCallExpressionType,
+        /** @param {import("estree").ConditionalExpression} node */
+        ConditionalExpression(node) {
+            const consequent = getType(node.consequent)
+            const alternate = getType(node.alternate)
+            return consequent === alternate ? consequent : null
+        },
+    }
+
+    const tracked = new Map()
+    return getType
+
+    /**
+     * Gets the type name of the given node.
+     * @param {Expression} node The Expression node
+     * @returns {TypeName | null} The type name of expression.
+     */
+    function getType(node) {
+        if (tracked.has(node)) {
+            return tracked.get(node)
+        }
+        tracked.set(node, null)
+        try {
+            const result = GET_TYPES[node.type]?.(node) ?? null
+            tracked.set(node, result)
+            return result
+        } catch {
+            return null
+        }
+    }
+
+    /**
+     * @param {import("estree").Identifier} node
+     * @returns {import("eslint").Scope.Variable | null}
+     */
+    function findVariableFromIdentifier(node) {
+        return findVariable(
+            context.getSourceCode().scopeManager.globalScope,
+            node,
+        )
+    }
+
+    /**
+     * @param {import("estree").Literal} node
+     * @returns {TypeName | null}
+     */
+    function getLiteralType(node) {
+        if (node.regex) {
+            return "RegExp"
+        }
+        if (node.bigint) {
+            return "BigInt"
+        }
+        if (node.value == null) {
+            return "null"
+        }
+        const valueType = typeof node.value
+        if (valueType === "string") {
+            return "String"
+        }
+        if (valueType === "number") {
+            return "Number"
+        }
+        if (valueType === "boolean") {
+            return "Boolean"
+        }
+        return valueType[0].toUpperCase() + valueType.slice(1)
+    }
+
+    /**
+     * @param {import("estree").Identifier} node
+     * @returns {TypeName | null}
+     */
+    function getIdentifierType(node) {
+        const variable = findVariableFromIdentifier(node)
+        if (variable) {
+            if (variable.defs.length === 0) {
+                // It is a global variable
+                return WELLKNOWN_GLOBALS[node.name]?.type ?? null
+            } else if (variable.defs.length === 1) {
+                const def = variable.defs[0]
+                if (def.type === "Variable") {
+                    if (
+                        // It has an initial value.
+                        def.node.init &&
+                        // It does not write new values.
+                        (def.parent.kind === "const" ||
+                            variable.references.every(
+                                (ref) =>
+                                    ref.isReadOnly() ||
+                                    ref.identifier === def.name,
+                            ))
+                    ) {
+                        // The type of the initial value is the type of the variable.
+                        return getType(def.node.init)
+                    }
+                } else if (def.type === "FunctionName") {
+                    return "Function"
+                }
+            }
+        }
+        return null
+    }
+
+    /**
+     * @param {import("estree").BinaryOperator
+     *   | import("estree").LogicalOperator
+     *   | import("estree").AssignmentOperator} operator
+     * @param {import("estree").Expression} leftNode
+     * @param {import("estree").Expression} rightNode
+     * @returns {TypeName | null}
+     */
+    // eslint-disable-next-line complexity
+    function getOperatorType(operator, leftNode, rightNode) {
+        if (operator === "=") {
+            return getType(rightNode)
+        }
+        if (operator === "+" || operator === "+=") {
+            return getPlusOperatorType(leftNode, rightNode)
+        }
+        if (
+            operator === "==" ||
+            operator === "!=" ||
+            operator === "===" ||
+            operator === "!==" ||
+            operator === "<" ||
+            operator === "<=" ||
+            operator === ">" ||
+            operator === ">=" ||
+            operator === "in" ||
+            operator === "instanceof"
+        ) {
+            return "Boolean"
+        }
+        if (
+            operator === "-" ||
+            operator === "-=" ||
+            operator === "*" ||
+            operator === "*=" ||
+            operator === "/" ||
+            operator === "/=" ||
+            operator === "%" ||
+            operator === "%=" ||
+            operator === "^" ||
+            operator === "^=" ||
+            operator === "**" ||
+            operator === "**=" ||
+            operator === "&" ||
+            operator === "&=" ||
+            operator === "|" ||
+            operator === "|="
+        ) {
+            const left = getType(leftNode)
+            const right = getType(rightNode)
+            if (left === "BigInt" || right === "BigInt") {
+                return "BigInt"
+            }
+            return left == null && right == null ? null : "Number"
+        }
+        if (
+            operator === "<<" ||
+            operator === "<<=" ||
+            operator === ">>" ||
+            operator === ">>=" ||
+            operator === ">>>" ||
+            operator === ">>>="
+        ) {
+            return "Number"
+        }
+        if (
+            operator === "&&" ||
+            operator === "&&=" ||
+            operator === "||" ||
+            operator === "||=" ||
+            operator === "??" ||
+            operator === "??="
+        ) {
+            const left = getType(leftNode)
+            const right = getType(rightNode)
+            return left === right ? left : null
+        }
+        return null
+    }
+
+    /**
+     * @param {import("estree").Expression} leftNode
+     * @param {import("estree").Expression} rightNode
+     * @returns {TypeName | null}
+     */
+    function getPlusOperatorType(leftNode, rightNode) {
+        const left = getType(leftNode)
+        const right = getType(rightNode)
+        if (left === "String" || right === "String") {
+            return "String"
+        }
+        if (left === "BigInt" || right === "BigInt") {
+            return "BigInt"
+        }
+        if (right === "Number") {
+            return "Number"
+        }
+        if (left === "Number") {
+            if (right === "null" || right === "undefined") {
+                return "Number"
+            }
+        }
+        if (right == null) {
+            return null
+        }
+        return "String"
+    }
+
+    /**
+     * @param {import("estree").UnaryExpression} node
+     * @returns {TypeName | null}
+     */
+    function getUnaryExpressionType(node) {
+        if (node.operator === "!" || node.operator === "delete") {
+            return "Boolean"
+        }
+        if (node.operator === "+") {
+            return "Number"
+        }
+        if (node.operator === "-" || node.operator === "~") {
+            const argument = getType(node.argument)
+            if (argument === "BigInt") {
+                return argument
+            }
+            return argument == null ? null : "Number"
+        }
+        if (node.operator === "typeof") {
+            return "String"
+        }
+        if (node.operator === "void") {
+            return "undefined"
+        }
+        return null
+    }
+
+    /**
+     * @param {import("estree").SimpleCallExpression | import("estree").NewExpression | import("estree").TaggedTemplateExpression} node
+     * @returns {TypeName | null}
+     */
+
+    function getCallExpressionType(node) {
+        const callee =
+            node.type === "CallExpression" || node.type === "NewExpression"
+                ? node.callee
+                : node.tag
+        if (callee.type === "Identifier") {
+            if (!isGlobalObject(callee)) {
+                return null
+            }
+            const obj = WELLKNOWN_GLOBALS[callee.name]
+            return obj?.type === "Function" ? obj.returnType : null
+        } else if (callee.type === "MemberExpression") {
+            if (
+                callee.computed ||
+                callee.property.type !== "Identifier" ||
+                !isGlobalObject(callee.object)
+            ) {
+                return null
+            }
+            const obj = WELLKNOWN_GLOBALS[callee.object.name]
+            if (obj?.type === "Object") {
+                const prop = obj.properties[callee.property.name]
+                return prop?.type === "Function" ? prop.returnType : null
+            }
+        }
+        return null
+    }
+
+    /**
+     * @param {import("estree").Node} node
+     * @returns {node is import("estree").Identifier}
+     */
+    function isGlobalObject(node) {
+        if (node.type !== "Identifier") {
+            return false
+        }
+        const variable = findVariableFromIdentifier(node)
+        return Boolean(variable && variable.defs.length === 0)
+    }
+}


Test patch:
diff --git a/tests/lib/rules/no-symbol-prototype-description.js b/tests/lib/rules/no-symbol-prototype-description.js
index 981552c..d74b2e6 100644
--- a/tests/lib/rules/no-symbol-prototype-description.js
+++ b/tests/lib/rules/no-symbol-prototype-description.js
@@ -13,7 +13,6 @@ new RuleTester().run(ruleId, rule, {
     valid: [
         "foo.description",
         "Symbol.iterator.description",
-        "Symbol().description",
         {
             code: "foo.description",
             options: [{ aggressive: false }],
@@ -21,6 +20,12 @@ new RuleTester().run(ruleId, rule, {
         },
     ],
     invalid: [
+        {
+            code: "Symbol().description",
+            errors: [
+                "ES2019 'Symbol.prototype.description' property is forbidden.",
+            ],
+        },
         {
             code: "foo.description",
             errors: [
diff --git a/tests/lib/util/define-prototype-method-handler/object-type-checker.js b/tests/lib/util/define-prototype-method-handler/object-type-checker.js
new file mode 100644
index 0000000..26f64d8
--- /dev/null
+++ b/tests/lib/util/define-prototype-method-handler/object-type-checker.js
@@ -0,0 +1,465 @@
+"use strict"
+
+const RuleTester = require("../../../tester")
+const { Linter } = require("eslint")
+const { deepStrictEqual } = require("assert")
+const {
+    buildExpressionTypeProvider,
+} = require("../../../../lib/util/define-prototype-method-handler/object-type-checker")
+
+// -----------------------------------------------------------------------------
+// Tests
+// -----------------------------------------------------------------------------
+
+if (!RuleTester.isSupported(2022)) {
+    //eslint-disable-next-line no-console
+    console.log(
+        "Skip the tests of define-prototype-method-handler/object-type-checker.",
+    )
+    return
+}
+
+describe("define-prototype-method-handler/object-type-checker", () => {
+    describe("buildExpressionTypeProvider", () => {
+        for (const { code, result } of [
+            {
+                code: "target('foo');",
+                result: ["String"],
+            },
+            {
+                code: "target(42);",
+                result: ["Number"],
+            },
+            {
+                code: "target(true);",
+                result: ["Boolean"],
+            },
+            {
+                code: "target(/foo/);",
+                result: ["RegExp"],
+            },
+            {
+                code: "target(null);",
+                result: ["null"],
+            },
+            {
+                code: "target(42n);",
+                result: ["BigInt"],
+            },
+            {
+                code: "target([]);",
+                result: ["Array"],
+            },
+            {
+                code: "target({});",
+                result: ["Object"],
+            },
+            {
+                code: "target(function(){});",
+                result: ["Function"],
+            },
+            {
+                code: "target(()=>{});",
+                result: ["Function"],
+            },
+            {
+                code: "target(a);",
+                result: [null],
+            },
+            {
+                code: "target(NaN);",
+                result: ["Number"],
+            },
+            {
+                code: "target(Infinity);",
+                result: ["Number"],
+            },
+            {
+                code: "target(undefined);",
+                result: ["undefined"],
+            },
+            {
+                code: `
+                const a = 'i'
+                target(a);`,
+                result: ["String"],
+            },
+            {
+                code: `
+                function f(){}
+                target(f);`,
+                result: ["Function"],
+            },
+            {
+                code: `
+                const a = b
+                target(a);`,
+                result: [null],
+            },
+            {
+                code: `
+                const a = a
+                target(a);`,
+                result: [null],
+            },
+            {
+                code: `
+                var a = 'foo'
+                var a = 42
+                target(a);`,
+                result: [null],
+            },
+            {
+                code: `
+                var a = 'foo'
+                a = 42
+                var b = 'bar'
+                target(a, b);`,
+                result: [null, "String"],
+            },
+            {
+                code: "target(`foo`);",
+                result: ["String"],
+            },
+            {
+                // eslint-disable-next-line no-template-curly-in-string
+                code: "target(`foo${a}`);",
+                result: ["String"],
+            },
+            {
+                code: `
+                const a = 'i'
+                target('foo' + 'bar');
+                target(a + 'foo');
+                target('foo' + a + 'bar');
+            `,
+                result: ["String", "String", "String"],
+            },
+            {
+                code: `
+                const a = 'i'
+                target(b + 'bar');
+                target('foo' + a + b);
+                `,
+                result: ["String", "String"],
+            },
+            {
+                code: `
+                const a = 42
+                target(
+                    a + 'bar',
+                    foo + 'bar',
+                    'bar' + foo,
+                    42 + a,
+                    {} + 42,
+                    42 + null,
+                    42 + {},
+                    42n + foo,
+                    42 + foo,
+                );
+                `,
+                result: [
+                    "String",
+                    "String",
+                    "String",
+                    "Number",
+                    "Number",
+                    "Number",
+                    "String",
+                    "BigInt",
+                    null,
+                ],
+            },
+            {
+                code: `
+                let b = 42
+                target(
+                    b += 'bar',
+                );
+                `,
+                result: ["String"],
+            },
+            {
+                code: `
+                let b = 42
+                target(
+                    b = 'bar',
+                );
+                `,
+                result: ["String"],
+            },
+            ...[
+                "==",
+                "!=",
+                "===",
+                "!==",
+                "<",
+                "<=",
+                ">",
+                ">=",
+                "in",
+                "instanceof",
+            ].map((op) => ({
+                code: `
+                target(
+                    foo ${op} bar,
+                );
+                `,
+                result: ["Boolean"],
+            })),
+            ...["-", "*", "/", "%", "^", "**", "&", "|"].map((op) => ({
+                code: `
+                target(
+                    foo ${op} bar,
+                    42 ${op} bar,
+                    foo ${op} 42,
+                    42n ${op} bar,
+                    foo ${op} 42n,
+                );
+                `,
+                result: [null, "Number", "Number", "BigInt", "BigInt"],
+            })),
+            ...["-", "*", "/", "%", "^", "**", "&", "|"].map((op) => ({
+                code: `
+                target(
+                    foo ${op}= bar,
+                    foo ${op}= 42,
+                    foo ${op}= 42n,
+                );
+                `,
+                result: [null, "Number", "BigInt"],
+            })),
+            ...["&&", "||", "??"].map((op) => ({
+                code: `
+                    const a = 1
+                    const b = 1
+                    const c = 1n
+                    const d = 1n
+                    target(
+                        a ${op} b,
+                        c ${op} d,
+                        foo ${op} bar,
+                        42 ${op} bar,
+                        foo ${op} 42,
+                        42n ${op} bar,
+                        foo ${op} 42n,
+                    );
+                    `,
+                result: ["Number", "BigInt", null, null, null, null, null],
+            })),
+            ...["&&", "||", "??"].map((op) => ({
+                code: `
+                const a = 1
+                const b = 1
+                const c = 1n
+                const d = 1n
+                target(
+                    a ${op}= b,
+                    c ${op}= d,
+                    foo ${op}= bar,
+                    foo ${op}= 42,
+                    foo ${op}= 42n,
+                );
+                `,
+                result: ["Number", "BigInt", null, null, null],
+            })),
+            ...["<<", ">>", ">>>"].map((op) => ({
+                code: `
+                target(
+                    foo ${op} bar,
+                );
+                `,
+                result: ["Number"],
+            })),
+            ...["<<", ">>", ">>>"].map((op) => ({
+                code: `
+                target(
+                    foo ${op}= bar,
+                );
+                `,
+                result: ["Number"],
+            })),
+            {
+                code: `
+                target(
+                    !foo,
+                    delete foo.bar
+                );
+                `,
+                result: ["Boolean", "Boolean"],
+            },
+            {
+                code: `
+                target(
+                    +bar,
+                );
+                `,
+                result: ["Number"],
+            },
+            ...["-", "~"].map((op) => ({
+                code: `
+                target(
+                    ${op}foo,
+                    ${op}42,
+                    ${op}42n,
+                );
+                `,
+                result: [null, "Number", "BigInt"],
+            })),
+            {
+                code: `
+                target(
+                    typeof bar,
+                    void bar,
+                );
+                `,
+                result: ["String", "undefined"],
+            },
+            {
+                code: `
+                target(
+                    (foo,'bar'),
+                );
+                `,
+                result: ["String"],
+            },
+            {
+                code: `
+                target(
+                    class Foo {},
+                );
+                `,
+                result: ["Function"],
+            },
+            ...[
+                "String",
+                "Number",
+                "BigInt",
+                "Boolean",
+                "Symbol",
+                "Object",
+                "Array",
+                "Function",
+                "RegExp",
+                "Date",
+            ].map((t) => ({
+                code: `
+                target(
+                    ${t}(),
+                    ${t}\`\`,
+                );
+                `,
+                result: [t, t],
+            })),
+            ...[
+                "String",
+                "Number",
+                "BigInt",
+                "Boolean",
+                "Symbol",
+                "Object",
+                "Array",
+                "Function",
+                "RegExp",
+                "Date",
+                "Promise",
+                "Intl.Collator",
+                "Intl.DateTimeFormat",
+                "Intl.ListFormat",
+                "Intl.NumberFormat",
+                "Intl.PluralRules",
+                "Intl.RelativeTimeFormat",
+                "Intl.Segmenter",
+                "Int8Array",
+                "Uint8Array",
+                "Uint8ClampedArray",
+                "Int16Array",
+                "Uint16Array",
+                "Int32Array",
+                "Uint32Array",
+                "Float32Array",
+                "Float64Array",
+                "BigInt64Array",
+                "BigUint64Array",
+            ].map((t) => ({
+                code: `
+                target(
+                    new ${t}(),
+                );
+                `,
+                result: [t],
+            })),
+            {
+                code: `
+                target(
+                    a ? 'a' : 'b',
+                    a ? 'a' : 42,
+                );
+                `,
+                result: ["String", null],
+            },
+            {
+                code: `
+                target(
+                    a++,
+                    --a,
+                );
+                `,
+                result: ["Number", "Number"],
+            },
+        ]) {
+            it(code, () => {
+                deepStrictEqual(
+                    getResultOfBuildExpressionTypeProvider(code),
+                    result,
+                )
+            })
+        }
+    })
+})
+
+function getResultOfBuildExpressionTypeProvider(code) {
+    const linter = new Linter()
+
+    const result = []
+    linter.defineRule("test-rule", {
+        create(context) {
+            const getType = buildExpressionTypeProvider(context)
+            return {
+                "CallExpression[callee.name = target]"(node) {
+                    result.push(...node.arguments.map(getType))
+                },
+            }
+        },
+    })
+
+    const linterResult = linter.verify(code, {
+        parserOptions: {
+            ecmaVersion: 2022,
+            sourceType: "module",
+        },
+        globals: {
+            String: "readonly",
+            Number: "readonly",
+            Boolean: "readonly",
+            Symbol: "readonly",
+            Promise: "readonly",
+            BigInt: "readonly",
+            Intl: "readonly",
+            Int8Array: "readonly",
+            Uint8Array: "readonly",
+            Uint8ClampedArray: "readonly",
+            Int16Array: "readonly",
+            Uint16Array: "readonly",
+            Int32Array: "readonly",
+            Uint32Array: "readonly",
+            Float32Array: "readonly",
+            Float64Array: "readonly",
+            BigInt64Array: "readonly",
+            BigUint64Array: "readonly",
+        },
+        rules: { "test-rule": "warn" },
+    })
+    deepStrictEqual(linterResult, [])
+    return result
+}