0909e3166d952e1fb52feda96698ec4ea1229588
[dotfiles/.git] / no-magic-numbers.js
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
8 //------------------------------------------------------------------------------
9 // Rule Definition
10 //------------------------------------------------------------------------------
11
12 module.exports = {
13     meta: {
14         type: "suggestion",
15
16         docs: {
17             description: "disallow magic numbers",
18             category: "Best Practices",
19             recommended: false,
20             url: "https://eslint.org/docs/rules/no-magic-numbers"
21         },
22
23         schema: [{
24             type: "object",
25             properties: {
26                 detectObjects: {
27                     type: "boolean",
28                     default: false
29                 },
30                 enforceConst: {
31                     type: "boolean",
32                     default: false
33                 },
34                 ignore: {
35                     type: "array",
36                     items: {
37                         type: "number"
38                     },
39                     uniqueItems: true
40                 },
41                 ignoreArrayIndexes: {
42                     type: "boolean",
43                     default: false
44                 }
45             },
46             additionalProperties: false
47         }],
48
49         messages: {
50             useConst: "Number constants declarations must use 'const'.",
51             noMagic: "No magic number: {{raw}}."
52         }
53     },
54
55     create(context) {
56         const config = context.options[0] || {},
57             detectObjects = !!config.detectObjects,
58             enforceConst = !!config.enforceConst,
59             ignore = config.ignore || [],
60             ignoreArrayIndexes = !!config.ignoreArrayIndexes;
61
62         /**
63          * Returns whether the node is number literal
64          * @param {Node} node the node literal being evaluated
65          * @returns {boolean} true if the node is a number literal
66          */
67         function isNumber(node) {
68             return typeof node.value === "number";
69         }
70
71         /**
72          * Returns whether the number should be ignored
73          * @param {number} num the number
74          * @returns {boolean} true if the number should be ignored
75          */
76         function shouldIgnoreNumber(num) {
77             return ignore.indexOf(num) !== -1;
78         }
79
80         /**
81          * Returns whether the number should be ignored when used as a radix within parseInt() or Number.parseInt()
82          * @param {ASTNode} parent the non-"UnaryExpression" parent
83          * @param {ASTNode} node the node literal being evaluated
84          * @returns {boolean} true if the number should be ignored
85          */
86         function shouldIgnoreParseInt(parent, node) {
87             return parent.type === "CallExpression" && node === parent.arguments[1] &&
88                 (parent.callee.name === "parseInt" ||
89                 parent.callee.type === "MemberExpression" &&
90                 parent.callee.object.name === "Number" &&
91                 parent.callee.property.name === "parseInt");
92         }
93
94         /**
95          * Returns whether the number should be ignored when used to define a JSX prop
96          * @param {ASTNode} parent the non-"UnaryExpression" parent
97          * @returns {boolean} true if the number should be ignored
98          */
99         function shouldIgnoreJSXNumbers(parent) {
100             return parent.type.indexOf("JSX") === 0;
101         }
102
103         /**
104          * Returns whether the number should be ignored when used as an array index with enabled 'ignoreArrayIndexes' option.
105          * @param {ASTNode} parent the non-"UnaryExpression" parent.
106          * @returns {boolean} true if the number should be ignored
107          */
108         function shouldIgnoreArrayIndexes(parent) {
109             return parent.type === "MemberExpression" && ignoreArrayIndexes;
110         }
111
112         return {
113             Literal(node) {
114                 const okTypes = detectObjects ? [] : ["ObjectExpression", "Property", "AssignmentExpression"];
115
116                 if (!isNumber(node)) {
117                     return;
118                 }
119
120                 let fullNumberNode;
121                 let parent;
122                 let value;
123                 let raw;
124
125                 // For negative magic numbers: update the value and parent node
126                 if (node.parent.type === "UnaryExpression" && node.parent.operator === "-") {
127                     fullNumberNode = node.parent;
128                     parent = fullNumberNode.parent;
129                     value = -node.value;
130                     raw = `-${node.raw}`;
131                 } else {
132                     fullNumberNode = node;
133                     parent = node.parent;
134                     value = node.value;
135                     raw = node.raw;
136                 }
137
138                 if (shouldIgnoreNumber(value) ||
139                     shouldIgnoreParseInt(parent, fullNumberNode) ||
140                     shouldIgnoreArrayIndexes(parent) ||
141                     shouldIgnoreJSXNumbers(parent)) {
142                     return;
143                 }
144
145                 if (parent.type === "VariableDeclarator") {
146                     if (enforceConst && parent.parent.kind !== "const") {
147                         context.report({
148                             node: fullNumberNode,
149                             messageId: "useConst"
150                         });
151                     }
152                 } else if (
153                     okTypes.indexOf(parent.type) === -1 ||
154                     (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
155                 ) {
156                     context.report({
157                         node: fullNumberNode,
158                         messageId: "noMagic",
159                         data: {
160                             raw
161                         }
162                     });
163                 }
164             }
165         };
166     }
167 };