You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
7.4 KiB

4 years ago
  1. /**
  2. * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime
  3. * @author Jacob Moore
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Rule Definition
  8. //------------------------------------------------------------------------------
  9. module.exports = {
  10. meta: {
  11. type: "problem",
  12. docs: {
  13. description: "disallow literal numbers that lose precision",
  14. category: "Possible Errors",
  15. recommended: false,
  16. url: "https://eslint.org/docs/rules/no-loss-of-precision"
  17. },
  18. schema: [],
  19. messages: {
  20. noLossOfPrecision: "This number literal will lose precision at runtime."
  21. }
  22. },
  23. create(context) {
  24. /**
  25. * Returns whether the node is number literal
  26. * @param {Node} node the node literal being evaluated
  27. * @returns {boolean} true if the node is a number literal
  28. */
  29. function isNumber(node) {
  30. return typeof node.value === "number";
  31. }
  32. /**
  33. * Checks whether the number is base ten
  34. * @param {ASTNode} node the node being evaluated
  35. * @returns {boolean} true if the node is in base ten
  36. */
  37. function isBaseTen(node) {
  38. const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"];
  39. return prefixes.every(prefix => !node.raw.startsWith(prefix)) &&
  40. !/^0[0-7]+$/u.test(node.raw);
  41. }
  42. /**
  43. * Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type
  44. * @param {Node} node the node being evaluated
  45. * @returns {boolean} true if they do not match
  46. */
  47. function notBaseTenLosesPrecision(node) {
  48. const rawString = node.raw.toUpperCase();
  49. let base = 0;
  50. if (rawString.startsWith("0B")) {
  51. base = 2;
  52. } else if (rawString.startsWith("0X")) {
  53. base = 16;
  54. } else {
  55. base = 8;
  56. }
  57. return !rawString.endsWith(node.value.toString(base).toUpperCase());
  58. }
  59. /**
  60. * Adds a decimal point to the numeric string at index 1
  61. * @param {string} stringNumber the numeric string without any decimal point
  62. * @returns {string} the numeric string with a decimal point in the proper place
  63. */
  64. function addDecimalPointToNumber(stringNumber) {
  65. return `${stringNumber.slice(0, 1)}.${stringNumber.slice(1)}`;
  66. }
  67. /**
  68. * Returns the number stripped of leading zeros
  69. * @param {string} numberAsString the string representation of the number
  70. * @returns {string} the stripped string
  71. */
  72. function removeLeadingZeros(numberAsString) {
  73. return numberAsString.replace(/^0*/u, "");
  74. }
  75. /**
  76. * Returns the number stripped of trailing zeros
  77. * @param {string} numberAsString the string representation of the number
  78. * @returns {string} the stripped string
  79. */
  80. function removeTrailingZeros(numberAsString) {
  81. return numberAsString.replace(/0*$/u, "");
  82. }
  83. /**
  84. * Converts an integer to to an object containing the the integer's coefficient and order of magnitude
  85. * @param {string} stringInteger the string representation of the integer being converted
  86. * @returns {Object} the object containing the the integer's coefficient and order of magnitude
  87. */
  88. function normalizeInteger(stringInteger) {
  89. const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
  90. return {
  91. magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1,
  92. coefficient: addDecimalPointToNumber(significantDigits)
  93. };
  94. }
  95. /**
  96. *
  97. * Converts a float to to an object containing the the floats's coefficient and order of magnitude
  98. * @param {string} stringFloat the string representation of the float being converted
  99. * @returns {Object} the object containing the the integer's coefficient and order of magnitude
  100. */
  101. function normalizeFloat(stringFloat) {
  102. const trimmedFloat = removeLeadingZeros(stringFloat);
  103. if (trimmedFloat.startsWith(".")) {
  104. const decimalDigits = trimmedFloat.split(".").pop();
  105. const significantDigits = removeLeadingZeros(decimalDigits);
  106. return {
  107. magnitude: significantDigits.length - decimalDigits.length - 1,
  108. coefficient: addDecimalPointToNumber(significantDigits)
  109. };
  110. }
  111. return {
  112. magnitude: trimmedFloat.indexOf(".") - 1,
  113. coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", ""))
  114. };
  115. }
  116. /**
  117. * Converts a base ten number to proper scientific notation
  118. * @param {string} stringNumber the string representation of the base ten number to be converted
  119. * @returns {string} the number converted to scientific notation
  120. */
  121. function convertNumberToScientificNotation(stringNumber) {
  122. const splitNumber = stringNumber.replace("E", "e").split("e");
  123. const originalCoefficient = splitNumber[0];
  124. const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient)
  125. : normalizeInteger(originalCoefficient);
  126. const normalizedCoefficient = normalizedNumber.coefficient;
  127. const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude)
  128. : normalizedNumber.magnitude;
  129. return `${normalizedCoefficient}e${magnitude}`;
  130. }
  131. /**
  132. * Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type
  133. * @param {Node} node the node being evaluated
  134. * @returns {boolean} true if they do not match
  135. */
  136. function baseTenLosesPrecision(node) {
  137. const normalizedRawNumber = convertNumberToScientificNotation(node.raw);
  138. const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length;
  139. if (requestedPrecision > 100) {
  140. return true;
  141. }
  142. const storedNumber = node.value.toPrecision(requestedPrecision);
  143. const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber);
  144. return normalizedRawNumber !== normalizedStoredNumber;
  145. }
  146. /**
  147. * Checks that the user-intended number equals the actual number after is has been converted to the Number type
  148. * @param {Node} node the node being evaluated
  149. * @returns {boolean} true if they do not match
  150. */
  151. function losesPrecision(node) {
  152. return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node);
  153. }
  154. return {
  155. Literal(node) {
  156. if (node.value && isNumber(node) && losesPrecision(node)) {
  157. context.report({
  158. messageId: "noLossOfPrecision",
  159. node
  160. });
  161. }
  162. }
  163. };
  164. }
  165. };