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/latest/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[0]}.${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 | for (let i = 0; i < numberAsString.length; i++) {
|
---|
96 | if (numberAsString[i] !== "0") {
|
---|
97 | return numberAsString.slice(i);
|
---|
98 | }
|
---|
99 | }
|
---|
100 | return numberAsString;
|
---|
101 | }
|
---|
102 |
|
---|
103 | /**
|
---|
104 | * Returns the number stripped of trailing zeros
|
---|
105 | * @param {string} numberAsString the string representation of the number
|
---|
106 | * @returns {string} the stripped string
|
---|
107 | */
|
---|
108 | function removeTrailingZeros(numberAsString) {
|
---|
109 | for (let i = numberAsString.length - 1; i >= 0; i--) {
|
---|
110 | if (numberAsString[i] !== "0") {
|
---|
111 | return numberAsString.slice(0, i + 1);
|
---|
112 | }
|
---|
113 | }
|
---|
114 | return numberAsString;
|
---|
115 | }
|
---|
116 |
|
---|
117 | /**
|
---|
118 | * Converts an integer to an object containing the integer's coefficient and order of magnitude
|
---|
119 | * @param {string} stringInteger the string representation of the integer being converted
|
---|
120 | * @returns {Object} the object containing the integer's coefficient and order of magnitude
|
---|
121 | */
|
---|
122 | function normalizeInteger(stringInteger) {
|
---|
123 | const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
|
---|
124 |
|
---|
125 | return {
|
---|
126 | magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1,
|
---|
127 | coefficient: addDecimalPointToNumber(significantDigits)
|
---|
128 | };
|
---|
129 | }
|
---|
130 |
|
---|
131 | /**
|
---|
132 | *
|
---|
133 | * Converts a float to an object containing the floats's coefficient and order of magnitude
|
---|
134 | * @param {string} stringFloat the string representation of the float being converted
|
---|
135 | * @returns {Object} the object containing the integer's coefficient and order of magnitude
|
---|
136 | */
|
---|
137 | function normalizeFloat(stringFloat) {
|
---|
138 | const trimmedFloat = removeLeadingZeros(stringFloat);
|
---|
139 |
|
---|
140 | if (trimmedFloat.startsWith(".")) {
|
---|
141 | const decimalDigits = trimmedFloat.slice(1);
|
---|
142 | const significantDigits = removeLeadingZeros(decimalDigits);
|
---|
143 |
|
---|
144 | return {
|
---|
145 | magnitude: significantDigits.length - decimalDigits.length - 1,
|
---|
146 | coefficient: addDecimalPointToNumber(significantDigits)
|
---|
147 | };
|
---|
148 |
|
---|
149 | }
|
---|
150 | return {
|
---|
151 | magnitude: trimmedFloat.indexOf(".") - 1,
|
---|
152 | coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", ""))
|
---|
153 |
|
---|
154 | };
|
---|
155 | }
|
---|
156 |
|
---|
157 | /**
|
---|
158 | * Converts a base ten number to proper scientific notation
|
---|
159 | * @param {string} stringNumber the string representation of the base ten number to be converted
|
---|
160 | * @returns {string} the number converted to scientific notation
|
---|
161 | */
|
---|
162 | function convertNumberToScientificNotation(stringNumber) {
|
---|
163 | const splitNumber = stringNumber.replace("E", "e").split("e");
|
---|
164 | const originalCoefficient = splitNumber[0];
|
---|
165 | const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient)
|
---|
166 | : normalizeInteger(originalCoefficient);
|
---|
167 | const normalizedCoefficient = normalizedNumber.coefficient;
|
---|
168 | const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude)
|
---|
169 | : normalizedNumber.magnitude;
|
---|
170 |
|
---|
171 | return `${normalizedCoefficient}e${magnitude}`;
|
---|
172 | }
|
---|
173 |
|
---|
174 | /**
|
---|
175 | * Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type
|
---|
176 | * @param {Node} node the node being evaluated
|
---|
177 | * @returns {boolean} true if they do not match
|
---|
178 | */
|
---|
179 | function baseTenLosesPrecision(node) {
|
---|
180 | const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node));
|
---|
181 | const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length;
|
---|
182 |
|
---|
183 | if (requestedPrecision > 100) {
|
---|
184 | return true;
|
---|
185 | }
|
---|
186 | const storedNumber = node.value.toPrecision(requestedPrecision);
|
---|
187 | const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber);
|
---|
188 |
|
---|
189 | return normalizedRawNumber !== normalizedStoredNumber;
|
---|
190 | }
|
---|
191 |
|
---|
192 |
|
---|
193 | /**
|
---|
194 | * Checks that the user-intended number equals the actual number after is has been converted to the Number type
|
---|
195 | * @param {Node} node the node being evaluated
|
---|
196 | * @returns {boolean} true if they do not match
|
---|
197 | */
|
---|
198 | function losesPrecision(node) {
|
---|
199 | return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node);
|
---|
200 | }
|
---|
201 |
|
---|
202 |
|
---|
203 | return {
|
---|
204 | Literal(node) {
|
---|
205 | if (node.value && isNumber(node) && losesPrecision(node)) {
|
---|
206 | context.report({
|
---|
207 | messageId: "noLossOfPrecision",
|
---|
208 | node
|
---|
209 | });
|
---|
210 | }
|
---|
211 | }
|
---|
212 | };
|
---|
213 | }
|
---|
214 | };
|
---|