'use strict'; const {getStaticValue} = require('@eslint-community/eslint-utils'); const {isNumberLiteral} = require('../ast/index.js'); const isStaticProperties = (node, object, properties) => node.type === 'MemberExpression' && !node.computed && !node.optional && node.object.type === 'Identifier' && node.object.name === object && node.property.type === 'Identifier' && properties.has(node.property.name); const isFunctionCall = (node, functionName) => node.type === 'CallExpression' && !node.optional && node.callee.type === 'Identifier' && node.callee.name === functionName; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#static_properties const mathProperties = new Set([ 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2', ]); // `Math.{E,LN2,LN10,LOG2E,LOG10E,PI,SQRT1_2,SQRT2}` const isMathProperty = node => isStaticProperties(node, 'Math', mathProperties); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#static_methods const mathMethods = new Set([ 'abs', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh', 'atan2', 'cbrt', 'ceil', 'clz32', 'cos', 'cosh', 'exp', 'expm1', 'floor', 'fround', 'hypot', 'imul', 'log', 'log1p', 'log10', 'log2', 'max', 'min', 'pow', 'random', 'round', 'sign', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc', ]); // `Math.{abs, …, trunc}(…)` const isMathMethodCall = node => node.type === 'CallExpression' && !node.optional && isStaticProperties(node.callee, 'Math', mathMethods); // `Number(…)` const isNumberCall = node => isFunctionCall(node, 'Number'); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#static_properties const numberProperties = new Set([ 'EPSILON', 'MAX_SAFE_INTEGER', 'MAX_VALUE', 'MIN_SAFE_INTEGER', 'MIN_VALUE', 'NaN', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY', ]); const isNumberProperty = node => isStaticProperties(node, 'Number', numberProperties); // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#static_methods const numberMethods = new Set([ 'parseFloat', 'parseInt', ]); const isNumberMethodCall = node => node.type === 'CallExpression' && !node.optional && isStaticProperties(node.callee, 'Number', numberMethods); const isGlobalParseToNumberFunctionCall = node => isFunctionCall(node, 'parseInt') || isFunctionCall(node, 'parseFloat'); const isStaticNumber = (node, scope) => typeof getStaticValue(node, scope)?.value === 'number'; const isLengthProperty = node => node.type === 'MemberExpression' && !node.computed && !node.optional && node.property.type === 'Identifier' && node.property.name === 'length'; // `+` and `>>>` operators are handled separately const mathOperators = new Set(['-', '*', '/', '%', '**', '<<', '>>', '|', '^', '&']); function isNumber(node, scope) { if ( isNumberLiteral(node) || isMathProperty(node) || isMathMethodCall(node) || isNumberCall(node) || isNumberProperty(node) || isNumberMethodCall(node) || isGlobalParseToNumberFunctionCall(node) || isLengthProperty(node) ) { return true; } switch (node.type) { case 'AssignmentExpression': { const {operator} = node; if (operator === '=' && isNumber(node.right, scope)) { return true; } // Fall through } case 'BinaryExpression': { let {operator} = node; if (node.type === 'AssignmentExpression') { operator = operator.slice(0, -1); } if (operator === '+' && isNumber(node.left, scope) && isNumber(node.right, scope)) { return true; } // `>>>` (zero-fill right shift) can't use on `BigInt` // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#operators if (operator === '>>>') { return true; } // `a * b` can be `BigInt`, we need make sure at least one side is number if (mathOperators.has(operator) && (isNumber(node.left, scope) || isNumber(node.right, scope))) { return true; } break; } case 'UnaryExpression': { const {operator} = node; // `+` can't use on `BigInt` // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt#operators if (operator === '+') { return true; } if ((operator === '-' || operator === '~') && isNumber(node.argument, scope)) { return true; } break; } case 'UpdateExpression': { if (isNumber(node.argument, scope)) { return true; } break; } case 'ConditionalExpression': { const isConsequentNumber = isNumber(node.consequent, scope); const isAlternateNumber = isNumber(node.alternate, scope); if (isConsequentNumber && isAlternateNumber) { return true; } const testStaticValueResult = getStaticValue(node.test, scope); if ( testStaticValueResult !== null && ( (testStaticValueResult.value && isConsequentNumber) || (!testStaticValueResult.value && isAlternateNumber) ) ) { return true; } break; } case 'SequenceExpression': { if (isNumber(node.expressions[node.expressions.length - 1], scope)) { return true; } break; } // No default } return isStaticNumber(node, scope); } module.exports = isNumber;