| /** | |
|  * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime | |
|  * @author Jacob Moore | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow literal numbers that lose precision", | |
|             recommended: true, | |
|             url: "https://eslint.org/docs/latest/rules/no-loss-of-precision" | |
|         }, | |
|         schema: [], | |
|         messages: { | |
|             noLossOfPrecision: "This number literal will lose precision at runtime." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
| 
 | |
|         /** | |
|          * Returns whether the node is number literal | |
|          * @param {Node} node the node literal being evaluated | |
|          * @returns {boolean} true if the node is a number literal | |
|          */ | |
|         function isNumber(node) { | |
|             return typeof node.value === "number"; | |
|         } | |
| 
 | |
|         /** | |
|          * Gets the source code of the given number literal. Removes `_` numeric separators from the result. | |
|          * @param {Node} node the number `Literal` node | |
|          * @returns {string} raw source code of the literal, without numeric separators | |
|          */ | |
|         function getRaw(node) { | |
|             return node.raw.replace(/_/gu, ""); | |
|         } | |
| 
 | |
|         /** | |
|          * Checks whether the number is  base ten | |
|          * @param {ASTNode} node the node being evaluated | |
|          * @returns {boolean} true if the node is in base ten | |
|          */ | |
|         function isBaseTen(node) { | |
|             const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; | |
| 
 | |
|             return prefixes.every(prefix => !node.raw.startsWith(prefix)) && | |
|             !/^0[0-7]+$/u.test(node.raw); | |
|         } | |
| 
 | |
|         /** | |
|          * Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type | |
|          * @param {Node} node the node being evaluated | |
|          * @returns {boolean} true if they do not match | |
|          */ | |
|         function notBaseTenLosesPrecision(node) { | |
|             const rawString = getRaw(node).toUpperCase(); | |
|             let base = 0; | |
| 
 | |
|             if (rawString.startsWith("0B")) { | |
|                 base = 2; | |
|             } else if (rawString.startsWith("0X")) { | |
|                 base = 16; | |
|             } else { | |
|                 base = 8; | |
|             } | |
| 
 | |
|             return !rawString.endsWith(node.value.toString(base).toUpperCase()); | |
|         } | |
| 
 | |
|         /** | |
|          * Adds a decimal point to the numeric string at index 1 | |
|          * @param {string} stringNumber the numeric string without any decimal point | |
|          * @returns {string} the numeric string with a decimal point in the proper place | |
|          */ | |
|         function addDecimalPointToNumber(stringNumber) { | |
|             return `${stringNumber[0]}.${stringNumber.slice(1)}`; | |
|         } | |
| 
 | |
|         /** | |
|          * Returns the number stripped of leading zeros | |
|          * @param {string} numberAsString the string representation of the number | |
|          * @returns {string} the stripped string | |
|          */ | |
|         function removeLeadingZeros(numberAsString) { | |
|             for (let i = 0; i < numberAsString.length; i++) { | |
|                 if (numberAsString[i] !== "0") { | |
|                     return numberAsString.slice(i); | |
|                 } | |
|             } | |
|             return numberAsString; | |
|         } | |
| 
 | |
|         /** | |
|          * Returns the number stripped of trailing zeros | |
|          * @param {string} numberAsString the string representation of the number | |
|          * @returns {string} the stripped string | |
|          */ | |
|         function removeTrailingZeros(numberAsString) { | |
|             for (let i = numberAsString.length - 1; i >= 0; i--) { | |
|                 if (numberAsString[i] !== "0") { | |
|                     return numberAsString.slice(0, i + 1); | |
|                 } | |
|             } | |
|             return numberAsString; | |
|         } | |
| 
 | |
|         /** | |
|          * Converts an integer to an object containing the integer's coefficient and order of magnitude | |
|          * @param {string} stringInteger the string representation of the integer being converted | |
|          * @returns {Object} the object containing the integer's coefficient and order of magnitude | |
|          */ | |
|         function normalizeInteger(stringInteger) { | |
|             const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger)); | |
| 
 | |
|             return { | |
|                 magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1, | |
|                 coefficient: addDecimalPointToNumber(significantDigits) | |
|             }; | |
|         } | |
| 
 | |
|         /** | |
|          * | |
|          * Converts a float to an object containing the floats's coefficient and order of magnitude | |
|          * @param {string} stringFloat the string representation of the float being converted | |
|          * @returns {Object} the object containing the integer's coefficient and order of magnitude | |
|          */ | |
|         function normalizeFloat(stringFloat) { | |
|             const trimmedFloat = removeLeadingZeros(stringFloat); | |
| 
 | |
|             if (trimmedFloat.startsWith(".")) { | |
|                 const decimalDigits = trimmedFloat.slice(1); | |
|                 const significantDigits = removeLeadingZeros(decimalDigits); | |
| 
 | |
|                 return { | |
|                     magnitude: significantDigits.length - decimalDigits.length - 1, | |
|                     coefficient: addDecimalPointToNumber(significantDigits) | |
|                 }; | |
| 
 | |
|             } | |
|             return { | |
|                 magnitude: trimmedFloat.indexOf(".") - 1, | |
|                 coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", "")) | |
| 
 | |
|             }; | |
|         } | |
| 
 | |
|         /** | |
|          * Converts a base ten number to proper scientific notation | |
|          * @param {string} stringNumber the string representation of the base ten number to be converted | |
|          * @returns {string} the number converted to scientific notation | |
|          */ | |
|         function convertNumberToScientificNotation(stringNumber) { | |
|             const splitNumber = stringNumber.replace("E", "e").split("e"); | |
|             const originalCoefficient = splitNumber[0]; | |
|             const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient) | |
|                 : normalizeInteger(originalCoefficient); | |
|             const normalizedCoefficient = normalizedNumber.coefficient; | |
|             const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude) | |
|                 : normalizedNumber.magnitude; | |
| 
 | |
|             return `${normalizedCoefficient}e${magnitude}`; | |
|         } | |
| 
 | |
|         /** | |
|          * Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type | |
|          * @param {Node} node the node being evaluated | |
|          * @returns {boolean} true if they do not match | |
|          */ | |
|         function baseTenLosesPrecision(node) { | |
|             const normalizedRawNumber = convertNumberToScientificNotation(getRaw(node)); | |
|             const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length; | |
| 
 | |
|             if (requestedPrecision > 100) { | |
|                 return true; | |
|             } | |
|             const storedNumber = node.value.toPrecision(requestedPrecision); | |
|             const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber); | |
| 
 | |
|             return normalizedRawNumber !== normalizedStoredNumber; | |
|         } | |
| 
 | |
| 
 | |
|         /** | |
|          * Checks that the user-intended number equals the actual number after is has been converted to the Number type | |
|          * @param {Node} node the node being evaluated | |
|          * @returns {boolean} true if they do not match | |
|          */ | |
|         function losesPrecision(node) { | |
|             return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node); | |
|         } | |
| 
 | |
| 
 | |
|         return { | |
|             Literal(node) { | |
|                 if (node.value && isNumber(node) && losesPrecision(node)) { | |
|                     context.report({ | |
|                         messageId: "noLossOfPrecision", | |
|                         node | |
|                     }); | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |