8000 New: no-loss-of-precision (fixes #11279) (#12747) · eslint/eslint@c636d57 · GitHub
[go: up one dir, main page]

Skip to content

Commit c636d57

Browse files
New: no-loss-of-precision (fixes #11279) (#12747)
* Created rule and test files * Working rules and tests * Working for decimals and scientific notation * Check all digits match instead of just most precise * Created rule and test files * Working rules and tests * Working for decimals and scientific notation * Check all digits match instead of just most precise * Expanded rule to non-base ten numbers * Added docs and updated config files * Update docs/rules/no-loss-of-precision.md Co-Authored-By: Teddy Katz <teddy.katz@gmail.com> * Update lib/rules/no-loss-of-precision.js Co-Authored-By: Teddy Katz <teddy.katz@gmail.com> * Removed rule from recommended * Renamed functions; fixed function description * Update lib/rules/no-loss-of-precision.js Co-Authored-By: Teddy Katz <teddy.katz@gmail.com> * Removed always-true conditional * Removed rule from recommended * Fixing octal cases * Working with octals * Changed isNotBaseTen to isBaseTen * Simplify isBaseTen test * Added regression tests * Additional regression test Co-authored-by: Teddy Katz <teddy.katz@gmail.com>
1 parent 72a4e10 commit c636d57

File tree

5 files changed

+392
-1
lines changed

5 files changed

+392
-1
lines changed

docs/rules/no-loss-of-precision.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Disallow Number Literals That Lose Precision (no-loss-of-precision)
2+
3+
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.
4+
5+
## Rule Details
6+
7+
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.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```js
12+
/*eslint no-loss-of-precision: "error"*/
13+
14+
const x = 9007199254740993
15+
const x = 5123000000000000000000000000001
16+
const x = 1230000000000000000000000.0
17+
const x = .1230000000000000000000000
18+
const x = 0X20000000000001
19+
```
20+
21+
Examples of **correct** code for this rule:
22+
23+
```js
24+
/*eslint no-loss-of-precision: "error"*/
25+
26+
const x = 12345
27+
const x = 123.456
28+
const x = 123e34
29+
const x = 12300000000000000000000000
30+
const x = 0x1FFFFFFFFFFFFF
31+
const x = 9007199254740991
32+
```

lib/rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ module.exports = new LazyLoadingRuleMap(Object.entries({
148148
"no-lone-blocks": () => require("./no-lone-blocks"),
149149
"no-lonely-if": () => require("./no-lonely-if"),
150150
"no-loop-func": () => require("./no-loop-func"),
151+
"no-loss-of-precision": () => require("./no-loss-of-precision"),
151152
"no-magic-numbers": () => require("./no-magic-numbers"),
152153
"no-misleading-character-class": () => require("./no-misleading-character-class"),
153154
"no-mixed-operators": () => require("./no-mixed-operators"),

lib/rules/no-loss-of-precision.js

Lines changed: 198 additions & 0 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/**
2+
* @fileoverview Rule to flag numbers that will lose significant figure precision at runtime
3+
* @author Jacob Moore
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Rule Definition
10+
//------------------------------------------------------------------------------
11+
12+
module.exports = {
13+
meta: {
14+
type: "problem",
15+
16+
docs: {
17+
description: "disallow literal numbers that lose precision",
18+
category: "Possible Errors",
19+
recommended: false,
20+
url: "https://eslint.org/docs/rules/no-loss-of-precision"
21+
},
22+
schema: [],
23+
messages: {
24+
noLossOfPrecision: "This number literal will lose precision at runtime."
25+
}
26+
},
27+
28+
create(context) {
29+
30+
/**
31+
* Returns whether the node is number literal
32+
* @param {Node} node the node literal being evaluated
33+
* @returns {boolean} true if the node is a number literal
34+
*/
35+
function isNumber(node) {
36+
return typeof node.value === "number";
37+
}
38+
39+
40+
/**
41+
* Checks whether the number is base ten
42+
* @param {ASTNode} node the node being evaluated
43+
* @returns {boolean} true if the node is in base ten
44+
*/
45+
function isBaseTen(node) {
46+
const prefixes = ["0x", "0X", "0b", "0B", "0o", "0O"];
47+
48+
return prefixes.every(prefix => !node.raw.startsWith(prefix)) &&
49+
!/^0[0-7]+$/u.test(node.raw);
50+
}
51+
52+
/**
53+
* Checks that the user-intended non-base ten number equals the actual number after is has been converted to the Number type
54+
* @param {Node} node the node being evaluated
55+
* @returns {boolean} true if they do not match
56+
*/
57+
function notBaseTenLosesPrecision(node) {
58+
const rawString = node.raw.toUpperCase();
59+
let base = 0;
60+
61+
if (rawString.startsWith("0B")) {
62+
base = 2;
63+
} else if (rawString.startsWith("0X")) {
64+
base = 16;
65+
} else {
66+
base = 8;
67+
}
68+
69+
return !rawString.endsWith(node.value.toString(base).toUpperCase());
70+
}
71+
72+
/**
73+
* Adds a decimal point to the numeric string at index 1
74+
* @param {string} stringNumber the numeric string without any decimal point
75+
* @returns {string} the numeric string with a decimal point in the proper place
76+
*/
77+
function addDecimalPointToNumber(stringNumber) {
78+
return `${stringNumber.slice(0, 1)}.${stringNumber.slice(1)}`;
79+
}
80+
81+
/**
82+
* Returns the number stripped of leading zeros
83+
* @param {string} numberAsString the string representation of the number
84+
* @returns {string} the stripped string
85+
*/
86+
function removeLeadingZeros(numberAsString) {
87+
return numberAsString.replace(/^0*/u, "");
88+
}
89+
90+
/**
91+
* Returns the number stripped of trailing zeros
92+
* @param {string} numberAsString the string representation of the number
93+
* @returns {string} the stripped string
94+
*/
95+
function removeTrailingZeros(numberAsString) {
96+
return numberAsString.replace(/0*$/u, "");
97+
}
98+
99+
/**
100+
* Converts an integer to to an object containing the the integer's coefficient and order of magnitude
101+
* @param {string} stringInteger the string representation of the integer being converted
102+
* @returns {Object} the object containing the the integer's coefficient and order of magnitude
103+
*/
104+
function normalizeInteger(stringInteger) {
105+
const significantDigits = removeTrailingZeros(removeLeadingZeros(stringInteger));
106+
107+
return {
108+
magnitude: stringInteger.startsWith("0") ? stringInteger.length - 2 : stringInteger.length - 1,
109+
coefficient: addDecimalPointToNumber(significantDigits)
110+
};
111+
}
112+
113+
/**
114+
*
115+
* Converts a float to to an object containing the the floats's coefficient and order of magnitude
116+
* @param {string} stringFloat the string representation of the float being converted
117+
* @returns {Object} the object containing the the integer's coefficient and order of magnitude
118+
*/
119+
function normalizeFloat(stringFloat) {
120+
const trimmedFloat = removeLeadingZeros(stringFloat);
121+
122+
if (trimmedFloat.startsWith(".")) {
123+
const decimalDigits = trimmedFloat.split(".").pop();
124+
const significantDigits = removeLeadingZeros(decimalDigits);
125+
126+
return {
127+
magnitude: significantDigits.length - decimalDigits.length - 1,
128+
coefficient: addDecimalPointToNumber(significantDigits)
129+
};
130+
131+
}
132+
return {
133+
magnitude: trimmedFloat.indexOf(".") - 1,
134+
coefficient: addDecimalPointToNumber(trimmedFloat.replace(".", ""))
135+
136+
};
137+
}
138+
139+
140+
/**
141+
* Converts a base ten number to proper scientific notation
142+
* @param {string} stringNumber the string representation of the base ten number to be converted
143+
* @returns {string} the number converted to scientific notation
144+
*/
145+
function convertNumberToScientificNotation(stringNumber) {
146+
const splitNumber = stringNumber.replace("E", "e").split("e");
147+
const originalCoefficient = splitNumber[0];
148+
const normalizedNumber = stringNumber.includes(".") ? normalizeFloat(originalCoefficient)
149+
: normalizeInteger(originalCoefficient);
150+
const normalizedCoefficient = normalizedNumber.coefficient;
151+
const magnitude = splitNumber.length > 1 ? (parseInt(splitNumber[1], 10) + normalizedNumber.magnitude)
152+
: normalizedNumber.magnitude;
153+
154+
return `${normalizedCoefficient}e${magnitude}`;
155+
156+
}
157+
158+
/**
159+
* Checks that the user-intended base ten number equals the actual number after is has been converted to the Number type
160+
* @param {Node} node the node being evaluated
161+
* @returns {boolean} true if they do not match
162+
*/
163+
function baseTenLosesPrecision(node) {
164+
const normalizedRawNumber = convertNumberToScientificNotation(node.raw);
165+
const requestedPrecision = normalizedRawNumber.split("e")[0].replace(".", "").length;
166+
167+
if (requestedPrecision > 100) {
168+
return true;
169+
}
170+
const storedNumber = node.value.toPrecision(requestedPrecision);
171+
const normalizedStoredNumber = convertNumberToScientificNotation(storedNumber);
172+
173+
return normalizedRawNumber !== normalizedStoredNumber;
174+
}
175+
176+
177+
/**
178+
* Checks that the user-intended number equals the actual number after is has been converted to the Number type
179+
* @param {Node} node the node being evaluated
180+
* @returns {boolean} true if they do not match
181+
*/
182+
function losesPrecision(node) {
183+
return isBaseTen(node) ? baseTenLosesPrecision(node) : notBaseTenLosesPrecision(node);
184+
}
185+
186+
187+
return {
188+
Literal(node) {
189+
if (node.value && isNumber(node) && losesPrecision(node)) {
190+
context.report({
191+
messageId: "noLossOfPrecision",
192+
node
193+
});
194+
}
195+
}
196+
};
197+
}
198+
};
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
*@fileoverview Tests for no-loss-of-precision rule.
3+
*@author Jacob Moore
4+
*/
5+
6+
"use strict";
7+
8+
//------------------------------------------------------------------------------
9+
// Requirements
10+
//------------------------------------------------------------------------------
11+
12+
const rule = require("../../../lib/rules/no-loss-of-precision"),
13+
{ RuleTester } = require("../../../lib/rule-tester");
14+
15+
//------------------------------------------------------------------------------
16+
// Helpers
17+
//------------------------------------------------------------------------------
18+
19+
const ruleTester = new RuleTester();
20+
21+
ruleTester.run("no-loss-of-precision", rule, {
22+
valid: [
23+
"var x = 12345",
24+
"var x = 123.456",
25+
"var x = -123.456",
26+
"var x = -123456",
27+
"var x = 123e34",
28+
"var x = 123.0e34",
29+
"var x = 123e-34",
30+
"var x = -123e34",
31+
"var x = -123e-34",
32+
"var x = 12.3e34",
33+
"var x = 12.3e-34",
34+
"var x = -12.3e34",
35+
"var x = -12.3e-34",
36+
"var x = 12300000000000000000000000",
37+
"var x = -12300000000000000000000000",
38+
"var x = 0.00000000000000000000000123",
39+
"var x = -0.00000000000000000000000123",
40+
"var x = 9007199254740991",
41+
"var x = 0",
42+
"var x = 0.0",
43+
"var x = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000",
44+
"var x = -0",
45+
"var x = 123.0000000000000000000000",
46+
"var x = 019.5",
47+
"var x = 0195",
48+
"var x = 0e5",
49+
50+
51+
{ code: "var x = 0b11111111111111111111111111111111111111111111111111111", parserOptions: { ecmaVersion: 6 } },
52+
{ code: "var x = 0B11111111111111111111111111111111111111111111111111111", parserOptions: { ecmaVersion: 6 } },
53+
54+
{ code: "var x = 0o377777777777777777", parserOptions: { ecmaVersion: 6 } },
55+
{ code: "var x = 0O377777777777777777", parserOptions: { ecmaVersion: 6 } },
56+
"var x = 0377777777777777777",
57+
58+
"var x = 0x1FFFFFFFFFFFFF",
59+
"var x = 0X1FFFFFFFFFFFFF",
60+
"var x = true",
61+
"var x = 'abc'",
62+
"var x = ''",
63+
"var x = null",
64+
"var x = undefined",
65+
"var x = {}",
66+
"var x = ['a', 'b']",
67+
"var x = new Date()",
68+
"var x = '9007199254740993'"
69+
70+
],
71+
invalid: [
72+
{
73+
code: "var x = 9007199254740993",
74+
errors: [{ messageId: "noLossOfPrecision" }]
75+
},
76+
{
77+
code: "var x = 9007199254740.993e3",
78+
errors: [{ messageId: "noLossOfPrecision" }]
79+
},
80+
{
81+
code: "var x = 9.007199254740993e15",
82+
errors: [{ messageId: "noLossOfPrecision" }]
83+
},
84+
{
85+
code: "var x = -9007199254740993",
86+
errors: [{ messageId: "noLossOfPrecision" }]
87+
},
88+
{
89+
code: "var x = 900719.9254740994",
90+
errors: [{ messageId: "noLossOfPrecision" }]
91+
},
92+
{
93+
code: "var x = -900719.9254740994",
94+
errors: [{ messageId: "noLossOfPrecision" }]
95+
},
96+
97+
{
98+
code: "var x = 5123000000000000000000000000001",
99+
errors: [{ messageId: "noLossOfPrecision" }]
100+
},
101+
{
102+
code: "var x = -5123000000000000000000000000001",
103+
errors: [{ messageId: "noLossOfPrecision" }]
104+
},
105+
{
106+
code: "var x = 1230000000000000000000000.0",
107+
errors: [{ messageId: "noLossOfPrecision" }]
108+
},
109+
{
110+
code: "var x = 1.0000000000000000000000123",
111+
errors: [{ messageId: "noLossOfPrecision" }]
112+
},
113+
{
114+
code: "var x = 17498005798264095394980017816940970922825355447145699491406164851279623993595007385788105416184430592",
115+
errors: [{ messageId: "noLossOfPrecision" }]
116+
},< 741A /div>
117+
{
118+
code: "var x = 2e999",
119+
errors: [{ messageId: "noLossOfPrecision" }]
120+
},
121+
{
122+
code: "var x = .1230000000000000000000000",
123+
errors: [{ messageId: "noLossOfPrecision" }]
124+
},
125+
{
126+
code: "var x = 0b100000000000000000000000000000000000000000000000000001",
127+
parserOptions: { ecmaVersion: 6 },
128+
errors: [{ messageId: "noLossOfPrecision" }]
129+
},
130+
{
131+
code: "var x = 0B100000000000000000000000000000000000000000000000000001",
132+
parserOptions: { ecmaVersion: 6 },
133+
errors: [{ messageId: "noLossOfPrecision" }]
134+
},
135+
{
136+
code: "var x = 0o400000000000000001",
137+
parserOptions: { ecmaVersion: 6 },
138+
errors: [{ messageId: "noLossOfPrecision" }]
139+
},
140+
{
141+
code: "var x = 0O400000000000000001",
142+
parserOptions: { ecmaVersion: 6 },
143+
errors: [{ messageId: "noLossOfPrecision" }]
144+
},
145+
{
146+
code: "var x = 0400000000000000001",
147+
errors: [{ messageId: "noLossOfPrecision" }]
148+
},
149+
{
150+
code: "var x = 0x20000000000001",
151+
errors: [{ messageId: "noLossOfPrecision" }]
152+
},
153+
{
154+
code: "var x = 0X20000000000001",
155+
errors: [{ messageId: "noLossOfPrecision" }]
156+
}
157+
158+
]
159+
});

0 commit comments

Comments
 (0)
0