[d565449] | 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 | };
|
---|