• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag statements that use magic numbers (adapted from https://github.com/danielstjules/buddy.js)
3 * @author Vincent Lemeunier
4 */
5
6"use strict";
7
8const astUtils = require("./utils/ast-utils");
9
10// Maximum array length by the ECMAScript Specification.
11const MAX_ARRAY_LENGTH = 2 ** 32 - 1;
12
13//------------------------------------------------------------------------------
14// Rule Definition
15//------------------------------------------------------------------------------
16
17/**
18 * Convert the value to bigint if it's a string. Otherwise return the value as-is.
19 * @param {bigint|number|string} x The value to normalize.
20 * @returns {bigint|number} The normalized value.
21 */
22function normalizeIgnoreValue(x) {
23    if (typeof x === "string") {
24        return BigInt(x.slice(0, -1));
25    }
26    return x;
27}
28
29module.exports = {
30    meta: {
31        type: "suggestion",
32
33        docs: {
34            description: "disallow magic numbers",
35            category: "Best Practices",
36            recommended: false,
37            url: "https://eslint.org/docs/rules/no-magic-numbers"
38        },
39
40        schema: [{
41            type: "object",
42            properties: {
43                detectObjects: {
44                    type: "boolean",
45                    default: false
46                },
47                enforceConst: {
48                    type: "boolean",
49                    default: false
50                },
51                ignore: {
52                    type: "array",
53                    items: {
54                        anyOf: [
55                            { type: "number" },
56                            { type: "string", pattern: "^[+-]?(?:0|[1-9][0-9]*)n$" }
57                        ]
58                    },
59                    uniqueItems: true
60                },
61                ignoreArrayIndexes: {
62                    type: "boolean",
63                    default: false
64                },
65                ignoreDefaultValues: {
66                    type: "boolean",
67                    default: false
68                }
69            },
70            additionalProperties: false
71        }],
72
73        messages: {
74            useConst: "Number constants declarations must use 'const'.",
75            noMagic: "No magic number: {{raw}}."
76        }
77    },
78
79    create(context) {
80        const config = context.options[0] || {},
81            detectObjects = !!config.detectObjects,
82            enforceConst = !!config.enforceConst,
83            ignore = (config.ignore || []).map(normalizeIgnoreValue),
84            ignoreArrayIndexes = !!config.ignoreArrayIndexes,
85            ignoreDefaultValues = !!config.ignoreDefaultValues;
86
87        const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
88
89        /**
90         * Returns whether the rule is configured to ignore the given value
91         * @param {bigint|number} value The value to check
92         * @returns {boolean} true if the value is ignored
93         */
94        function isIgnoredValue(value) {
95            return ignore.indexOf(value) !== -1;
96        }
97
98        /**
99         * Returns whether the number is a default value assignment.
100         * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
101         * @returns {boolean} true if the number is a default value
102         */
103        function isDefaultValue(fullNumberNode) {
104            const parent = fullNumberNode.parent;
105
106            return parent.type === "AssignmentPattern" && parent.right === fullNumberNode;
107        }
108
109        /**
110         * Returns whether the given node is used as a radix within parseInt() or Number.parseInt()
111         * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
112         * @returns {boolean} true if the node is radix
113         */
114        function isParseIntRadix(fullNumberNode) {
115            const parent = fullNumberNode.parent;
116
117            return parent.type === "CallExpression" && fullNumberNode === parent.arguments[1] &&
118                (
119                    astUtils.isSpecificId(parent.callee, "parseInt") ||
120                    astUtils.isSpecificMemberAccess(parent.callee, "Number", "parseInt")
121                );
122        }
123
124        /**
125         * Returns whether the given node is a direct child of a JSX node.
126         * In particular, it aims to detect numbers used as prop values in JSX tags.
127         * Example: <input maxLength={10} />
128         * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
129         * @returns {boolean} true if the node is a JSX number
130         */
131        function isJSXNumber(fullNumberNode) {
132            return fullNumberNode.parent.type.indexOf("JSX") === 0;
133        }
134
135        /**
136         * Returns whether the given node is used as an array index.
137         * Value must coerce to a valid array index name: "0", "1", "2" ... "4294967294".
138         *
139         * All other values, like "-1", "2.5", or "4294967295", are just "normal" object properties,
140         * which can be created and accessed on an array in addition to the array index properties,
141         * but they don't affect array's length and are not considered by methods such as .map(), .forEach() etc.
142         *
143         * The maximum array length by the specification is 2 ** 32 - 1 = 4294967295,
144         * thus the maximum valid index is 2 ** 32 - 2 = 4294967294.
145         *
146         * All notations are allowed, as long as the value coerces to one of "0", "1", "2" ... "4294967294".
147         *
148         * Valid examples:
149         * a[0], a[1], a[1.2e1], a[0xAB], a[0n], a[1n]
150         * a[-0] (same as a[0] because -0 coerces to "0")
151         * a[-0n] (-0n evaluates to 0n)
152         *
153         * Invalid examples:
154         * a[-1], a[-0xAB], a[-1n], a[2.5], a[1.23e1], a[12e-1]
155         * a[4294967295] (above the max index, it's an access to a regular property a["4294967295"])
156         * a[999999999999999999999] (even if it wasn't above the max index, it would be a["1e+21"])
157         * a[1e310] (same as a["Infinity"])
158         * @param {ASTNode} fullNumberNode `Literal` or `UnaryExpression` full number node
159         * @param {bigint|number} value Value expressed by the fullNumberNode
160         * @returns {boolean} true if the node is a valid array index
161         */
162        function isArrayIndex(fullNumberNode, value) {
163            const parent = fullNumberNode.parent;
164
165            return parent.type === "MemberExpression" && parent.property === fullNumberNode &&
166                (Number.isInteger(value) || typeof value === "bigint") &&
167                value >= 0 && value < MAX_ARRAY_LENGTH;
168        }
169
170        return {
171            Literal(node) {
172                if (!astUtils.isNumericLiteral(node)) {
173                    return;
174                }
175
176                let fullNumberNode;
177                let value;
178                let raw;
179
180                // Treat unary minus as a part of the number
181                if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
182                    fullNumberNode = node.parent;
183                    value = -node.value;
184                    raw = `-${node.raw}`;
185                } else {
186                    fullNumberNode = node;
187                    value = node.value;
188                    raw = node.raw;
189                }
190
191                const parent = fullNumberNode.parent;
192
193                // Always allow radix arguments and JSX props
194                if (
195                    isIgnoredValue(value) ||
196                    (ignoreDefaultValues && isDefaultValue(fullNumberNode)) ||
197                    isParseIntRadix(fullNumberNode) ||
198                    isJSXNumber(fullNumberNode) ||
199                    (ignoreArrayIndexes && isArrayIndex(fullNumberNode, value))
200                ) {
201                    return;
202                }
203
204                if (parent.type === "VariableDeclarator") {
205                    if (enforceConst && parent.parent.kind !== "const") {
206                        context.report({
207                            node: fullNumberNode,
208                            messageId: "useConst"
209                        });
210                    }
211                } else if (
212                    okTypes.indexOf(parent.type) === -1 ||
213                    (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
214                ) {
215                    context.report({
216                        node: fullNumberNode,
217                        messageId: "noMagic",
218                        data: {
219                            raw
220                        }
221                    });
222                }
223            }
224        };
225    }
226};
227