Skip to content

Commit 2724afa

Browse files
authored
no-zero-fractions: Handle numeric separators, fix missing parentheses, improve report location (#1238)
1 parent 2e82dc8 commit 2724afa

File tree

8 files changed

+2809
-126
lines changed

8 files changed

+2809
-126
lines changed

docs/rules/no-zero-fractions.md

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,58 @@ There is no difference in JavaScript between, for example, `1`, `1.0` and `1.`,
44

55
This rule is fixable.
66

7-
87
## Fail
98

109
```js
1110
const foo = 1.0;
11+
```
12+
13+
```js
1214
const foo = -1.0;
13-
const foo = 123123123.0;
15+
```
16+
17+
```js
18+
const foo = 123_456.000_000;
19+
```
20+
21+
```js
1422
const foo = 1.;
23+
```
24+
25+
```js
1526
const foo = 123.111000000;
16-
const foo = 123.00e20;
1727
```
1828

29+
```js
30+
const foo = 123.00e20;
31+
```
1932

2033
## Pass
2134

2235
```js
2336
const foo = 1;
37+
```
38+
39+
```js
2440
const foo = -1;
25-
const foo = 123123123;
41+
```
42+
43+
```js
44+
const foo = 123456;
45+
```
46+
47+
```js
2648
const foo = 1.1;
49+
```
50+
51+
```js
2752
const foo = -1.1;
28-
const foo = 123123123.4;
53+
```
54+
55+
```js
56+
const foo = 123.456;
57+
```
58+
59+
```js
2960
const foo = 1e3;
3061
```

rules/no-zero-fractions.js

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
'use strict';
2+
const {isParenthesized} = require('eslint-utils');
23
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
const needsSemicolon = require('./utils/needs-semicolon');
5+
const {isNumber, isDecimalInteger} = require('./utils/numeric');
36

47
const MESSAGE_ZERO_FRACTION = 'zero-fraction';
58
const MESSAGE_DANGLING_DOT = 'dangling-dot';
@@ -8,45 +11,54 @@ const messages = {
811
[MESSAGE_DANGLING_DOT]: 'Don\'t use a dangling dot in the number.'
912
};
1013

11-
// Groups:
12-
// 1. Integer part.
13-
// 2. Dangling dot or dot with zeroes.
14-
// 3. Dot with digits except last zeroes.
15-
// 4. Scientific notation.
16-
const RE_DANGLINGDOT_OR_ZERO_FRACTIONS = /^(?<integerPart>[+-]?\d*)(?:(?<dotAndZeroes>\.0*)|(?<dotAndDigits>\.\d*[1-9])0+)(?<scientificNotationSuffix>e[+-]?\d+)?$/;
17-
1814
const create = context => {
1915
return {
2016
Literal: node => {
21-
if (typeof node.value !== 'number') {
17+
if (!isNumber(node)) {
2218
return;
2319
}
2420

25-
const match = RE_DANGLINGDOT_OR_ZERO_FRACTIONS.exec(node.raw);
26-
if (match === null) {
21+
// Legacy octal number `0777` and prefixed number `0o1234` cannot have a dot.
22+
const {raw} = node;
23+
const match = raw.match(/^(?<before>[\d_]*)(?<dotAndFractions>\.[\d_]*)(?<after>.*)$/);
24+
if (!match) {
2725
return;
2826
}
2927

30-
const {
31-
integerPart,
32-
dotAndZeroes,
33-
dotAndDigits,
34-
scientificNotationSuffix
35-
} = match.groups;
28+
const {before, dotAndFractions, after} = match.groups;
29+
const formatted = before + dotAndFractions.replace(/[.0_]+$/g, '') + after;
3630

37-
const isDanglingDot = dotAndZeroes === '.';
31+
if (formatted === raw) {
32+
return;
33+
}
3834

35+
const isDanglingDot = dotAndFractions === '.';
36+
// End of fractions
37+
const end = node.range[0] + before.length + dotAndFractions.length;
38+
const start = end - (raw.length - formatted.length);
39+
const sourceCode = context.getSourceCode();
3940
context.report({
40-
node,
41+
loc: {
42+
start: sourceCode.getLocFromIndex(start),
43+
end: sourceCode.getLocFromIndex(end)
44+
},
4145
messageId: isDanglingDot ? MESSAGE_DANGLING_DOT : MESSAGE_ZERO_FRACTION,
4246
fix: fixer => {
43-
let wantedString = dotAndZeroes === undefined ? integerPart + dotAndDigits : integerPart;
47+
let fixed = formatted;
48+
if (
49+
node.parent.type === 'MemberExpression' &&
50+
node.parent.object === node &&
51+
isDecimalInteger(formatted) &&
52+
!isParenthesized(node, sourceCode)
53+
) {
54+
fixed = `(${fixed})`;
4455

45-
if (scientificNotationSuffix !== undefined) {
46-
wantedString += scientificNotationSuffix;
56+
if (needsSemicolon(sourceCode.getTokenBefore(node), sourceCode, fixed)) {
57+
fixed = `;${fixed}`;
58+
}
4759
}
4860

49-
return fixer.replaceText(node, wantedString);
61+
return fixer.replaceText(node, fixed);
5062
}
5163
});
5264
}

rules/numeric-separators-style.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function addSeparatorFromLeft(value, options) {
3535
}
3636

3737
function formatNumber(value, options) {
38-
const {integer, dot, fractional} = numeric.parseFloat(value);
38+
const {integer, dot, fractional} = numeric.parseFloatNumber(value);
3939
return addSeparator(integer, options) + dot + addSeparatorFromLeft(fractional, options);
4040
}
4141

@@ -99,7 +99,7 @@ const create = context => {
9999

100100
return {
101101
Literal: node => {
102-
if (!numeric.isNumberic(node) || numeric.isLegacyOctal(node)) {
102+
if (!numeric.isNumeric(node) || numeric.isLegacyOctal(node)) {
103103
return;
104104
}
105105

rules/utils/numeric.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
// Determine whether this node is a decimal integer literal.
44
// Copied from https://github.com/eslint/eslint/blob/cc4871369645c3409dc56ded7a555af8a9f63d51/lib/rules/utils/ast-utils.js#L1237
55
const DECIMAL_INTEGER_PATTERN = /^(?:0|0[0-7]*[89]\d*|[1-9](?:_?\d)*)$/u;
6-
const isDecimalInteger = node => isNumber(node) && DECIMAL_INTEGER_PATTERN.test(node.raw);
6+
const isDecimalInteger = text => DECIMAL_INTEGER_PATTERN.test(text);
7+
const isDecimalIntegerNode = node => isNumber(node) && isDecimalInteger(node.raw);
78

89
const isNumber = node => typeof node.value === 'number';
910
const isBigInt = node => Boolean(node.bigint);
10-
const isNumberic = node => isNumber(node) || isBigInt(node);
11+
const isNumeric = node => isNumber(node) || isBigInt(node);
1112
const isLegacyOctal = node => isNumber(node) && /^0\d+$/.test(node.raw);
1213

1314
function getPrefix(text) {
@@ -28,17 +29,27 @@ function parseNumber(text) {
2829
mark = '',
2930
sign = '',
3031
power = ''
31-
} = text.match(/^(?<number>.*?)(?:(?<mark>e)(?<sign>[+-])?(?<power>\d+))?$/i).groups;
32+
} = text.match(/^(?<number>[\d._]*?)(?:(?<mark>[Ee])(?<sign>[+-])?(?<power>[\d_]+))?$/).groups;
3233

3334
return {number, mark, sign, power};
3435
}
3536

36-
function parseFloat(text) {
37+
function parseFloatNumber(text) {
3738
const parts = text.split('.');
3839
const [integer, fractional = ''] = parts;
3940
const dot = parts.length === 2 ? '.' : '';
4041

4142
return {integer, dot, fractional};
4243
}
4344

44-
module.exports = {isNumber, isBigInt, isNumberic, isLegacyOctal, getPrefix, parseNumber, parseFloat, isDecimalInteger};
45+
module.exports = {
46+
isDecimalIntegerNode,
47+
isDecimalInteger,
48+
isNumber,
49+
isBigInt,
50+
isNumeric,
51+
isLegacyOctal,
52+
getPrefix,
53+
parseNumber,
54+
parseFloatNumber
55+
};

rules/utils/should-add-parentheses-to-member-expression-object.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict';
22

33
const isNewExpressionWithParentheses = require('./is-new-expression-with-parentheses');
4-
const {isDecimalInteger} = require('./numeric');
4+
const {isDecimalIntegerNode} = require('./numeric');
55

66
/**
77
Check if parentheses should to be added to a `node` when it's used as an `object` of `MemberExpression`.
@@ -24,7 +24,7 @@ function shouldAddParenthesesToMemberExpressionObject(node, sourceCode) {
2424
return !isNewExpressionWithParentheses(node, sourceCode);
2525
case 'Literal': {
2626
/* istanbul ignore next */
27-
if (isDecimalInteger(node)) {
27+
if (isDecimalIntegerNode(node)) {
2828
return true;
2929
}
3030

0 commit comments

Comments
 (0)