]> git.proxmox.com Git - pve-eslint.git/blob - eslint/lib/rules/no-loss-of-precision.js
fefc7b768fe9bca5d5d7a8a1684bc4ab7b2efa60
[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 /** @type {import('../shared/types').Rule} */
13 module.exports = {
14 meta: {
15 type: "problem",
16
17 docs: {
18 description: "disallow literal numbers that lose precision",
19 recommended: true,
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
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 }
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) {
66 const rawString = getRaw(node).toUpperCase();
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 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 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 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 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) {
172 const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node));
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 };