blob: 2c6ea61e28331582d72514738902d0827be81f33 [file] [log] [blame]
/**
* @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js)
* @author Vincent Lemeunier
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow magic numbers",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/no-magic-numbers"
},
schema: [{
type: "object",
properties: {
detectObjects: {
type: "boolean",
default: false
},
enforceConst: {
type: "boolean",
default: false
},
ignore: {
type: "array",
items: {
type: "number"
},
uniqueItems: true
},
ignoreArrayIndexes: {
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 = config.ignore || [],
ignoreArrayIndexes = !!config.ignoreArrayIndexes;
/**
* Returns whether the node is number literal
* @param {Node} node - the node literal being evaluated
* @returns {boolean} true if the node is a number literal
*/
function isNumber(node) {
return typeof node.value === "number";
}
/**
* Returns whether the number should be ignored
* @param {number} num - the number
* @returns {boolean} true if the number should be ignored
*/
function shouldIgnoreNumber(num) {
return ignore.indexOf(num) !== -1;
}
/**
* Returns whether the number should be ignored when used as a radix within parseInt() or Number.parseInt()
* @param {ASTNode} parent - the non-"UnaryExpression" parent
* @param {ASTNode} node - the node literal being evaluated
* @returns {boolean} true if the number should be ignored
*/
function shouldIgnoreParseInt(parent, node) {
return parent.type === "CallExpression" && node === parent.arguments[1] &&
(parent.callee.name === "parseInt" ||
parent.callee.type === "MemberExpression" &&
parent.callee.object.name === "Number" &&
parent.callee.property.name === "parseInt");
}
/**
* Returns whether the number should be ignored when used to define a JSX prop
* @param {ASTNode} parent - the non-"UnaryExpression" parent
* @returns {boolean} true if the number should be ignored
*/
function shouldIgnoreJSXNumbers(parent) {
return parent.type.indexOf("JSX") === 0;
}
/**
* Returns whether the number should be ignored when used as an array index with enabled 'ignoreArrayIndexes' option.
* @param {ASTNode} parent - the non-"UnaryExpression" parent.
* @returns {boolean} true if the number should be ignored
*/
function shouldIgnoreArrayIndexes(parent) {
return parent.type === "MemberExpression" && ignoreArrayIndexes;
}
return {
Literal(node) {
const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
if (!isNumber(node)) {
return;
}
let fullNumberNode;
let parent;
let value;
let raw;
// For negative magic numbers: update the value and parent node
if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
fullNumberNode = node.parent;
parent = fullNumberNode.parent;
value = -node.value;
raw = `-${node.raw}`;
} else {
fullNumberNode = node;
parent = node.parent;
value = node.value;
raw = node.raw;
}
if (shouldIgnoreNumber(value) ||
shouldIgnoreParseInt(parent, fullNumberNode) ||
shouldIgnoreArrayIndexes(parent) ||
shouldIgnoreJSXNumbers(parent)) {
return;
}
if (parent.type === "VariableDeclarator") {
if (enforceConst && parent.parent.kind !== "const") {
context.report({
node: fullNumberNode,
messageId: "useConst"
});
}
} else if (
okTypes.indexOf(parent.type) === -1 ||
(parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
) {
context.report({
node: fullNumberNode,
messageId: "noMagic",
data: {
raw
}
});
}
}
};
}
};