From bece581800c934e6ae0e0696124c7dc539a272c3 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Sun, 29 Dec 2019 16:00:11 -0500 Subject: [PATCH 01/23] Created rule and test files --- lib/rules/no-loss-of-precision.js | 68 +++++++ tests/lib/rules/no-loss-of-precision.js | 234 ++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 lib/rules/no-loss-of-precision.js create mode 100644 tests/lib/rules/no-loss-of-precision.js diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js new file mode 100644 index 00000000000..277cef5cc7c --- /dev/null +++ b/lib/rules/no-loss-of-precision.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime + * @author Jacob Moore + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "problem", + + docs: { + description: "disallow numbers that lose precision", + category: "Possible Errors", + recommended: true, + url: "https://eslint.org/docs/rules/no-loss-of-precision" + }, + + schema: [], + + messages: { + noLossOfPrecision: "Numbers only support 16 significant digit precision." + } + }, + + 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"; + } + + /** + * Returns whether the node will lose precision + * @param {Node} node the node literal being evaluated + * @returns {boolean} true if the node will lose precision + */ + function willLosePrecision(node) { + const splitNumber = node.value.toString().split("."); + + if (splitNumber.length === 2 && !Number(splitNumber[0])) { + splitNumber.shift(); + } + + return splitNumber.join("").length > 16; + + } + + + return { + Literal(node) { + if (isNumber(node) && willLosePrecision(node)) { + context.report({ + messageId: "noLossOfPrecision" + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js new file mode 100644 index 00000000000..091013deed1 --- /dev/null +++ b/tests/lib/rules/no-loss-of-precision.js @@ -0,0 +1,234 @@ +/** + * @fileoverview Tests for no-loss-of-precision rule. + * @author Jacob Moore + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-loss-of-precision"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run("no-loss-of-precision", rule, { + valid: [ + "var x = 12345", + "var x = 123.456", + "var x = -123", + "var x = 123e15", + "var x = Number.parseInt(y, 10);", + { + code: "const foo = 42;", + env: { es6: true } + }, + { + code: "var foo = 42;", + options: [{ + enforceConst: false + }], + env: { es6: true } + }, + "var foo = -42;", + { + code: "var foo = 0 + 1 - 2 + -2;", + options: [{ + ignore: [0, 1, 2, -2] + }] + }, + { + code: "var foo = 0 + 1 + 2 + 3 + 4;", + options: [{ + ignore: [0, 1, 2, 3, 4] + }] + }, + "var foo = { bar:10 }", + { + code: "setTimeout(function() {return 1;}, 0);", + options: [{ + ignore: [0, 1] + }] + }, + { + code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", + options: [{ + ignoreArrayIndexes: true + }] + }, + { + code: "var a = ;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, + { + code: "var a =
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + ], + invalid: [ + { + code: "var foo = 42", + options: [{ + enforceConst: true + }], + env: { es6: true }, + errors: [{ messageId: "useConst" }] + }, + { + code: "var foo = 0 + 1;", + errors: [ + { messageId: "noMagic", data: { raw: "0" } }, + { messageId: "noMagic", data: { raw: "1" } } + ] + }, + { + code: "a = a + 5;", + errors: [ + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "a += 5;", + errors: [ + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "var foo = 0 + 1 + -2 + 2;", + errors: [ + { messageId: "noMagic", data: { raw: "0" } }, + { messageId: "noMagic", data: { raw: "1" } }, + { messageId: "noMagic", data: { raw: "-2" } }, + { messageId: "noMagic", data: { raw: "2" } } + ] + }, + { + code: "var foo = 0 + 1 + 2;", + options: [{ + ignore: [0, 1] + }], + errors: [ + { messageId: "noMagic", data: { raw: "2" } } + ] + }, + { + code: "var foo = { bar:10 }", + options: [{ + detectObjects: true + }], + errors: [ + { messageId: "noMagic", data: { raw: "10" } } + ] + }, { + code: "console.log(0x1A + 0x02); console.log(071);", + errors: [ + { messageId: "noMagic", data: { raw: "0x1A" } }, + { messageId: "noMagic", data: { raw: "0x02" } }, + { messageId: "noMagic", data: { raw: "071" } } + ] + }, { + code: "var stats = {avg: 42};", + options: [{ + detectObjects: true + }], + errors: [ + { messageId: "noMagic", data: { raw: "42" } } + ] + }, { + code: "var colors = {}; colors.RED = 2; colors.YELLOW = 3; colors.BLUE = 4 + 5;", + errors: [ + { messageId: "noMagic", data: { raw: "4" } }, + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "function getSecondsInMinute() {return 60;}", + errors: [ + { message: "No magic number: 60." } + ] + }, + { + code: "function getNegativeSecondsInMinute() {return -60;}", + errors: [ + { messageId: "noMagic", data: { raw: "-60" } } + ] + }, + { + code: "var Promise = require('bluebird');\n" + + "var MINUTE = 60;\n" + + "var HOUR = 3600;\n" + + "const DAY = 86400;\n" + + "var configObject = {\n" + + "key: 90,\n" + + "another: 10 * 10,\n" + + "10: 'an \"integer\" key'\n" + + "};\n" + + "function getSecondsInDay() {\n" + + " return 24 * HOUR;\n" + + "}\n" + + "function getMillisecondsInDay() {\n" + + "return (getSecondsInDay() *\n" + + "(1000)\n" + + ");\n" + + "}\n" + + "function callSetTimeoutZero(func) {\n" + + "setTimeout(func, 0);\n" + + "}\n" + + "function invokeInTen(func) {\n" + + "setTimeout(func, 10);\n" + + "}\n", + env: { es6: true }, + errors: [ + { messageId: "noMagic", data: { raw: "10" }, line: 7 }, + { messageId: "noMagic", data: { raw: "10" }, line: 7 }, + { messageId: "noMagic", data: { raw: "24" }, line: 11 }, + { messageId: "noMagic", data: { raw: "1000" }, line: 15 }, + { messageId: "noMagic", data: { raw: "0" }, line: 19 }, + { messageId: "noMagic", data: { raw: "10" }, line: 22 } + ] + }, + { + code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", + options: [{}], + errors: [{ + messageId: "noMagic", data: { raw: "3" }, line: 1 + }] + }, + { + code: "var a =
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { messageId: "noMagic", data: { raw: "1" }, line: 1 }, + { messageId: "noMagic", data: { raw: "2" }, line: 1 }, + { messageId: "noMagic", data: { raw: "3" }, line: 1 } + ] + }, + { + code: "var min, max, mean; min = 1; max = 10; mean = 4;", + options: [{}], + errors: [ + { messageId: "noMagic", data: { raw: "1" }, line: 1 }, + { messageId: "noMagic", data: { raw: "10" }, line: 1 }, + { messageId: "noMagic", data: { raw: "4" }, line: 1 } + ] + } + ] +}); From f95678e8fe1f6b4f79fcc420e6a4519fec2e940f Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Mon, 30 Dec 2019 00:41:42 -0500 Subject: [PATCH 02/23] Working rules and tests --- lib/rules/no-loss-of-precision.js | 14 +- tests/lib/rules/no-loss-of-precision.js | 236 ++++++------------------ 2 files changed, 66 insertions(+), 184 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 277cef5cc7c..8746b7e3c20 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -44,14 +44,15 @@ module.exports = { * @returns {boolean} true if the node will lose precision */ function willLosePrecision(node) { - const splitNumber = node.value.toString().split("."); - - if (splitNumber.length === 2 && !Number(splitNumber[0])) { - splitNumber.shift(); + if (node.raw.startsWith("0x")) { + return Math.abs(node.value) > Number.MAX_VALUE; } - return splitNumber.join("").length > 16; + const strippedNumber = node.raw.split("e")[0].replace("-", ""); + return strippedNumber.includes(".") + ? Number(strippedNumber.replace(".", "")) > Number.MAX_SAFE_INTEGER + : Number(strippedNumber.replace(/0*$/u, "")) > Number.MAX_SAFE_INTEGER; } @@ -59,7 +60,8 @@ module.exports = { Literal(node) { if (isNumber(node) && willLosePrecision(node)) { context.report({ - messageId: "noLossOfPrecision" + messageId: "noLossOfPrecision", + node }); } } diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index 091013deed1..b6fd5c304d6 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -22,213 +22,93 @@ ruleTester.run("no-loss-of-precision", rule, { valid: [ "var x = 12345", "var x = 123.456", - "var x = -123", - "var x = 123e15", - "var x = Number.parseInt(y, 10);", - { - code: "const foo = 42;", - env: { es6: true } - }, - { - code: "var foo = 42;", - options: [{ - enforceConst: false - }], - env: { es6: true } - }, - "var foo = -42;", - { - code: "var foo = 0 + 1 - 2 + -2;", - options: [{ - ignore: [0, 1, 2, -2] - }] - }, - { - code: "var foo = 0 + 1 + 2 + 3 + 4;", - options: [{ - ignore: [0, 1, 2, 3, 4] - }] - }, - "var foo = { bar:10 }", - { - code: "setTimeout(function() {return 1;}, 0);", - options: [{ - ignore: [0, 1] - }] - }, + "var x = -123.456", + "var x = -123456", + "var x = 123e34", + "var x = 123e-34", + "var x = -123e34", + "var x = -123e-34", + "var x = 12.3e34", + "var x = 12.3e-34", + "var x = -12.3e34", + "var x = -12.3e-34", + "var x = 12300000000000000000000000", + "var x = -12300000000000000000000000", + "var x = 0000000000000000000000012300000000000000000000000", + "var x = -0000000000000000000012300000000000000000000000", + "var x = 0.00000000000000000000000123", + "var x = -0.00000000000000000000000123", + "var x = 9007199254740991", + "var x = -9007199254740991", + "var x = 9007.199254740991", + "var x = -9007.199254740991", + "var x = 900719925474099100", + "var x = -900719925474099100", + "var x = 9007199254740991e3", + "var x = 9007199254740991e-3", + "var x = .9007199254740991", + "var x = -.0009007199254740991" + ], + invalid: [ { - code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", - options: [{ - ignoreArrayIndexes: true - }] + code: "var x = 9007199254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a = ;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - } + code: "var x = -9007199254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a =
;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - } - ], - invalid: [ - { - code: "var foo = 42", - options: [{ - enforceConst: true - }], - env: { es6: true }, - errors: [{ messageId: "useConst" }] + code: "var x = 900719.9254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1;", - errors: [ - { messageId: "noMagic", data: { raw: "0" } }, - { messageId: "noMagic", data: { raw: "1" } } - ] + code: "var x = -900719.9254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "a = a + 5;", - errors: [ - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = 9007199254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "a += 5;", - errors: [ - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = -9007199254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1 + -2 + 2;", - errors: [ - { messageId: "noMagic", data: { raw: "0" } }, - { messageId: "noMagic", data: { raw: "1" } }, - { messageId: "noMagic", data: { raw: "-2" } }, - { messageId: "noMagic", data: { raw: "2" } } - ] + code: "var x = 900719.9254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1 + 2;", - options: [{ - ignore: [0, 1] - }], - errors: [ - { messageId: "noMagic", data: { raw: "2" } } - ] + code: "var x = -900719.9254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = { bar:10 }", - options: [{ - detectObjects: true - }], - errors: [ - { messageId: "noMagic", data: { raw: "10" } } - ] - }, { - code: "console.log(0x1A + 0x02); console.log(071);", - errors: [ - { messageId: "noMagic", data: { raw: "0x1A" } }, - { messageId: "noMagic", data: { raw: "0x02" } }, - { messageId: "noMagic", data: { raw: "071" } } - ] - }, { - code: "var stats = {avg: 42};", - options: [{ - detectObjects: true - }], - errors: [ - { messageId: "noMagic", data: { raw: "42" } } - ] - }, { - code: "var colors = {}; colors.RED = 2; colors.YELLOW = 3; colors.BLUE = 4 + 5;", - errors: [ - { messageId: "noMagic", data: { raw: "4" } }, - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = 5123000000000000000000000000001", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "function getSecondsInMinute() {return 60;}", - errors: [ - { message: "No magic number: 60." } - ] + code: "var x = -5123000000000000000000000000001", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "function getNegativeSecondsInMinute() {return -60;}", - errors: [ - { messageId: "noMagic", data: { raw: "-60" } } - ] + code: "var x = 1230000000000000000000000.0", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var Promise = require('bluebird');\n" + - "var MINUTE = 60;\n" + - "var HOUR = 3600;\n" + - "const DAY = 86400;\n" + - "var configObject = {\n" + - "key: 90,\n" + - "another: 10 * 10,\n" + - "10: 'an \"integer\" key'\n" + - "};\n" + - "function getSecondsInDay() {\n" + - " return 24 * HOUR;\n" + - "}\n" + - "function getMillisecondsInDay() {\n" + - "return (getSecondsInDay() *\n" + - "(1000)\n" + - ");\n" + - "}\n" + - "function callSetTimeoutZero(func) {\n" + - "setTimeout(func, 0);\n" + - "}\n" + - "function invokeInTen(func) {\n" + - "setTimeout(func, 10);\n" + - "}\n", - env: { es6: true }, - errors: [ - { messageId: "noMagic", data: { raw: "10" }, line: 7 }, - { messageId: "noMagic", data: { raw: "10" }, line: 7 }, - { messageId: "noMagic", data: { raw: "24" }, line: 11 }, - { messageId: "noMagic", data: { raw: "1000" }, line: 15 }, - { messageId: "noMagic", data: { raw: "0" }, line: 19 }, - { messageId: "noMagic", data: { raw: "10" }, line: 22 } - ] + code: "var x = 123.0000000000000000000000", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", - options: [{}], - errors: [{ - messageId: "noMagic", data: { raw: "3" }, line: 1 - }] + code: "var x = .1230000000000000000000000", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a =
;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - }, - errors: [ - { messageId: "noMagic", data: { raw: "1" }, line: 1 }, - { messageId: "noMagic", data: { raw: "2" }, line: 1 }, - { messageId: "noMagic", data: { raw: "3" }, line: 1 } - ] + code: "var x = 1.0000000000000000000000123", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var min, max, mean; min = 1; max = 10; mean = 4;", - options: [{}], - errors: [ - { messageId: "noMagic", data: { raw: "1" }, line: 1 }, - { messageId: "noMagic", data: { raw: "10" }, line: 1 }, - { messageId: "noMagic", data: { raw: "4" }, line: 1 } - ] + code: "var x = 1.230000000000000000000000e35", + errors: [{ messageId: "noLossOfPrecision" }] } ] }); From dd9d66d877a9d414ffa886e736ca7fb4795b67af Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Tue, 31 Dec 2019 10:30:22 -0500 Subject: [PATCH 03/23] Working for decimals and scientific notation --- lib/rules/no-loss-of-precision.js | 113 ++++++++++++++++++++---- tests/lib/rules/no-loss-of-precision.js | 40 ++------- 2 files changed, 104 insertions(+), 49 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 8746b7e3c20..3f6ab3bec75 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -14,16 +14,14 @@ module.exports = { type: "problem", docs: { - description: "disallow numbers that lose precision", + description: "disallow literal numbers that lose precision", category: "Possible Errors", recommended: true, url: "https://eslint.org/docs/rules/no-loss-of-precision" }, - schema: [], - messages: { - noLossOfPrecision: "Numbers only support 16 significant digit precision." + noLossOfPrecision: "This number will lose precision when stored as a Number type." } }, @@ -39,26 +37,111 @@ module.exports = { } /** - * Returns whether the node will lose precision - * @param {Node} node the node literal being evaluated - * @returns {boolean} true if the node will lose precision + * Returns the number stripped of sign and exponential + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function getNumericPortionOfRepresentation(numberAsString) { + return numberAsString.replace("-", "").replace("E", "e").split("e")[0]; + } + + + /** + * Returns the number stripped of leading zeros + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function removeLeadingZeros(numberAsString) { + return numberAsString.replace(/^0?\.0*/u, ""); + } + + /** + * Returns the number stripped of trailing zeros + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function removeTrailingZeros(numberAsString) { + return numberAsString.replace(/0*$/u, ""); + } + + /** + * Returns the number stripped of non-significant digits + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string */ - function willLosePrecision(node) { - if (node.raw.startsWith("0x")) { - return Math.abs(node.value) > Number.MAX_VALUE; + function stripToSignificantDigits(numberAsString) { + const numericPortion = getNumericPortionOfRepresentation(numberAsString); + + if (numericPortion.includes(".")) { + return removeLeadingZeros(numericPortion).replace(".", ""); } + return removeTrailingZeros(numericPortion); + } + + /** + * Returns the requested precision calculated as the number of sig-figs supplied + * @param {Node} node tne node being evaluated + * @returns {number} requested precision level + */ + function getRequestedLevelOfPrecision(node) { + return stripToSignificantDigits(node.raw).length; + } - const strippedNumber = node.raw.split("e")[0].replace("-", ""); + /** + * Returns the smallest value that could be added to the number while keeping the same level of precision + * @param {Node} node the node literal being evaluated + * @returns {number} number the smallest value stored as a number + */ + function getSmallestUnitOfRequestedPrecision(node) { + const numericPortion = getNumericPortionOfRepresentation(node.raw); + const increment = numericPortion.includes(".") + ? `.${"0".repeat(numericPortion.split(".")[1].length - 1)}1` + : `1${"0".repeat(Math.max(0, numericPortion.length - numericPortion.replace(/0*$/u, "").length - 1))}`; + + const nonNumericPortion = node.raw.replace(numericPortion, ""); + + return Number(increment + nonNumericPortion); + } + + /** + * Returns the string representation of the most precise digit + * @param {string} numberAsString the number being evaluated + * @returns {string} the value of the most precise digit + */ + function getMostPreciseDigit(numberAsString) { + const numericPortion = getNumericPortionOfRepresentation(numberAsString); + + return numericPortion.includes(".") ? numericPortion.split(".")[1].slice(-1) + : removeTrailingZeros(numericPortion).slice(-1); + } + + /** + * Checks that the string representation of the evaluated number matches the raw value + * @param {Node} node the node being evaluated + * @returns {boolean} true if they match + */ + function checkRawMatchesValue(node) { + const requestedPrecision = getRequestedLevelOfPrecision(node); + + return getMostPreciseDigit(node.raw) === getMostPreciseDigit(node.value.toPrecision(requestedPrecision)); + } + + + /** + * Returns whether the number will retain accuracy at the requested level of precision + * @param {Node} node the node literal being evaluated + * @returns {boolean} true if the number will retain accuracy at tjerequested level of precision + */ + function checkNumericValue(node) { + const mostPreciseValue = getSmallestUnitOfRequestedPrecision(node); - return strippedNumber.includes(".") - ? Number(strippedNumber.replace(".", "")) > Number.MAX_SAFE_INTEGER - : Number(strippedNumber.replace(/0*$/u, "")) > Number.MAX_SAFE_INTEGER; + return !checkRawMatchesValue(node) || node.value === node.value + mostPreciseValue || node.value === node.value - mostPreciseValue; } return { Literal(node) { - if (isNumber(node) && willLosePrecision(node)) { + if (isNumber(node) && checkNumericValue(node)) { context.report({ messageId: "noLossOfPrecision", node diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index b6fd5c304d6..af7187771e3 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -25,6 +25,7 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = -123.456", "var x = -123456", "var x = 123e34", + "var x = 123.0e34", "var x = 123e-34", "var x = -123e34", "var x = -123e-34", @@ -34,20 +35,10 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = -12.3e-34", "var x = 12300000000000000000000000", "var x = -12300000000000000000000000", - "var x = 0000000000000000000000012300000000000000000000000", - "var x = -0000000000000000000012300000000000000000000000", "var x = 0.00000000000000000000000123", "var x = -0.00000000000000000000000123", - "var x = 9007199254740991", - "var x = -9007199254740991", - "var x = 9007.199254740991", - "var x = -9007.199254740991", - "var x = 900719925474099100", - "var x = -900719925474099100", - "var x = 9007199254740991e3", - "var x = 9007199254740991e-3", - "var x = .9007199254740991", - "var x = -.0009007199254740991" + "var x = 9007199254740991" + ], invalid: [ { @@ -59,29 +50,14 @@ ruleTester.run("no-loss-of-precision", rule, { errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = 900719.9254740992", + code: "var x = 900719.9254740994", errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = -900719.9254740992", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 9007199254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = -9007199254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 900719.9254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = -900719.9254740992e23", + code: "var x = -900719.9254740994", errors: [{ messageId: "noLossOfPrecision" }] }, + { code: "var x = 5123000000000000000000000000001", errors: [{ messageId: "noLossOfPrecision" }] @@ -105,10 +81,6 @@ ruleTester.run("no-loss-of-precision", rule, { { code: "var x = 1.0000000000000000000000123", errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 1.230000000000000000000000e35", - errors: [{ messageId: "noLossOfPrecision" }] } ] }); From a02acc2fb2d8fc583c613321865c6d8ac98ee2d7 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Tue, 31 Dec 2019 10:59:00 -0500 Subject: [PATCH 04/23] Check all digits match instead of just most precise --- lib/rules/no-loss-of-precision.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 3f6ab3bec75..047020a46f4 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -103,18 +103,6 @@ module.exports = { return Number(increment + nonNumericPortion); } - /** - * Returns the string representation of the most precise digit - * @param {string} numberAsString the number being evaluated - * @returns {string} the value of the most precise digit - */ - function getMostPreciseDigit(numberAsString) { - const numericPortion = getNumericPortionOfRepresentation(numberAsString); - - return numericPortion.includes(".") ? numericPortion.split(".")[1].slice(-1) - : removeTrailingZeros(numericPortion).slice(-1); - } - /** * Checks that the string representation of the evaluated number matches the raw value * @param {Node} node the node being evaluated @@ -123,7 +111,7 @@ module.exports = { function checkRawMatchesValue(node) { const requestedPrecision = getRequestedLevelOfPrecision(node); - return getMostPreciseDigit(node.raw) === getMostPreciseDigit(node.value.toPrecision(requestedPrecision)); + return stripToSignificantDigits(node.raw) === stripToSignificantDigits(node.value.toPrecision(requestedPrecision)); } From 7258cbdd5d56935375b90ecd5ef726f006e85015 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Sun, 29 Dec 2019 16:00:11 -0500 Subject: [PATCH 05/23] Created rule and test files --- lib/rules/no-loss-of-precision.js | 68 +++++++ tests/lib/rules/no-loss-of-precision.js | 234 ++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 lib/rules/no-loss-of-precision.js create mode 100644 tests/lib/rules/no-loss-of-precision.js diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js new file mode 100644 index 00000000000..277cef5cc7c --- /dev/null +++ b/lib/rules/no-loss-of-precision.js @@ -0,0 +1,68 @@ +/** + * @fileoverview Rule to flag numbers that will lose significant figure precision at runtime + * @author Jacob Moore + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: "problem", + + docs: { + description: "disallow numbers that lose precision", + category: "Possible Errors", + recommended: true, + url: "https://eslint.org/docs/rules/no-loss-of-precision" + }, + + schema: [], + + messages: { + noLossOfPrecision: "Numbers only support 16 significant digit precision." + } + }, + + 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"; + } + + /** + * Returns whether the node will lose precision + * @param {Node} node the node literal being evaluated + * @returns {boolean} true if the node will lose precision + */ + function willLosePrecision(node) { + const splitNumber = node.value.toString().split("."); + + if (splitNumber.length === 2 && !Number(splitNumber[0])) { + splitNumber.shift(); + } + + return splitNumber.join("").length > 16; + + } + + + return { + Literal(node) { + if (isNumber(node) && willLosePrecision(node)) { + context.report({ + messageId: "noLossOfPrecision" + }); + } + } + }; + } +}; diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js new file mode 100644 index 00000000000..091013deed1 --- /dev/null +++ b/tests/lib/rules/no-loss-of-precision.js @@ -0,0 +1,234 @@ +/** + * @fileoverview Tests for no-loss-of-precision rule. + * @author Jacob Moore + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-loss-of-precision"), + { RuleTester } = require("../../../lib/rule-tester"); + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester(); + +ruleTester.run("no-loss-of-precision", rule, { + valid: [ + "var x = 12345", + "var x = 123.456", + "var x = -123", + "var x = 123e15", + "var x = Number.parseInt(y, 10);", + { + code: "const foo = 42;", + env: { es6: true } + }, + { + code: "var foo = 42;", + options: [{ + enforceConst: false + }], + env: { es6: true } + }, + "var foo = -42;", + { + code: "var foo = 0 + 1 - 2 + -2;", + options: [{ + ignore: [0, 1, 2, -2] + }] + }, + { + code: "var foo = 0 + 1 + 2 + 3 + 4;", + options: [{ + ignore: [0, 1, 2, 3, 4] + }] + }, + "var foo = { bar:10 }", + { + code: "setTimeout(function() {return 1;}, 0);", + options: [{ + ignore: [0, 1] + }] + }, + { + code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", + options: [{ + ignoreArrayIndexes: true + }] + }, + { + code: "var a = ;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + }, + { + code: "var a =
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + } + } + ], + invalid: [ + { + code: "var foo = 42", + options: [{ + enforceConst: true + }], + env: { es6: true }, + errors: [{ messageId: "useConst" }] + }, + { + code: "var foo = 0 + 1;", + errors: [ + { messageId: "noMagic", data: { raw: "0" } }, + { messageId: "noMagic", data: { raw: "1" } } + ] + }, + { + code: "a = a + 5;", + errors: [ + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "a += 5;", + errors: [ + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "var foo = 0 + 1 + -2 + 2;", + errors: [ + { messageId: "noMagic", data: { raw: "0" } }, + { messageId: "noMagic", data: { raw: "1" } }, + { messageId: "noMagic", data: { raw: "-2" } }, + { messageId: "noMagic", data: { raw: "2" } } + ] + }, + { + code: "var foo = 0 + 1 + 2;", + options: [{ + ignore: [0, 1] + }], + errors: [ + { messageId: "noMagic", data: { raw: "2" } } + ] + }, + { + code: "var foo = { bar:10 }", + options: [{ + detectObjects: true + }], + errors: [ + { messageId: "noMagic", data: { raw: "10" } } + ] + }, { + code: "console.log(0x1A + 0x02); console.log(071);", + errors: [ + { messageId: "noMagic", data: { raw: "0x1A" } }, + { messageId: "noMagic", data: { raw: "0x02" } }, + { messageId: "noMagic", data: { raw: "071" } } + ] + }, { + code: "var stats = {avg: 42};", + options: [{ + detectObjects: true + }], + errors: [ + { messageId: "noMagic", data: { raw: "42" } } + ] + }, { + code: "var colors = {}; colors.RED = 2; colors.YELLOW = 3; colors.BLUE = 4 + 5;", + errors: [ + { messageId: "noMagic", data: { raw: "4" } }, + { messageId: "noMagic", data: { raw: "5" } } + ] + }, + { + code: "function getSecondsInMinute() {return 60;}", + errors: [ + { message: "No magic number: 60." } + ] + }, + { + code: "function getNegativeSecondsInMinute() {return -60;}", + errors: [ + { messageId: "noMagic", data: { raw: "-60" } } + ] + }, + { + code: "var Promise = require('bluebird');\n" + + "var MINUTE = 60;\n" + + "var HOUR = 3600;\n" + + "const DAY = 86400;\n" + + "var configObject = {\n" + + "key: 90,\n" + + "another: 10 * 10,\n" + + "10: 'an \"integer\" key'\n" + + "};\n" + + "function getSecondsInDay() {\n" + + " return 24 * HOUR;\n" + + "}\n" + + "function getMillisecondsInDay() {\n" + + "return (getSecondsInDay() *\n" + + "(1000)\n" + + ");\n" + + "}\n" + + "function callSetTimeoutZero(func) {\n" + + "setTimeout(func, 0);\n" + + "}\n" + + "function invokeInTen(func) {\n" + + "setTimeout(func, 10);\n" + + "}\n", + env: { es6: true }, + errors: [ + { messageId: "noMagic", data: { raw: "10" }, line: 7 }, + { messageId: "noMagic", data: { raw: "10" }, line: 7 }, + { messageId: "noMagic", data: { raw: "24" }, line: 11 }, + { messageId: "noMagic", data: { raw: "1000" }, line: 15 }, + { messageId: "noMagic", data: { raw: "0" }, line: 19 }, + { messageId: "noMagic", data: { raw: "10" }, line: 22 } + ] + }, + { + code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", + options: [{}], + errors: [{ + messageId: "noMagic", data: { raw: "3" }, line: 1 + }] + }, + { + code: "var a =
;", + parserOptions: { + ecmaFeatures: { + jsx: true + } + }, + errors: [ + { messageId: "noMagic", data: { raw: "1" }, line: 1 }, + { messageId: "noMagic", data: { raw: "2" }, line: 1 }, + { messageId: "noMagic", data: { raw: "3" }, line: 1 } + ] + }, + { + code: "var min, max, mean; min = 1; max = 10; mean = 4;", + options: [{}], + errors: [ + { messageId: "noMagic", data: { raw: "1" }, line: 1 }, + { messageId: "noMagic", data: { raw: "10" }, line: 1 }, + { messageId: "noMagic", data: { raw: "4" }, line: 1 } + ] + } + ] +}); From 479767b295ba627498feae31558aafcb2a67aae0 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Mon, 30 Dec 2019 00:41:42 -0500 Subject: [PATCH 06/23] Working rules and tests --- lib/rules/no-loss-of-precision.js | 14 +- tests/lib/rules/no-loss-of-precision.js | 236 ++++++------------------ 2 files changed, 66 insertions(+), 184 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 277cef5cc7c..8746b7e3c20 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -44,14 +44,15 @@ module.exports = { * @returns {boolean} true if the node will lose precision */ function willLosePrecision(node) { - const splitNumber = node.value.toString().split("."); - - if (splitNumber.length === 2 && !Number(splitNumber[0])) { - splitNumber.shift(); + if (node.raw.startsWith("0x")) { + return Math.abs(node.value) > Number.MAX_VALUE; } - return splitNumber.join("").length > 16; + const strippedNumber = node.raw.split("e")[0].replace("-", ""); + return strippedNumber.includes(".") + ? Number(strippedNumber.replace(".", "")) > Number.MAX_SAFE_INTEGER + : Number(strippedNumber.replace(/0*$/u, "")) > Number.MAX_SAFE_INTEGER; } @@ -59,7 +60,8 @@ module.exports = { Literal(node) { if (isNumber(node) && willLosePrecision(node)) { context.report({ - messageId: "noLossOfPrecision" + messageId: "noLossOfPrecision", + node }); } } diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index 091013deed1..b6fd5c304d6 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -22,213 +22,93 @@ ruleTester.run("no-loss-of-precision", rule, { valid: [ "var x = 12345", "var x = 123.456", - "var x = -123", - "var x = 123e15", - "var x = Number.parseInt(y, 10);", - { - code: "const foo = 42;", - env: { es6: true } - }, - { - code: "var foo = 42;", - options: [{ - enforceConst: false - }], - env: { es6: true } - }, - "var foo = -42;", - { - code: "var foo = 0 + 1 - 2 + -2;", - options: [{ - ignore: [0, 1, 2, -2] - }] - }, - { - code: "var foo = 0 + 1 + 2 + 3 + 4;", - options: [{ - ignore: [0, 1, 2, 3, 4] - }] - }, - "var foo = { bar:10 }", - { - code: "setTimeout(function() {return 1;}, 0);", - options: [{ - ignore: [0, 1] - }] - }, + "var x = -123.456", + "var x = -123456", + "var x = 123e34", + "var x = 123e-34", + "var x = -123e34", + "var x = -123e-34", + "var x = 12.3e34", + "var x = 12.3e-34", + "var x = -12.3e34", + "var x = -12.3e-34", + "var x = 12300000000000000000000000", + "var x = -12300000000000000000000000", + "var x = 0000000000000000000000012300000000000000000000000", + "var x = -0000000000000000000012300000000000000000000000", + "var x = 0.00000000000000000000000123", + "var x = -0.00000000000000000000000123", + "var x = 9007199254740991", + "var x = -9007199254740991", + "var x = 9007.199254740991", + "var x = -9007.199254740991", + "var x = 900719925474099100", + "var x = -900719925474099100", + "var x = 9007199254740991e3", + "var x = 9007199254740991e-3", + "var x = .9007199254740991", + "var x = -.0009007199254740991" + ], + invalid: [ { - code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", - options: [{ - ignoreArrayIndexes: true - }] + code: "var x = 9007199254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a = ;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - } + code: "var x = -9007199254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a =
;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - } - } - ], - invalid: [ - { - code: "var foo = 42", - options: [{ - enforceConst: true - }], - env: { es6: true }, - errors: [{ messageId: "useConst" }] + code: "var x = 900719.9254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1;", - errors: [ - { messageId: "noMagic", data: { raw: "0" } }, - { messageId: "noMagic", data: { raw: "1" } } - ] + code: "var x = -900719.9254740992", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "a = a + 5;", - errors: [ - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = 9007199254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "a += 5;", - errors: [ - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = -9007199254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1 + -2 + 2;", - errors: [ - { messageId: "noMagic", data: { raw: "0" } }, - { messageId: "noMagic", data: { raw: "1" } }, - { messageId: "noMagic", data: { raw: "-2" } }, - { messageId: "noMagic", data: { raw: "2" } } - ] + code: "var x = 900719.9254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = 0 + 1 + 2;", - options: [{ - ignore: [0, 1] - }], - errors: [ - { messageId: "noMagic", data: { raw: "2" } } - ] + code: "var x = -900719.9254740992e23", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var foo = { bar:10 }", - options: [{ - detectObjects: true - }], - errors: [ - { messageId: "noMagic", data: { raw: "10" } } - ] - }, { - code: "console.log(0x1A + 0x02); console.log(071);", - errors: [ - { messageId: "noMagic", data: { raw: "0x1A" } }, - { messageId: "noMagic", data: { raw: "0x02" } }, - { messageId: "noMagic", data: { raw: "071" } } - ] - }, { - code: "var stats = {avg: 42};", - options: [{ - detectObjects: true - }], - errors: [ - { messageId: "noMagic", data: { raw: "42" } } - ] - }, { - code: "var colors = {}; colors.RED = 2; colors.YELLOW = 3; colors.BLUE = 4 + 5;", - errors: [ - { messageId: "noMagic", data: { raw: "4" } }, - { messageId: "noMagic", data: { raw: "5" } } - ] + code: "var x = 5123000000000000000000000000001", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "function getSecondsInMinute() {return 60;}", - errors: [ - { message: "No magic number: 60." } - ] + code: "var x = -5123000000000000000000000000001", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "function getNegativeSecondsInMinute() {return -60;}", - errors: [ - { messageId: "noMagic", data: { raw: "-60" } } - ] + code: "var x = 1230000000000000000000000.0", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var Promise = require('bluebird');\n" + - "var MINUTE = 60;\n" + - "var HOUR = 3600;\n" + - "const DAY = 86400;\n" + - "var configObject = {\n" + - "key: 90,\n" + - "another: 10 * 10,\n" + - "10: 'an \"integer\" key'\n" + - "};\n" + - "function getSecondsInDay() {\n" + - " return 24 * HOUR;\n" + - "}\n" + - "function getMillisecondsInDay() {\n" + - "return (getSecondsInDay() *\n" + - "(1000)\n" + - ");\n" + - "}\n" + - "function callSetTimeoutZero(func) {\n" + - "setTimeout(func, 0);\n" + - "}\n" + - "function invokeInTen(func) {\n" + - "setTimeout(func, 10);\n" + - "}\n", - env: { es6: true }, - errors: [ - { messageId: "noMagic", data: { raw: "10" }, line: 7 }, - { messageId: "noMagic", data: { raw: "10" }, line: 7 }, - { messageId: "noMagic", data: { raw: "24" }, line: 11 }, - { messageId: "noMagic", data: { raw: "1000" }, line: 15 }, - { messageId: "noMagic", data: { raw: "0" }, line: 19 }, - { messageId: "noMagic", data: { raw: "10" }, line: 22 } - ] + code: "var x = 123.0000000000000000000000", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var data = ['foo', 'bar', 'baz']; var third = data[3];", - options: [{}], - errors: [{ - messageId: "noMagic", data: { raw: "3" }, line: 1 - }] + code: "var x = .1230000000000000000000000", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var a =
;", - parserOptions: { - ecmaFeatures: { - jsx: true - } - }, - errors: [ - { messageId: "noMagic", data: { raw: "1" }, line: 1 }, - { messageId: "noMagic", data: { raw: "2" }, line: 1 }, - { messageId: "noMagic", data: { raw: "3" }, line: 1 } - ] + code: "var x = 1.0000000000000000000000123", + errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var min, max, mean; min = 1; max = 10; mean = 4;", - options: [{}], - errors: [ - { messageId: "noMagic", data: { raw: "1" }, line: 1 }, - { messageId: "noMagic", data: { raw: "10" }, line: 1 }, - { messageId: "noMagic", data: { raw: "4" }, line: 1 } - ] + code: "var x = 1.230000000000000000000000e35", + errors: [{ messageId: "noLossOfPrecision" }] } ] }); From f4864e97a0c68cb8d99d5044f4016029083274ec Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Tue, 31 Dec 2019 10:30:22 -0500 Subject: [PATCH 07/23] Working for decimals and scientific notation --- lib/rules/no-loss-of-precision.js | 113 ++++++++++++++++++++---- tests/lib/rules/no-loss-of-precision.js | 40 ++------- 2 files changed, 104 insertions(+), 49 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 8746b7e3c20..3f6ab3bec75 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -14,16 +14,14 @@ module.exports = { type: "problem", docs: { - description: "disallow numbers that lose precision", + description: "disallow literal numbers that lose precision", category: "Possible Errors", recommended: true, url: "https://eslint.org/docs/rules/no-loss-of-precision" }, - schema: [], - messages: { - noLossOfPrecision: "Numbers only support 16 significant digit precision." + noLossOfPrecision: "This number will lose precision when stored as a Number type." } }, @@ -39,26 +37,111 @@ module.exports = { } /** - * Returns whether the node will lose precision - * @param {Node} node the node literal being evaluated - * @returns {boolean} true if the node will lose precision + * Returns the number stripped of sign and exponential + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function getNumericPortionOfRepresentation(numberAsString) { + return numberAsString.replace("-", "").replace("E", "e").split("e")[0]; + } + + + /** + * Returns the number stripped of leading zeros + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function removeLeadingZeros(numberAsString) { + return numberAsString.replace(/^0?\.0*/u, ""); + } + + /** + * Returns the number stripped of trailing zeros + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string + */ + function removeTrailingZeros(numberAsString) { + return numberAsString.replace(/0*$/u, ""); + } + + /** + * Returns the number stripped of non-significant digits + * @param {string} numberAsString the string representation of the number + * @returns {string} the stripped string */ - function willLosePrecision(node) { - if (node.raw.startsWith("0x")) { - return Math.abs(node.value) > Number.MAX_VALUE; + function stripToSignificantDigits(numberAsString) { + const numericPortion = getNumericPortionOfRepresentation(numberAsString); + + if (numericPortion.includes(".")) { + return removeLeadingZeros(numericPortion).replace(".", ""); } + return removeTrailingZeros(numericPortion); + } + + /** + * Returns the requested precision calculated as the number of sig-figs supplied + * @param {Node} node tne node being evaluated + * @returns {number} requested precision level + */ + function getRequestedLevelOfPrecision(node) { + return stripToSignificantDigits(node.raw).length; + } - const strippedNumber = node.raw.split("e")[0].replace("-", ""); + /** + * Returns the smallest value that could be added to the number while keeping the same level of precision + * @param {Node} node the node literal being evaluated + * @returns {number} number the smallest value stored as a number + */ + function getSmallestUnitOfRequestedPrecision(node) { + const numericPortion = getNumericPortionOfRepresentation(node.raw); + const increment = numericPortion.includes(".") + ? `.${"0".repeat(numericPortion.split(".")[1].length - 1)}1` + : `1${"0".repeat(Math.max(0, numericPortion.length - numericPortion.replace(/0*$/u, "").length - 1))}`; + + const nonNumericPortion = node.raw.replace(numericPortion, ""); + + return Number(increment + nonNumericPortion); + } + + /** + * Returns the string representation of the most precise digit + * @param {string} numberAsString the number being evaluated + * @returns {string} the value of the most precise digit + */ + function getMostPreciseDigit(numberAsString) { + const numericPortion = getNumericPortionOfRepresentation(numberAsString); + + return numericPortion.includes(".") ? numericPortion.split(".")[1].slice(-1) + : removeTrailingZeros(numericPortion).slice(-1); + } + + /** + * Checks that the string representation of the evaluated number matches the raw value + * @param {Node} node the node being evaluated + * @returns {boolean} true if they match + */ + function checkRawMatchesValue(node) { + const requestedPrecision = getRequestedLevelOfPrecision(node); + + return getMostPreciseDigit(node.raw) === getMostPreciseDigit(node.value.toPrecision(requestedPrecision)); + } + + + /** + * Returns whether the number will retain accuracy at the requested level of precision + * @param {Node} node the node literal being evaluated + * @returns {boolean} true if the number will retain accuracy at tjerequested level of precision + */ + function checkNumericValue(node) { + const mostPreciseValue = getSmallestUnitOfRequestedPrecision(node); - return strippedNumber.includes(".") - ? Number(strippedNumber.replace(".", "")) > Number.MAX_SAFE_INTEGER - : Number(strippedNumber.replace(/0*$/u, "")) > Number.MAX_SAFE_INTEGER; + return !checkRawMatchesValue(node) || node.value === node.value + mostPreciseValue || node.value === node.value - mostPreciseValue; } return { Literal(node) { - if (isNumber(node) && willLosePrecision(node)) { + if (isNumber(node) && checkNumericValue(node)) { context.report({ messageId: "noLossOfPrecision", node diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index b6fd5c304d6..af7187771e3 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -25,6 +25,7 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = -123.456", "var x = -123456", "var x = 123e34", + "var x = 123.0e34", "var x = 123e-34", "var x = -123e34", "var x = -123e-34", @@ -34,20 +35,10 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = -12.3e-34", "var x = 12300000000000000000000000", "var x = -12300000000000000000000000", - "var x = 0000000000000000000000012300000000000000000000000", - "var x = -0000000000000000000012300000000000000000000000", "var x = 0.00000000000000000000000123", "var x = -0.00000000000000000000000123", - "var x = 9007199254740991", - "var x = -9007199254740991", - "var x = 9007.199254740991", - "var x = -9007.199254740991", - "var x = 900719925474099100", - "var x = -900719925474099100", - "var x = 9007199254740991e3", - "var x = 9007199254740991e-3", - "var x = .9007199254740991", - "var x = -.0009007199254740991" + "var x = 9007199254740991" + ], invalid: [ { @@ -59,29 +50,14 @@ ruleTester.run("no-loss-of-precision", rule, { errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = 900719.9254740992", + code: "var x = 900719.9254740994", errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = -900719.9254740992", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 9007199254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = -9007199254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 900719.9254740992e23", - errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = -900719.9254740992e23", + code: "var x = -900719.9254740994", errors: [{ messageId: "noLossOfPrecision" }] }, + { code: "var x = 5123000000000000000000000000001", errors: [{ messageId: "noLossOfPrecision" }] @@ -105,10 +81,6 @@ ruleTester.run("no-loss-of-precision", rule, { { code: "var x = 1.0000000000000000000000123", errors: [{ messageId: "noLossOfPrecision" }] - }, - { - code: "var x = 1.230000000000000000000000e35", - errors: [{ messageId: "noLossOfPrecision" }] } ] }); From 64a61bf7f3185c80c70a8a013987fff4ccbc58e2 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Tue, 31 Dec 2019 10:59:00 -0500 Subject: [PATCH 08/23] Check all digits match instead of just most precise --- lib/rules/no-loss-of-precision.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 3f6ab3bec75..047020a46f4 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -103,18 +103,6 @@ module.exports = { return Number(increment + nonNumericPortion); } - /** - * Returns the string representation of the most precise digit - * @param {string} numberAsString the number being evaluated - * @returns {string} the value of the most precise digit - */ - function getMostPreciseDigit(numberAsString) { - const numericPortion = getNumericPortionOfRepresentation(numberAsString); - - return numericPortion.includes(".") ? numericPortion.split(".")[1].slice(-1) - : removeTrailingZeros(numericPortion).slice(-1); - } - /** * Checks that the string representation of the evaluated number matches the raw value * @param {Node} node the node being evaluated @@ -123,7 +111,7 @@ module.exports = { function checkRawMatchesValue(node) { const requestedPrecision = getRequestedLevelOfPrecision(node); - return getMostPreciseDigit(node.raw) === getMostPreciseDigit(node.value.toPrecision(requestedPrecision)); + return stripToSignificantDigits(node.raw) === stripToSignificantDigits(node.value.toPrecision(requestedPrecision)); } From d8edf525ec6e7e813c1861f1407a5d20ab92afaa Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Sun, 5 Jan 2020 10:45:43 -0500 Subject: [PATCH 09/23] Expanded rule to non-base ten numbers --- lib/rules/no-loss-of-precision.js | 145 ++++++++++++++++-------- tests/lib/rules/no-loss-of-precision.js | 76 +++++++++++-- 2 files changed, 169 insertions(+), 52 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 047020a46f4..cb863505e48 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -29,7 +29,7 @@ module.exports = { /** * Returns whether the node is number literal - * @param {Node} node the node literal being evaluated + * @param {ASTNode} node the node being evaluated * @returns {boolean} true if the node is a number literal */ function isNumber(node) { @@ -37,14 +37,45 @@ module.exports = { } /** - * Returns the number stripped of sign and exponential - * @param {string} numberAsString the string representation of the number - * @returns {string} the stripped string + * Checks whether the number is not base ten + * @param {ASTNode} node the node being evaluated + * @returns {boolean} true if the node is not in base ten + */ + function isNotBaseTen(node) { + const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; + + return prefixes.some(prefix => node.raw.startsWith(prefix)) || + (node.raw.startsWith("0") && !node.raw.startsWith("0.")); + } + + /** + * 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 getNumericPortionOfRepresentation(numberAsString) { - return numberAsString.replace("-", "").replace("E", "e").split("e")[0]; + function checkNotBaseTenNumber(node) { + const rawString = node.raw.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.slice(0, 1)}.${stringNumber.slice(1)}`; + } /** * Returns the number stripped of leading zeros @@ -52,7 +83,7 @@ module.exports = { * @returns {string} the stripped string */ function removeLeadingZeros(numberAsString) { - return numberAsString.replace(/^0?\.0*/u, ""); + return numberAsString.replace(/^0*/u, ""); } /** @@ -65,71 +96,95 @@ module.exports = { } /** - * Returns the number stripped of non-significant digits - * @param {string} numberAsString the string representation of the number - * @returns {string} the stripped string + * Converts an integer to to an object containing the 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 the integer's coefficient and order of magnitude */ - function stripToSignificantDigits(numberAsString) { - const numericPortion = getNumericPortionOfRepresentation(numberAsString); + function normalizeInteger(stringInteger) { + const significantDigits = removeTrailingZeros(stringInteger); - if (numericPortion.includes(".")) { - return removeLeadingZeros(numericPortion).replace(".", ""); - } - return removeTrailingZeros(numericPortion); + return { + magnitude: stringInteger.length - 1, + coefficient: addDecimalPointToNumber(significantDigits) + }; } /** - * Returns the requested precision calculated as the number of sig-figs supplied - * @param {Node} node tne node being evaluated - * @returns {number} requested precision level + * + * Converts a float to to an object containing the the floats's coefficient and order of magnitude + * @param {string} stringFloat the string representation of the floate being converted + * @returns {Object} the object containing the the integer's coefficient and order of magnitude */ - function getRequestedLevelOfPrecision(node) { - return stripToSignificantDigits(node.raw).length; + function normalizeFloat(stringFloat) { + if (stringFloat.startsWith("0") || stringFloat.startsWith(".")) { + const decimalDigits = stringFloat.split(".").pop(); + const significantDigits = removeLeadingZeros(decimalDigits); + + return { + magnitude: significantDigits.length - decimalDigits.length - 1, + coefficient: addDecimalPointToNumber(significantDigits) + }; + + } + return { + magnitude: stringFloat.indexOf(".") - 1, + coefficient: addDecimalPointToNumber(stringFloat.replace(".", "")) + + }; } + /** - * Returns the smallest value that could be added to the number while keeping the same level of precision - * @param {Node} node the node literal being evaluated - * @returns {number} number the smallest value stored as a number + * 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 getSmallestUnitOfRequestedPrecision(node) { - const numericPortion = getNumericPortionOfRepresentation(node.raw); - const increment = numericPortion.includes(".") - ? `.${"0".repeat(numericPortion.split(".")[1].length - 1)}1` - : `1${"0".repeat(Math.max(0, numericPortion.length - numericPortion.replace(/0*$/u, "").length - 1))}`; + function convertNumberToScientificNotation(stringNumber) { + const absoluteValue = stringNumber.startsWith("-") ? stringNumber.replace("-", "") : stringNumber; + const splitNumber = absoluteValue.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}`; - const nonNumericPortion = node.raw.replace(numericPortion, ""); - - return Number(increment + nonNumericPortion); } /** - * Checks that the string representation of the evaluated number matches the raw value + * 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 match + * @returns {boolean} true if they don not match */ - function checkRawMatchesValue(node) { - const requestedPrecision = getRequestedLevelOfPrecision(node); + function checkBaseTenNumber(node) { + const normalizedRawNumber = convertNumberToScientificNotation(node.raw); + const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length; + + if (requestedPrecision > 100) { + return true; + } + const storedNumber = node.value.toPrecision(requestedPrecision); + const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber); - return stripToSignificantDigits(node.raw) === stripToSignificantDigits(node.value.toPrecision(requestedPrecision)); + return normalizedRawNumber !== normalizedStoredNumber; } /** - * Returns whether the number will retain accuracy at the requested level of precision - * @param {Node} node the node literal being evaluated - * @returns {boolean} true if the number will retain accuracy at tjerequested level of precision + * 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 match */ - function checkNumericValue(node) { - const mostPreciseValue = getSmallestUnitOfRequestedPrecision(node); - - return !checkRawMatchesValue(node) || node.value === node.value + mostPreciseValue || node.value === node.value - mostPreciseValue; + function checkNumber(node) { + return isNotBaseTen(node) ? checkNotBaseTenNumber(node) : checkBaseTenNumber(node); } return { Literal(node) { - if (isNumber(node) && checkNumericValue(node)) { + if (node.value && isNumber(node) && checkNumber(node)) { context.report({ messageId: "noLossOfPrecision", node diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index af7187771e3..6383bf858c2 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -1,6 +1,6 @@ /** - * @fileoverview Tests for no-loss-of-precision rule. - * @author Jacob Moore + *@fileoverview Tests for no-loss-of-precision rule. + *@author Jacob Moore */ "use strict"; @@ -20,6 +20,7 @@ const ruleTester = new RuleTester(); ruleTester.run("no-loss-of-precision", rule, { valid: [ + "var x = 12345", "var x = 123.456", "var x = -123.456", @@ -37,16 +38,40 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = -12300000000000000000000000", "var x = 0.00000000000000000000000123", "var x = -0.00000000000000000000000123", - "var x = 9007199254740991" + "var x = 9007199254740991", + "var x = 0", + "var x = 0.0", + "var x = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000", + "var x = -0", + "var x = 123.0000000000000000000000", + + + { code: "var x = 0b11111111111111111111111111111111111111111111111111111", parserOptions: { ecmaVersion: 6 } }, + { code: "var x = 0B11111111111111111111111111111111111111111111111111111", parserOptions: { ecmaVersion: 6 } }, + + { code: "var x = 0o377777777777777777", parserOptions: { ecmaVersion: 6 } }, + { code: "var x = 0O377777777777777777", parserOptions: { ecmaVersion: 6 } }, + "var x = 0377777777777777777", + + "var x = 0x1FFFFFFFFFFFFF", + "var x = 0X1FFFFFFFFFFFFF" ], invalid: [ { - code: "var x = 9007199254740992", + code: "var x = 9007199254740993", errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = -9007199254740992", + code: "var x = 9007199254740.993e3", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 9.007199254740993e15", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = -9007199254740993", errors: [{ messageId: "noLossOfPrecision" }] }, { @@ -71,7 +96,15 @@ ruleTester.run("no-loss-of-precision", rule, { errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = 123.0000000000000000000000", + code: "var x = 1.0000000000000000000000123", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 17498005798264095394980017816940970922825355447145699491406164851279623993595007385788105416184430592", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 2e999", errors: [{ messageId: "noLossOfPrecision" }] }, { @@ -79,8 +112,37 @@ ruleTester.run("no-loss-of-precision", rule, { errors: [{ messageId: "noLossOfPrecision" }] }, { - code: "var x = 1.0000000000000000000000123", + code: "var x = 0b100000000000000000000000000000000000000000000000000001", + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0B100000000000000000000000000000000000000000000000000001", + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0o400000000000000001", + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0O400000000000000001", + parserOptions: { ecmaVersion: 6 }, + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0400000000000000001", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0x20000000000001", + errors: [{ messageId: "noLossOfPrecision" }] + }, + { + code: "var x = 0X20000000000001", errors: [{ messageId: "noLossOfPrecision" }] } + ] }); From 3a9be96c123310b90d2810d6c31b65320b043ce4 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Sun, 5 Jan 2020 15:28:07 -0500 Subject: [PATCH 10/23] Added docs and updated config files --- conf/eslint-recommended.js | 1 + docs/rules/no-loss-of-precision.md | 32 ++++++++++++++++++++++++++++++ lib/rules/index.js | 1 + tools/rule-types.json | 3 ++- 4 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 docs/rules/no-loss-of-precision.md diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js index e915ec44904..160c40571c7 100644 --- a/conf/eslint-recommended.js +++ b/conf/eslint-recommended.js @@ -40,6 +40,7 @@ module.exports = { "no-inner-declarations": "error", "no-invalid-regexp": "error", "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", "no-misleading-character-class": "error", "no-mixed-spaces-and-tabs": "error", "no-new-symbol": "error", diff --git a/docs/rules/no-loss-of-precision.md b/docs/rules/no-loss-of-precision.md new file mode 100644 index 00000000000..42786ef5928 --- /dev/null +++ b/docs/rules/no-loss-of-precision.md @@ -0,0 +1,32 @@ +# Disallow Number Literals That Lose Precision (no-loss-of-precision) + +This rule would disallow the use of number literals that immediately lose precision at runtime when converted to a JS `Number` due to 64-bit floating-point rounding. + +## Rule Details + +In JS `Number`s are stored as double precision floating point numbers according to the [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754). Because of this, numbers can only retain accuracy up to a certain amount of digits. If the programmer enters additional digits, those digits will be lost in the conversion to the `Number` type and will result in unexpected behavior. + +Examples of **incorrect** code for this rule: + +```js +/*eslint no-loss-of-precision: "error"*/ + +const x = 9007199254740993 +const x = 5123000000000000000000000000001 +const x = 1230000000000000000000000.0 +const x = .1230000000000000000000000 +const x = 0X20000000000001 +``` + +Examples of **correct** code for this rule: + +```js +/*eslint no-loss-of-precision: "error"*/ + +const x = 12345 +const x = 123.456 +const x = 123e34 +const x = 12300000000000000000000000 +const x = 0x1FFFFFFFFFFFFF +const x = 9007199254740991 +``` diff --git a/lib/rules/index.js b/lib/rules/index.js index 494c650e4eb..42dac77c1e5 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -147,6 +147,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({ "no-lone-blocks": () => require("./no-lone-blocks"), "no-lonely-if": () => require("./no-lonely-if"), "no-loop-func": () => require("./no-loop-func"), + "no-loss-of-precision": () => require("./no-loss-of-precision"), "no-magic-numbers": () => require("./no-magic-numbers"), "no-misleading-character-class": () => require("./no-misleading-character-class"), "no-mixed-operators": () => require("./no-mixed-operators"), diff --git a/tools/rule-types.json b/tools/rule-types.json index d78abfdfdfe..c556195dcfb 100644 --- a/tools/rule-types.json +++ b/tools/rule-types.json @@ -134,6 +134,7 @@ "no-lone-blocks": "suggestion", "no-lonely-if": "suggestion", "no-loop-func": "suggestion", + "no-loss-of-precision": "problem", "no-magic-numbers": "suggestion", "no-misleading-character-class": "problem", "no-mixed-operators": "suggestion", @@ -275,4 +276,4 @@ "wrap-regex": "layout", "yield-star-spacing": "layout", "yoda": "suggestion" -} +} \ No newline at end of file From e7ca7b7dc1098eff1e184e62380f7133d153d346 Mon Sep 17 00:00:00 2001 From: jmoore914 <30698083+jmoore914@users.noreply.github.com> Date: Fri, 31 Jan 2020 15:09:57 -0500 Subject: [PATCH 11/23] Update docs/rules/no-loss-of-precision.md Co-Authored-By: Teddy Katz --- docs/rules/no-loss-of-precision.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rules/no-loss-of-precision.md b/docs/rules/no-loss-of-precision.md index 42786ef5928..e1fcc4e09d7 100644 --- a/docs/rules/no-loss-of-precision.md +++ b/docs/rules/no-loss-of-precision.md @@ -4,7 +4,7 @@ This rule would disallow the use of number literals that immediately lose precis ## Rule Details -In JS `Number`s are stored as double precision floating point numbers according to the [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754). Because of this, numbers can only retain accuracy up to a certain amount of digits. If the programmer enters additional digits, those digits will be lost in the conversion to the `Number` type and will result in unexpected behavior. +In JS, `Number`s are stored as double-precision floating-point numbers according to the [IEEE 754 standard](https://en.wikipedia.org/wiki/IEEE_754). Because of this, numbers can only retain accuracy up to a certain amount of digits. If the programmer enters additional digits, those digits will be lost in the conversion to the `Number` type and will result in unexpected behavior. Examples of **incorrect** code for this rule: From 6238203d4523f155b3e538e2d75ed65e2c784ad5 Mon Sep 17 00:00:00 2001 From: jmoore914 <30698083+jmoore914@users.noreply.github.com> Date: Fri, 31 Jan 2020 15:10:10 -0500 Subject: [PATCH 12/23] Update lib/rules/no-loss-of-precision.js Co-Authored-By: Teddy Katz --- lib/rules/no-loss-of-precision.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 5cb1d169593..c0c6551d836 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -21,7 +21,7 @@ module.exports = { }, schema: [], messages: { - noLossOfPrecision: "This number will lose precision when stored as a Number type." + noLossOfPrecision: "This number literal will lose precision at runtime." } }, From 56f1aaeba84a9d5deceb4a2781945f3a528e7e1a Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 15:10:55 -0500 Subject: [PATCH 13/23] Removed rule from recommended --- conf/eslint-recommended.js | 1 - 1 file changed, 1 deletion(-) diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js index 160c40571c7..e915ec44904 100644 --- a/conf/eslint-recommended.js +++ b/conf/eslint-recommended.js @@ -40,7 +40,6 @@ module.exports = { "no-inner-declarations": "error", "no-invalid-regexp": "error", "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", "no-misleading-character-class": "error", "no-mixed-spaces-and-tabs": "error", "no-new-symbol": "error", From 5fcd4737469f49cba41bbd847c6b2ee9dbc8f9f2 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 15:15:27 -0500 Subject: [PATCH 14/23] Renamed functions; fixed function description --- lib/rules/no-loss-of-precision.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index c0c6551d836..48f2530902f 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -53,7 +53,7 @@ module.exports = { * @param {Node} node the node being evaluated * @returns {boolean} true if they do not match */ - function checkNotBaseTenNumber(node) { + function notBaseTenLosesPrecision(node) { const rawString = node.raw.toUpperCase(); let base = 0; @@ -156,9 +156,9 @@ module.exports = { /** * 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 don not match + * @returns {boolean} true if they do not match */ - function checkBaseTenNumber(node) { + function baseTenLosesPrecision(node) { const normalizedRawNumber = convertNumberToScientificNotation(node.raw); const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length; @@ -175,16 +175,16 @@ module.exports = { /** * 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 match + * @returns {boolean} true if they do not match */ - function checkNumber(node) { - return isNotBaseTen(node) ? checkNotBaseTenNumber(node) : checkBaseTenNumber(node); + function losesPrecision(node) { + return isNotBaseTen(node) ? notBaseTenLosesPrecision(node) : baseTenLosesPrecision(node); } return { Literal(node) { - if (node.value && isNumber(node) && checkNumber(node)) { + if (node.value && isNumber(node) && losesPrecision(node)) { context.report({ messageId: "noLossOfPrecision", node From a6e3df87a56d8c5715413aab78666379e93cac97 Mon Sep 17 00:00:00 2001 From: jmoore914 <30698083+jmoore914@users.noreply.github.com> Date: Fri, 31 Jan 2020 15:19:19 -0500 Subject: [PATCH 15/23] Update lib/rules/no-loss-of-precision.js Co-Authored-By: Teddy Katz --- lib/rules/no-loss-of-precision.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index c0c6551d836..da4c08e5bf2 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -112,7 +112,7 @@ module.exports = { /** * * Converts a float to to an object containing the the floats's coefficient and order of magnitude - * @param {string} stringFloat the string representation of the floate being converted + * @param {string} stringFloat the string representation of the float being converted * @returns {Object} the object containing the the integer's coefficient and order of magnitude */ function normalizeFloat(stringFloat) { From bb40fb4ba8788bc140603ba97137fd2dd6ed59cb Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 15:20:48 -0500 Subject: [PATCH 16/23] Removed always-true conditional --- lib/rules/no-loss-of-precision.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 48f2530902f..b0129fb183f 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -140,8 +140,7 @@ module.exports = { * @returns {string} the number converted to scientific notation */ function convertNumberToScientificNotation(stringNumber) { - const absoluteValue = stringNumber.startsWith("-") ? stringNumber.replace("-", "") : stringNumber; - const splitNumber = absoluteValue.replace("E", "e").split("e"); + const splitNumber = stringNumber.replace("E", "e").split("e"); const originalCoefficient = splitNumber[0]; const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient) : normalizeInteger(originalCoefficient); From 3a0d25188e4d752f98e76f7f655058f91e6b24fb Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 15:21:23 -0500 Subject: [PATCH 17/23] Removed rule from recommended --- lib/rules/no-loss-of-precision.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index b0129fb183f..216cf56445c 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -16,7 +16,7 @@ module.exports = { docs: { description: "disallow literal numbers that lose precision", category: "Possible Errors", - recommended: true, + recommended: false, url: "https://eslint.org/docs/rules/no-loss-of-precision" }, schema: [], From 4fb6481fd9ff04a3024345b2e33e48c9faab6455 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 16:32:13 -0500 Subject: [PATCH 18/23] Fixing octal cases --- lib/rules/no-loss-of-precision.js | 29 +++++++++++++++++++------ tests/lib/rules/no-loss-of-precision.js | 3 +++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 216cf56445c..1c1516bc396 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -36,6 +36,16 @@ module.exports = { return typeof node.value === "number"; } + + /** + * Returns whether the string only contains octal digits + * @param {string} rawString the string representation of the number being evaluated + * @returns {boolean} true if the string contains only octal digits + */ + function isOctalDigitsOnly(rawString) { + return rawString.split().every(digit => parseInt(digit, 10) < 8); + } + /** * Checks whether the number is not base ten * @param {ASTNode} node the node being evaluated @@ -45,7 +55,10 @@ module.exports = { const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; return prefixes.some(prefix => node.raw.startsWith(prefix)) || - (node.raw.startsWith("0") && !node.raw.startsWith("0.")); + (node.raw.startsWith("0") && + !node.raw.startsWith("0e") && + !node.raw.startsWith("0.") && + isOctalDigitsOnly(node.raw)); } /** @@ -101,10 +114,10 @@ module.exports = { * @returns {Object} the object containing the the integer's coefficient and order of magnitude */ function normalizeInteger(stringInteger) { - const significantDigits = removeTrailingZeros(stringInteger); + const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger)); return { - magnitude: stringInteger.length - 1, + magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1, coefficient: addDecimalPointToNumber(significantDigits) }; } @@ -116,8 +129,10 @@ module.exports = { * @returns {Object} the object containing the the integer's coefficient and order of magnitude */ function normalizeFloat(stringFloat) { - if (stringFloat.startsWith("0") || stringFloat.startsWith(".")) { - const decimalDigits = stringFloat.split(".").pop(); + const trimmedFloat = removeLeadingZeros(stringFloat); + + if (trimmedFloat.startsWith(".")) { + const decimalDigits = trimmedFloat.split(".").pop(); const significantDigits = removeLeadingZeros(decimalDigits); return { @@ -127,8 +142,8 @@ module.exports = { } return { - magnitude: stringFloat.indexOf(".") - 1, - coefficient: addDecimalPointToNumber(stringFloat.replace(".", "")) + magnitude: trimmedFloat.indexOf(".") - 1, + coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", "")) }; } diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index 44b90c64a08..08b8ba8a688 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -43,6 +43,9 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000", "var x = -0", "var x = 123.0000000000000000000000", + "var x = 019.5", + "var x = 0195", + "var x = 0e5", { code: "var x = 0b11111111111111111111111111111111111111111111111111111", parserOptions: { ecmaVersion: 6 } }, From f293b93b60c48a41f49c30d61d3c484129dc77e0 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 16:45:02 -0500 Subject: [PATCH 19/23] Working with octals --- lib/rules/no-loss-of-precision.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 1c1516bc396..3112c0542b5 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -43,7 +43,7 @@ module.exports = { * @returns {boolean} true if the string contains only octal digits */ function isOctalDigitsOnly(rawString) { - return rawString.split().every(digit => parseInt(digit, 10) < 8); + return rawString.split("").every(digit => parseInt(digit, 10) < 8); } /** From 9eccda27e98b7e203658c3e511c32a74f5b6a5b4 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 31 Jan 2020 19:11:04 -0500 Subject: [PATCH 20/23] Changed isNotBaseTen to isBaseTen --- lib/rules/no-loss-of-precision.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index beb80cd9f4e..4d679a6149d 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -47,18 +47,18 @@ module.exports = { } /** - * Checks whether the number is not base ten + * Checks whether the number is base ten * @param {ASTNode} node the node being evaluated - * @returns {boolean} true if the node is not in base ten + * @returns {boolean} true if the node is in base ten */ - function isNotBaseTen(node) { + function isBaseTen(node) { const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; - return prefixes.some(prefix => node.raw.startsWith(prefix)) || - (node.raw.startsWith("0") && - !node.raw.startsWith("0e") && - !node.raw.startsWith("0.") && - isOctalDigitsOnly(node.raw)); + return prefixes.every(prefix => !node.raw.startsWith(prefix)) && + (!node.raw.startsWith("0") || + node.raw.startsWith("0e") || + node.raw.startsWith("0.") || + !isOctalDigitsOnly(node.raw)); } /** @@ -192,7 +192,7 @@ module.exports = { * @returns {boolean} true if they do not match */ function losesPrecision(node) { - return isNotBaseTen(node) ? notBaseTenLosesPrecision(node) : baseTenLosesPrecision(node); + return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node); } From ec9b14e4deebd4410ff4316884625d95164f8491 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 7 Feb 2020 08:04:12 -0500 Subject: [PATCH 21/23] Simplify isBaseTen test --- lib/rules/no-loss-of-precision.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/rules/no-loss-of-precision.js b/lib/rules/no-loss-of-precision.js index 4d679a6149d..b95677c2e0b 100644 --- a/lib/rules/no-loss-of-precision.js +++ b/lib/rules/no-loss-of-precision.js @@ -37,15 +37,6 @@ module.exports = { } - /** - * Returns whether the string only contains octal digits - * @param {string} rawString the string representation of the number being evaluated - * @returns {boolean} true if the string contains only octal digits - */ - function isOctalDigitsOnly(rawString) { - return rawString.split("").every(digit => parseInt(digit, 10) < 8); - } - /** * Checks whether the number is base ten * @param {ASTNode} node the node being evaluated @@ -55,10 +46,7 @@ module.exports = { const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; return prefixes.every(prefix => !node.raw.startsWith(prefix)) && - (!node.raw.startsWith("0") || - node.raw.startsWith("0e") || - node.raw.startsWith("0.") || - !isOctalDigitsOnly(node.raw)); + !/^0[0-7]+$/u.test(node.raw); } /** From e82aaf5f711e7ccca7246bbf70813f2895890bba Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 20 Mar 2020 09:38:17 -0400 Subject: [PATCH 22/23] Added regression tests --- tests/lib/rules/no-loss-of-precision.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index 08b8ba8a688..3b55b173321 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -56,7 +56,15 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = 0377777777777777777", "var x = 0x1FFFFFFFFFFFFF", - "var x = 0X1FFFFFFFFFFFFF" + "var x = 0X1FFFFFFFFFFFFF", + "var x = true", + "var x = 'abc'", + "var x = ''", + "var x = null", + "var x = undefined", + "var x = {}", + "var x = ['a', 'b']", + "var x = new Date()" ], invalid: [ From 4398188e619b461e3c2a030291fdd0ab8acfa151 Mon Sep 17 00:00:00 2001 From: jmoore914 Date: Fri, 20 Mar 2020 09:40:39 -0400 Subject: [PATCH 23/23] Additional regression test --- tests/lib/rules/no-loss-of-precision.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/lib/rules/no-loss-of-precision.js b/tests/lib/rules/no-loss-of-precision.js index 3b55b173321..fd2fb204524 100644 --- a/tests/lib/rules/no-loss-of-precision.js +++ b/tests/lib/rules/no-loss-of-precision.js @@ -64,7 +64,8 @@ ruleTester.run("no-loss-of-precision", rule, { "var x = undefined", "var x = {}", "var x = ['a', 'b']", - "var x = new Date()" + "var x = new Date()", + "var x = '9007199254740993'" ], invalid: [