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