Skip to content

Commit 1f4cb8a

Browse files
authored
Add prefer-bigint-literals rule (#2722)
1 parent d96dfb7 commit 1f4cb8a

File tree

7 files changed

+642
-0
lines changed

7 files changed

+642
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Prefer `BigInt` literals over the constructor
2+
3+
💼 This rule is enabled in the ✅ `recommended` [config](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config).
4+
5+
🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
Use `1n` instead of `BigInt(1)`.
11+
12+
## Examples
13+
14+
```js
15+
//
16+
const bigint = BigInt(1);
17+
18+
//
19+
const bigint = 1n;
20+
```
21+
22+
```js
23+
//
24+
const bigint = BigInt('1');
25+
26+
//
27+
const bigint = 1n;
28+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export default [
134134
| [prefer-array-index-of](docs/rules/prefer-array-index-of.md) | Prefer `Array#{indexOf,lastIndexOf}()` over `Array#{findIndex,findLastIndex}()` when looking for the index of an item. || 🔧 | 💡 |
135135
| [prefer-array-some](docs/rules/prefer-array-some.md) | Prefer `.some(…)` over `.filter(…).length` check and `.{find,findLast,findIndex,findLastIndex}(…)`. || 🔧 | 💡 |
136136
| [prefer-at](docs/rules/prefer-at.md) | Prefer `.at()` method for index access and `String#charAt()`. || 🔧 | 💡 |
137+
| [prefer-bigint-literals](docs/rules/prefer-bigint-literals.md) | Prefer `BigInt` literals over the constructor. || 🔧 | 💡 |
137138
| [prefer-blob-reading-methods](docs/rules/prefer-blob-reading-methods.md) | Prefer `Blob#arrayBuffer()` over `FileReader#readAsArrayBuffer(…)` and `Blob#text()` over `FileReader#readAsText(…)`. || | |
138139
| [prefer-class-fields](docs/rules/prefer-class-fields.md) | Prefer class field declarations over `this` assignments in constructors. || 🔧 | 💡 |
139140
| [prefer-code-point](docs/rules/prefer-code-point.md) | Prefer `String#codePointAt(…)` over `String#charCodeAt(…)` and `String.fromCodePoint(…)` over `String.fromCharCode(…)`. || | 💡 |

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export {default as 'prefer-array-flat'} from './prefer-array-flat.js';
7878
export {default as 'prefer-array-index-of'} from './prefer-array-index-of.js';
7979
export {default as 'prefer-array-some'} from './prefer-array-some.js';
8080
export {default as 'prefer-at'} from './prefer-at.js';
81+
export {default as 'prefer-bigint-literals'} from './prefer-bigint-literals.js';
8182
export {default as 'prefer-blob-reading-methods'} from './prefer-blob-reading-methods.js';
8283
export {default as 'prefer-class-fields'} from './prefer-class-fields.js';
8384
export {default as 'prefer-code-point'} from './prefer-code-point.js';

rules/prefer-bigint-literals.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import {
2+
isCallExpression,
3+
isStringLiteral,
4+
} from './ast/index.js';
5+
6+
const MESSAGE_ID_ERROR = 'prefer-bigint-literals/error';
7+
const MESSAGE_ID_SUGGESTION = 'prefer-bigint-literals/suggestion';
8+
const messages = {
9+
[MESSAGE_ID_ERROR]: 'Prefer using bigint literal over `BigInt(…)`.',
10+
[MESSAGE_ID_SUGGESTION]: 'Replace with {{replacement}}.',
11+
};
12+
13+
const canUseNumericLiteralRaw = numericLiteral => {
14+
const raw = numericLiteral.raw.replaceAll('_', '').toLowerCase();
15+
16+
if (raw.includes('.')) {
17+
return false;
18+
}
19+
20+
const {value} = numericLiteral;
21+
22+
for (const {prefix, base} of [
23+
{prefix: '0b', base: 2},
24+
{prefix: '0o', base: 8},
25+
{prefix: '0x', base: 16},
26+
]) {
27+
if (raw.startsWith(prefix)) {
28+
return raw.slice(2) === value.toString(base);
29+
}
30+
}
31+
32+
if (raw.includes('e')) {
33+
return false;
34+
}
35+
36+
return raw === String(value);
37+
};
38+
39+
function getReplacement(valueNode) {
40+
if (isStringLiteral(valueNode)) {
41+
const raw = valueNode.raw.slice(1, -1);
42+
try {
43+
BigInt(raw);
44+
} catch {
45+
return;
46+
}
47+
48+
return {shouldUseSuggestion: false, text: `${raw.trimEnd()}n`};
49+
}
50+
51+
const {value, raw} = valueNode;
52+
53+
if (!Number.isInteger(value)) {
54+
return;
55+
}
56+
57+
let bigint;
58+
try {
59+
bigint = BigInt(value);
60+
} catch {
61+
return;
62+
}
63+
64+
const shouldUseSuggestion = !canUseNumericLiteralRaw(valueNode);
65+
const text = shouldUseSuggestion ? `${bigint}n` : `${raw}n`;
66+
return {shouldUseSuggestion, text};
67+
}
68+
69+
/** @param {import('eslint').Rule.RuleContext} context */
70+
const create = context => ({
71+
CallExpression(callExpression) {
72+
if (!isCallExpression(callExpression, {
73+
name: 'BigInt',
74+
argumentsLength: 1,
75+
optional: false,
76+
})) {
77+
return;
78+
}
79+
80+
const [valueNode] = callExpression.arguments;
81+
const replacement = getReplacement(valueNode);
82+
if (!replacement) {
83+
return;
84+
}
85+
86+
const problem = {
87+
node: callExpression,
88+
messageId: MESSAGE_ID_ERROR,
89+
};
90+
91+
const {shouldUseSuggestion, text} = replacement;
92+
93+
/** @param {import('eslint').Rule.RuleFixer} fixer */
94+
const fix = fixer => fixer.replaceText(callExpression, text);
95+
96+
if (shouldUseSuggestion || context.sourceCode.getCommentsInside(callExpression).length > 0) {
97+
problem.suggest = [
98+
{
99+
messageId: MESSAGE_ID_SUGGESTION,
100+
data: {
101+
replacement: text.length < 20 ? `\`${text}\`` : 'a bigint literal',
102+
},
103+
fix,
104+
},
105+
];
106+
} else {
107+
problem.fix = fix;
108+
}
109+
110+
return problem;
111+
},
112+
});
113+
114+
/** @type {import('eslint').Rule.RuleModule} */
115+
const config = {
116+
create,
117+
meta: {
118+
type: 'suggestion',
119+
docs: {
120+
description: 'Prefer `BigInt` literals over the constructor.',
121+
recommended: true,
122+
},
123+
fixable: 'code',
124+
hasSuggestions: true,
125+
messages,
126+
},
127+
};
128+
129+
export default config;

test/prefer-bigint-literals.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {getTester} from './utils/test.js';
2+
3+
const {test} = getTester(import.meta);
4+
5+
test.snapshot({
6+
valid: [
7+
'1n',
8+
'BigInt()',
9+
'BigInt(1, 1)',
10+
'BigInt(...[1])',
11+
'BigInt(true)',
12+
'BigInt(null)',
13+
'new BigInt(1)',
14+
'Not_BigInt(1)',
15+
'BigInt("1.0")',
16+
'BigInt("1.1")',
17+
'BigInt("1e3")',
18+
'BigInt(`1`)',
19+
'BigInt("1" + "2")',
20+
'BigInt?.(1)',
21+
'BigInt(1.1)',
22+
'typeof BigInt',
23+
'BigInt(1n)',
24+
'BigInt("not-number")',
25+
'BigInt("1_2")',
26+
'BigInt("1\\\n2")',
27+
'BigInt("1\\\n2")',
28+
String.raw`BigInt("\u{31}")`,
29+
],
30+
invalid: [
31+
'BigInt("0")',
32+
'BigInt(" 0 ")',
33+
'BigInt("9007199254740993")',
34+
'BigInt("0B11")',
35+
'BigInt("0O777")',
36+
'BigInt("0XFe")',
37+
`BigInt("${'9'.repeat(100)}")`,
38+
'BigInt(0)',
39+
'BigInt(0B11_11)',
40+
'BigInt(0O777_777)',
41+
'BigInt(0XFe_fE)',
42+
// Legacy octal literals
43+
...[
44+
'BigInt(0777)',
45+
'BigInt(0888)',
46+
].map(code => ({code, languageOptions: {sourceType: 'script'}})),
47+
// Not fixable
48+
'BigInt(9007199254740993)',
49+
'BigInt(0x20000000000001)',
50+
'BigInt(9_007_199_254_740_993)',
51+
'BigInt(0x20_00_00_00_00_00_01)',
52+
'BigInt(1.0)',
53+
'BigInt(1e2)',
54+
'BigInt(/* comment */1)',
55+
`BigInt(${'9'.repeat(100)})`,
56+
],
57+
});

0 commit comments

Comments
 (0)