]>
Commit | Line | Data |
---|---|---|
ebb53d86 TL |
1 | /** |
2 | * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime | |
3 | * @author Jacob Moore | |
4 | */ | |
5 | ||
6 | "use strict"; | |
7 | ||
8 | //------------------------------------------------------------------------------ | |
9 | // Rule Definition | |
10 | //------------------------------------------------------------------------------ | |
11 | ||
12 | module.exports = { | |
13 | meta: { | |
14 | type: "problem", | |
15 | ||
16 | docs: { | |
17 | description: "disallow literal numbers that lose precision", | |
18 | category: "Possible Errors", | |
19 | recommended: false, | |
20 | url: "https://eslint.org/docs/rules/no-loss-of-precision" | |
21 | }, | |
22 | schema: [], | |
23 | messages: { | |
24 | noLossOfPrecision: "This number literal will lose precision at runtime." | |
25 | } | |
26 | }, | |
27 | ||
28 | create(context) { | |
29 | ||
30 | /** | |
31 | * Returns whether the node is number literal | |
32 | * @param {Node} node the node literal being evaluated | |
33 | * @returns {boolean} true if the node is a number literal | |
34 | */ | |
35 | function isNumber(node) { | |
36 | return typeof node.value === "number"; | |
37 | } | |
38 | ||
6f036462 TL |
39 | /** |
40 | * Gets the source code of the given number literal. Removes `_` numeric separators from the result. | |
41 | * @param {Node} node the number `Literal` node | |
42 | * @returns {string} raw source code of the literal, without numeric separators | |
43 | */ | |
44 | function getRaw(node) { | |
45 | return node.raw.replace(/_/gu, ""); | |
46 | } | |
ebb53d86 TL |
47 | |
48 | /** | |
49 | * Checks whether the number is base ten | |
50 | * @param {ASTNode} node the node being evaluated | |
51 | * @returns {boolean} true if the node is in base ten | |
52 | */ | |
53 | function isBaseTen(node) { | |
54 | const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; | |
55 | ||
56 | return prefixes.every(prefix => !node.raw.startsWith(prefix)) && | |
57 | !/^0[0-7]+$/u.test(node.raw); | |
58 | } | |
59 | ||
60 | /** | |
61 | * Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type | |
62 | * @param {Node} node the node being evaluated | |
63 | * @returns {boolean} true if they do not match | |
64 | */ | |
65 | function notBaseTenLosesPrecision(node) { | |
6f036462 | 66 | const rawString = getRaw(node).toUpperCase(); |
ebb53d86 TL |
67 | let base = 0; |
68 | ||
69 | if (rawString.startsWith("0B")) { | |
70 | base = 2; | |
71 | } else if (rawString.startsWith("0X")) { | |
72 | base = 16; | |
73 | } else { | |
74 | base = 8; | |
75 | } | |
76 | ||
77 | return !rawString.endsWith(node.value.toString(base).toUpperCase()); | |
78 | } | |
79 | ||
80 | /** | |
81 | * Adds a decimal point to the numeric string at index 1 | |
82 | * @param {string} stringNumber the numeric string without any decimal point | |
83 | * @returns {string} the numeric string with a decimal point in the proper place | |
84 | */ | |
85 | function addDecimalPointToNumber(stringNumber) { | |
86 | return `${stringNumber.slice(0, 1)}.${stringNumber.slice(1)}`; | |
87 | } | |
88 | ||
89 | /** | |
90 | * Returns the number stripped of leading zeros | |
91 | * @param {string} numberAsString the string representation of the number | |
92 | * @returns {string} the stripped string | |
93 | */ | |
94 | function removeLeadingZeros(numberAsString) { | |
95 | return numberAsString.replace(/^0*/u, ""); | |
96 | } | |
97 | ||
98 | /** | |
99 | * Returns the number stripped of trailing zeros | |
100 | * @param {string} numberAsString the string representation of the number | |
101 | * @returns {string} the stripped string | |
102 | */ | |
103 | function removeTrailingZeros(numberAsString) { | |
104 | return numberAsString.replace(/0*$/u, ""); | |
105 | } | |
106 | ||
107 | /** | |
108 | * Converts an integer to to an object containing the the integer's coefficient and order of magnitude | |
109 | * @param {string} stringInteger the string representation of the integer being converted | |
110 | * @returns {Object} the object containing the the integer's coefficient and order of magnitude | |
111 | */ | |
112 | function normalizeInteger(stringInteger) { | |
113 | const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger)); | |
114 | ||
115 | return { | |
116 | magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1, | |
117 | coefficient: addDecimalPointToNumber(significantDigits) | |
118 | }; | |
119 | } | |
120 | ||
121 | /** | |
122 | * | |
123 | * Converts a float to to an object containing the the floats's coefficient and order of magnitude | |
124 | * @param {string} stringFloat the string representation of the float being converted | |
125 | * @returns {Object} the object containing the the integer's coefficient and order of magnitude | |
126 | */ | |
127 | function normalizeFloat(stringFloat) { | |
128 | const trimmedFloat = removeLeadingZeros(stringFloat); | |
129 | ||
130 | if (trimmedFloat.startsWith(".")) { | |
131 | const decimalDigits = trimmedFloat.split(".").pop(); | |
132 | const significantDigits = removeLeadingZeros(decimalDigits); | |
133 | ||
134 | return { | |
135 | magnitude: significantDigits.length - decimalDigits.length - 1, | |
136 | coefficient: addDecimalPointToNumber(significantDigits) | |
137 | }; | |
138 | ||
139 | } | |
140 | return { | |
141 | magnitude: trimmedFloat.indexOf(".") - 1, | |
142 | coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", "")) | |
143 | ||
144 | }; | |
145 | } | |
146 | ||
147 | ||
148 | /** | |
149 | * Converts a base ten number to proper scientific notation | |
150 | * @param {string} stringNumber the string representation of the base ten number to be converted | |
151 | * @returns {string} the number converted to scientific notation | |
152 | */ | |
153 | function convertNumberToScientificNotation(stringNumber) { | |
154 | const splitNumber = stringNumber.replace("E", "e").split("e"); | |
155 | const originalCoefficient = splitNumber[0]; | |
156 | const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient) | |
157 | : normalizeInteger(originalCoefficient); | |
158 | const normalizedCoefficient = normalizedNumber.coefficient; | |
159 | const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude) | |
160 | : normalizedNumber.magnitude; | |
161 | ||
162 | return `${normalizedCoefficient}e${magnitude}`; | |
163 | ||
164 | } | |
165 | ||
166 | /** | |
167 | * Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type | |
168 | * @param {Node} node the node being evaluated | |
169 | * @returns {boolean} true if they do not match | |
170 | */ | |
171 | function baseTenLosesPrecision(node) { | |
6f036462 | 172 | const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node)); |
ebb53d86 TL |
173 | const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length; |
174 | ||
175 | if (requestedPrecision > 100) { | |
176 | return true; | |
177 | } | |
178 | const storedNumber = node.value.toPrecision(requestedPrecision); | |
179 | const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber); | |
180 | ||
181 | return normalizedRawNumber !== normalizedStoredNumber; | |
182 | } | |
183 | ||
184 | ||
185 | /** | |
186 | * Checks that the user-intended number equals the actual number after is has been converted to the Number type | |
187 | * @param {Node} node the node being evaluated | |
188 | * @returns {boolean} true if they do not match | |
189 | */ | |
190 | function losesPrecision(node) { | |
191 | return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node); | |
192 | } | |
193 | ||
194 | ||
195 | return { | |
196 | Literal(node) { | |
197 | if (node.value && isNumber(node) && losesPrecision(node)) { | |
198 | context.report({ | |
199 | messageId: "noLossOfPrecision", | |
200 | node | |
201 | }); | |
202 | } | |
203 | } | |
204 | }; | |
205 | } | |
206 | }; |