244 lines
9.3 KiB
JavaScript
244 lines
9.3 KiB
JavaScript
/**
|
|
* @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js)
|
|
* @author Vincent Lemeunier
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const astUtils = require("./utils/ast-utils");
|
|
|
|
// Maximum array length by the ECMAScript Specification.
|
|
const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Rule Definition
|
|
//------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Convert the value to bigint if it's a string. Otherwise return the value as-is.
|
|
* @param {bigint|number|string} x The value to normalize.
|
|
* @returns {bigint|number} The normalized value.
|
|
*/
|
|
function normalizeIgnoreValue(x) {
|
|
if (typeof x === "string") {
|
|
return BigInt(x.slice(0, -1));
|
|
}
|
|
return x;
|
|
}
|
|
|
|
/** @type {import('../shared/types').Rule} */
|
|
module.exports = {
|
|
meta: {
|
|
type: "suggestion",
|
|
|
|
docs: {
|
|
description: "Disallow magic numbers",
|
|
recommended: false,
|
|
url: "https://eslint.org/docs/latest/rules/no-magic-numbers"
|
|
},
|
|
|
|
schema: [{
|
|
type: "object",
|
|
properties: {
|
|
detectObjects: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
enforceConst: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
ignore: {
|
|
type: "array",
|
|
items: {
|
|
anyOf: [
|
|
{ type: "number" },
|
|
{ type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" }
|
|
]
|
|
},
|
|
uniqueItems: true
|
|
},
|
|
ignoreArrayIndexes: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
ignoreDefaultValues: {
|
|
type: "boolean",
|
|
default: false
|
|
},
|
|
ignoreClassFieldInitialValues: {
|
|
type: "boolean",
|
|
default: false
|
|
}
|
|
},
|
|
additionalProperties: false
|
|
}],
|
|
|
|
messages: {
|
|
useConst: "Number constants declarations must use 'const'.",
|
|
noMagic: "No magic number: {{raw}}."
|
|
}
|
|
},
|
|
|
|
create(context) {
|
|
const config = context.options[0] || {},
|
|
detectObjects = !!config.detectObjects,
|
|
enforceConst = !!config.enforceConst,
|
|
ignore = new Set((config.ignore || []).map(normalizeIgnoreValue)),
|
|
ignoreArrayIndexes = !!config.ignoreArrayIndexes,
|
|
ignoreDefaultValues = !!config.ignoreDefaultValues,
|
|
ignoreClassFieldInitialValues = !!config.ignoreClassFieldInitialValues;
|
|
|
|
const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
|
|
|
|
/**
|
|
* Returns whether the rule is configured to ignore the given value
|
|
* @param {bigint|number} value The value to check
|
|
* @returns {boolean} true if the value is ignored
|
|
*/
|
|
function isIgnoredValue(value) {
|
|
return ignore.has(value);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the number is a default value assignment.
|
|
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
|
* @returns {boolean} true if the number is a default value
|
|
*/
|
|
function isDefaultValue(fullNumberNode) {
|
|
const parent = fullNumberNode.parent;
|
|
|
|
return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the number is the initial value of a class field.
|
|
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
|
* @returns {boolean} true if the number is the initial value of a class field.
|
|
*/
|
|
function isClassFieldInitialValue(fullNumberNode) {
|
|
const parent = fullNumberNode.parent;
|
|
|
|
return parent.type === "PropertyDefinition" && parent.value === fullNumberNode;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
|
|
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
|
* @returns {boolean} true if the node is radix
|
|
*/
|
|
function isParseIntRadix(fullNumberNode) {
|
|
const parent = fullNumberNode.parent;
|
|
|
|
return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] &&
|
|
(
|
|
astUtils.isSpecificId(parent.callee, "parseInt") ||
|
|
astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given node is a direct child of a JSX node.
|
|
* In particular, it aims to detect numbers used as prop values in JSX tags.
|
|
* Example: <input maxLength={10} />
|
|
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
|
* @returns {boolean} true if the node is a JSX number
|
|
*/
|
|
function isJSXNumber(fullNumberNode) {
|
|
return fullNumberNode.parent.type.indexOf("JSX") === 0;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the given node is used as an array index.
|
|
* Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294".
|
|
*
|
|
* All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties,
|
|
* which can be created and accessed on an array in addition to the array index properties,
|
|
* but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc.
|
|
*
|
|
* The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
|
|
* thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
|
|
*
|
|
* All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
|
|
*
|
|
* Valid examples:
|
|
* a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n]
|
|
* a[-0] (same as a[0] because -0 coerces to "0")
|
|
* a[-0n] (-0n evaluates to 0n)
|
|
*
|
|
* Invalid examples:
|
|
* a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1]
|
|
* a[4294967295] (above the max index, it's an access to a regular property a["4294967295"])
|
|
* a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"])
|
|
* a[1e310] (same as a["Infinity"])
|
|
* @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
|
|
* @param {bigint|number} value Value expressed by the fullNumberNode
|
|
* @returns {boolean} true if the node is a valid array index
|
|
*/
|
|
function isArrayIndex(fullNumberNode, value) {
|
|
const parent = fullNumberNode.parent;
|
|
|
|
return parent.type === "MemberExpression" && parent.property === fullNumberNode &&
|
|
(Number.isInteger(value) || typeof value === "bigint") &&
|
|
value >= 0 && value < MAX_ARRAY_LENGTH;
|
|
}
|
|
|
|
return {
|
|
Literal(node) {
|
|
if (!astUtils.isNumericLiteral(node)) {
|
|
return;
|
|
}
|
|
|
|
let fullNumberNode;
|
|
let value;
|
|
let raw;
|
|
|
|
// Treat unary minus as a part of the number
|
|
if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
|
|
fullNumberNode = node.parent;
|
|
value = -node.value;
|
|
raw = `-${node.raw}`;
|
|
} else {
|
|
fullNumberNode = node;
|
|
value = node.value;
|
|
raw = node.raw;
|
|
}
|
|
|
|
const parent = fullNumberNode.parent;
|
|
|
|
// Always allow radix arguments and JSX props
|
|
if (
|
|
isIgnoredValue(value) ||
|
|
(ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
|
|
(ignoreClassFieldInitialValues && isClassFieldInitialValue(fullNumberNode)) ||
|
|
isParseIntRadix(fullNumberNode) ||
|
|
isJSXNumber(fullNumberNode) ||
|
|
(ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (parent.type === "VariableDeclarator") {
|
|
if (enforceConst && parent.parent.kind !== "const") {
|
|
context.report({
|
|
node: fullNumberNode,
|
|
messageId: "useConst"
|
|
});
|
|
}
|
|
} else if (
|
|
!okTypes.includes(parent.type) ||
|
|
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
|
|
) {
|
|
context.report({
|
|
node: fullNumberNode,
|
|
messageId: "noMagic",
|
|
data: {
|
|
raw
|
|
}
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|