"use strict" const { findVariable } = require("@eslint-community/eslint-utils") const { getSourceCode } = require("eslint-compat-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" }, ArrayBuffer: { type: "Function", returnType: "ArrayBuffer" }, SharedArrayBuffer: { type: "Function", returnType: "SharedArrayBuffer" }, 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 = getSourceCode(context).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 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( getSourceCode(context).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) } }