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 12module.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 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 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) { 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}; 207