New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New: no-loss-of-precision (fixes #11279) #12747
Changes from 23 commits
bece581
f95678e
dd9d66d
a02acc2
7258cbd
479767b
f4864e9
64a61bf
d8edf52
1f4b1ea
3a9be96
e7ca7b7
6238203
56f1aae
ef8ec88
5fcd473
a6e3df8
bb40fb4
3a0d251
4fb6481
f293b93
ff1e610
9eccda2
ec9b14e
76179a2
d39d8e4
e82aaf5
4398188
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
/** | ||
* @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 literal numbers that lose precision", | ||
category: "Possible Errors", | ||
recommended: false, | ||
url: "https://eslint.org/docs/rules/no-loss-of-precision" | ||
}, | ||
schema: [], | ||
messages: { | ||
noLossOfPrecision: "This number literal will lose precision at runtime." | ||
} | ||
}, | ||
|
||
create(context) { | ||
|
||
/** | ||
* Returns whether the node is number literal | ||
* @param {Node} node the node literal being evaluated | ||
* @returns {boolean} true if the node is a number literal | ||
*/ | ||
function isNumber(node) { | ||
return typeof node.value === "number"; | ||
} | ||
|
||
|
||
/** | ||
* 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 | ||
* @returns {boolean} true if the node is in base ten | ||
*/ | ||
function isBaseTen(node) { | ||
const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"]; | ||
|
||
return prefixes.every(prefix => !node.raw.startsWith(prefix)) && | ||
(!node.raw.startsWith("0") || | ||
node.raw.startsWith("0e") || | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will be incorrect for something like |
||
node.raw.startsWith("0.") || | ||
!isOctalDigitsOnly(node.raw)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could be simplified to: return prefixes.every(prefix => !node.raw.startsWith(prefix)) &&
!/^0[0-7]+$/.test(node.raw) |
||
} | ||
|
||
/** | ||
* Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type | ||
* @param {Node} node the node being evaluated | ||
* @returns {boolean} true if they do not match | ||
*/ | ||
function notBaseTenLosesPrecision(node) { | ||
const rawString = 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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Can you add some tests that use leading zeros on non-base-10 literals (e.g. |
||
} | ||
|
||
/** | ||
* 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 | ||
* @param {string} numberAsString the string representation of the number | ||
* @returns {string} the stripped string | ||
*/ | ||
function removeLeadingZeros(numberAsString) { | ||
return numberAsString.replace(/^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, ""); | ||
} | ||
|
||
/** | ||
* 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 normalizeInteger(stringInteger) { | ||
const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger)); | ||
|
||
return { | ||
magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1, | ||
coefficient: addDecimalPointToNumber(significantDigits) | ||
}; | ||
} | ||
|
||
/** | ||
* | ||
* Converts a float to to an object containing the the floats's coefficient and order of magnitude | ||
* @param {string} stringFloat the string representation of the float being converted | ||
* @returns {Object} the object containing the the integer's coefficient and order of magnitude | ||
*/ | ||
function normalizeFloat(stringFloat) { | ||
const trimmedFloat = removeLeadingZeros(stringFloat); | ||
|
||
if (trimmedFloat.startsWith(".")) { | ||
const decimalDigits = trimmedFloat.split(".").pop(); | ||
const significantDigits = removeLeadingZeros(decimalDigits); | ||
|
||
return { | ||
magnitude: significantDigits.length - decimalDigits.length - 1, | ||
coefficient: addDecimalPointToNumber(significantDigits) | ||
}; | ||
|
||
} | ||
return { | ||
magnitude: trimmedFloat.indexOf(".") - 1, | ||
coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", "")) | ||
|
||
}; | ||
} | ||
|
||
|
||
/** | ||
* Converts a base ten number to proper scientific notation | ||
* @param {string} stringNumber the string representation of the base ten number to be converted | ||
* @returns {string} the number converted to scientific notation | ||
*/ | ||
function convertNumberToScientificNotation(stringNumber) { | ||
const splitNumber = stringNumber.replace("E", "e").split("e"); | ||
const originalCoefficient = splitNumber[0]; | ||
const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient) | ||
: normalizeInteger(originalCoefficient); | ||
const normalizedCoefficient = normalizedNumber.coefficient; | ||
const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude) | ||
: normalizedNumber.magnitude; | ||
|
||
return `${normalizedCoefficient}e${magnitude}`; | ||
|
||
} | ||
|
||
/** | ||
* Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type | ||
* @param {Node} node the node being evaluated | ||
* @returns {boolean} true if they do not match | ||
*/ | ||
function baseTenLosesPrecision(node) { | ||
const normalizedRawNumber = convertNumberToScientificNotation(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 normalizedRawNumber !== normalizedStoredNumber; | ||
} | ||
|
||
|
||
/** | ||
* Checks that the user-intended number equals the actual number after is has been converted to the Number type | ||
* @param {Node} node the node being evaluated | ||
* @returns {boolean} true if they do not match | ||
*/ | ||
function losesPrecision(node) { | ||
return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node); | ||
} | ||
|
||
|
||
return { | ||
Literal(node) { | ||
if (node.value && isNumber(node) && losesPrecision(node)) { | ||
context.report({ | ||
messageId: "noLossOfPrecision", | ||
node | ||
}); | ||
} | ||
} | ||
}; | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will treat the literal
0
as octal, which is incorrect although it doesn't affect the result of the precision check.