Skip to content

Commit 4a30863

Browse files
authored
Add require-number-to-fixed-digits-argument rule (#1288)
1 parent be51226 commit 4a30863

11 files changed

+241
-5
lines changed

docs/rules/require-array-join-separator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Enforce using the separator argument with `Array#join()`
22

3-
It's better to make it clear what the separator is when calling [Array#join()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join), instead of relying on the default comma (`,`) separator.
3+
It's better to make it clear what the separator is when calling [Array#join()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join), instead of relying on the default comma (`','`) separator.
44

55
This rule is fixable.
66

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Enforce using the digits argument with `Number#toFixed()`
2+
3+
It's better to make it clear what the value of the `digits` argument is when calling [Number#toFixed()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed), instead of relying on the default value of `0`.
4+
5+
This rule is fixable.
6+
7+
## Fail
8+
9+
```js
10+
const string = number.toFixed();
11+
```
12+
13+
## Pass
14+
15+
```js
16+
const string = foo.toFixed(0);
17+
```
18+
19+
```js
20+
const string = foo.toFixed(2);
21+
```
22+
23+
```js
24+
const integer = Math.floor(foo);
25+
```
26+
27+
```js
28+
const integer = Math.ceil(foo);
29+
```
30+
31+
```js
32+
const integer = Math.round(foo);
33+
```
34+
35+
```js
36+
const integer = Math.trunc(foo);
37+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ module.exports = {
118118
'unicorn/prefer-type-error': 'error',
119119
'unicorn/prevent-abbreviations': 'error',
120120
'unicorn/require-array-join-separator': 'error',
121+
'unicorn/require-number-to-fixed-digits-argument': 'error',
121122
'unicorn/string-content': 'off',
122123
'unicorn/throw-new-error': 'error'
123124
}

readme.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ Configure it in `package.json`.
113113
"unicorn/prefer-type-error": "error",
114114
"unicorn/prevent-abbreviations": "error",
115115
"unicorn/require-array-join-separator": "error",
116+
"unicorn/require-number-to-fixed-digits-argument": "error",
116117
"unicorn/string-content": "off",
117118
"unicorn/throw-new-error": "error"
118119
}
@@ -131,7 +132,7 @@ Each rule has emojis denoting:
131132
<!-- Do not manually modify this table. Run: `npm run generate-rules-table` -->
132133
<!-- RULES_TABLE_START -->
133134

134-
| Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Description || 🔧 |
135+
| Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Description || 🔧 |
135136
| :-- | :-- | :-- | :-- |
136137
| [better-regex](docs/rules/better-regex.md) | Improve regexes by making them shorter, consistent, and safer. || 🔧 |
137138
| [catch-error-name](docs/rules/catch-error-name.md) | Enforce a specific parameter name in catch clauses. || 🔧 |
@@ -210,6 +211,7 @@ Each rule has emojis denoting:
210211
| [prefer-type-error](docs/rules/prefer-type-error.md) | Enforce throwing `TypeError` in type checking conditions. || 🔧 |
211212
| [prevent-abbreviations](docs/rules/prevent-abbreviations.md) | Prevent abbreviations. || 🔧 |
212213
| [require-array-join-separator](docs/rules/require-array-join-separator.md) | Enforce using the separator argument with `Array#join()`. || 🔧 |
214+
| [require-number-to-fixed-digits-argument](docs/rules/require-number-to-fixed-digits-argument.md) | Enforce using the digits argument with `Number#toFixed()`. || 🔧 |
213215
| [string-content](docs/rules/string-content.md) | Enforce better string content. | | 🔧 |
214216
| [throw-new-error](docs/rules/throw-new-error.md) | Require `new` when throwing an error. || 🔧 |
215217

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
const getDocumentationUrl = require('./utils/get-documentation-url');
3+
const methodSelector = require('./utils/method-selector');
4+
5+
const MESSAGE_ID = 'require-number-to-fixed-digits-argument';
6+
const messages = {
7+
[MESSAGE_ID]: 'Missing the digits argument.'
8+
};
9+
10+
const mathToFixed = methodSelector({
11+
name: 'toFixed',
12+
length: 0
13+
});
14+
15+
/** @param {import('eslint').Rule.RuleContext} context */
16+
const create = context => {
17+
return {
18+
[mathToFixed](node) {
19+
const [
20+
openingParenthesis,
21+
closingParenthesis
22+
] = context.getSourceCode().getLastTokens(node, 2);
23+
24+
context.report({
25+
loc: {
26+
start: openingParenthesis.loc.start,
27+
end: closingParenthesis.loc.end
28+
},
29+
messageId: MESSAGE_ID,
30+
/** @param {import('eslint').Rule.RuleFixer} fixer */
31+
fix: fixer => fixer.insertTextBefore(closingParenthesis, '0')
32+
});
33+
}
34+
};
35+
};
36+
37+
module.exports = {
38+
create,
39+
meta: {
40+
type: 'suggestion',
41+
docs: {
42+
description: 'Enforce using the digits argument with `Number#toFixed()`.',
43+
url: getDocumentationUrl(__filename)
44+
},
45+
fixable: 'code',
46+
schema: [],
47+
messages
48+
}
49+
};

scripts/create-rule.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ function checkFiles(ruleId) {
1313
const files = [
1414
`docs/rules/${ruleId}.md`,
1515
`rules/${ruleId}.js`,
16-
`test/${ruleId}.mjs`
16+
`test/${ruleId}.mjs`,
17+
`test/snapshots/${ruleId}.mjs.md`,
18+
`test/snapshots/${ruleId}.mjs.snap`
1719
];
1820

1921
for (const file of files) {

scripts/generate-rules-table.mjs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ const rulesTableContent = ruleNames
3434
return `| ${link} | ${description} | ${isRecommended ? EMOJI_RECOMMENDED : ''} | ${isFixable ? EMOJI_FIXABLE : ''} |`;
3535
})
3636
.join('\n');
37-
const spaces = '&nbsp;'.repeat(Math.max(...ruleNames.map(({length}) => length)) + 8);
3837

3938
writeFileSync(
4039
pathReadme,
@@ -43,7 +42,7 @@ writeFileSync(
4342
outdent`
4443
<!-- RULES_TABLE_START -->
4544
46-
| Name${spaces} | Description | ${EMOJI_RECOMMENDED} | ${EMOJI_FIXABLE} |
45+
| Name${'&nbsp;'.repeat(40)} | Description | ${EMOJI_RECOMMENDED} | ${EMOJI_FIXABLE} |
4746
| :-- | :-- | :-- | :-- |
4847
${rulesTableContent}
4948

scripts/rename-rule.mjs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
#!/usr/bin/env node
2+
import fs, {promises as fsAsync} from 'node:fs';
3+
import enquirer from 'enquirer';
4+
import unicorn from '../index.js';
5+
6+
const rules = Object.keys(unicorn.rules);
7+
const resolveFile = file => new URL(`../${file}`, import.meta.url);
8+
9+
function checkFiles(ruleId) {
10+
const files = [
11+
`docs/rules/${ruleId}.md`,
12+
`rules/${ruleId}.js`,
13+
`test/${ruleId}.mjs`,
14+
`test/snapshots/${ruleId}.mjs.md`,
15+
`test/snapshots/${ruleId}.mjs.snap`
16+
];
17+
18+
for (const file of files) {
19+
if (fs.existsSync(resolveFile(file))) {
20+
throw new Error(`“${file}” already exists.`);
21+
}
22+
}
23+
}
24+
25+
async function renameFile(source, target) {
26+
source = resolveFile(source);
27+
target = resolveFile(target);
28+
29+
if (fs.existsSync(source)) {
30+
await fsAsync.rename(source, target);
31+
}
32+
}
33+
34+
async function renameRule(from, to) {
35+
await renameFile(`docs/rules/${from}.md`, `docs/rules/${to}.md`);
36+
await renameFile(`rules/${from}.js`, `rules/${to}.js`);
37+
await renameFile(`test/${from}.mjs`, `test/${to}.mjs`);
38+
await renameFile(`test/snapshots/${from}.mjs.md`, `test/snapshots/${to}.mjs.md`);
39+
await renameFile(`test/snapshots/${from}.mjs.snap`, `test/snapshots/${to}.mjs.snap`);
40+
41+
for (const file of [
42+
'readme.md',
43+
'index.js',
44+
`docs/rules/${to}.md`,
45+
`rules/${to}.js`,
46+
`test/${to}.mjs`,
47+
`test/snapshots/${to}.mjs.md`
48+
].map(file => resolveFile(file))
49+
) {
50+
if (!fs.existsSync(file)) {
51+
continue;
52+
}
53+
54+
// eslint-disable-next-line no-await-in-loop
55+
let text = await fsAsync.readFile(file, 'utf8');
56+
text = text.replaceAll(from, to);
57+
// eslint-disable-next-line no-await-in-loop
58+
await fsAsync.writeFile(file, text);
59+
}
60+
}
61+
62+
(async () => {
63+
const originalRuleId = await new enquirer.AutoComplete({
64+
message: 'Select the rule you want rename:',
65+
limit: 10,
66+
choices: rules
67+
}).run();
68+
69+
const ruleId = await new enquirer.Input({
70+
message: 'New name:',
71+
initial: originalRuleId
72+
}).run();
73+
74+
if (!ruleId || originalRuleId === ruleId) {
75+
return;
76+
}
77+
78+
if (Reflect.has(rules, ruleId)) {
79+
console.log(`${ruleId} already exists.`);
80+
return;
81+
}
82+
83+
checkFiles(ruleId);
84+
renameRule(originalRuleId, ruleId);
85+
})().catch(error => {
86+
console.error(error);
87+
process.exit(1);
88+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {getTester} from './utils/test.mjs';
2+
3+
const {test} = getTester(import.meta);
4+
5+
test.snapshot({
6+
valid: [
7+
'number.toFixed(0)',
8+
'number.toFixed(...[])',
9+
'number.toFixed(2)',
10+
'number.toFixed(1,2,3)',
11+
'number[toFixed]()',
12+
'number["toFixed"]()',
13+
'number?.toFixed()',
14+
'number.toFixed?.()',
15+
'number.notToFixed();'
16+
],
17+
invalid: [
18+
'const string = number.toFixed();',
19+
'const string = number.toFixed( /* comment */ );'
20+
]
21+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Snapshot report for `test/require-number-to-fixed-digits-argument.mjs`
2+
3+
The actual snapshot is saved in `require-number-to-fixed-digits-argument.mjs.snap`.
4+
5+
Generated by [AVA](https://avajs.dev).
6+
7+
## Invalid #1
8+
1 | const string = number.toFixed();
9+
10+
> Output
11+
12+
`␊
13+
1 | const string = number.toFixed(0);␊
14+
`
15+
16+
> Error 1/1
17+
18+
`␊
19+
> 1 | const string = number.toFixed();␊
20+
| ^^ Missing the digits argument.␊
21+
`
22+
23+
## Invalid #2
24+
1 | const string = number.toFixed( /* comment */ );
25+
26+
> Output
27+
28+
`␊
29+
1 | const string = number.toFixed( /* comment */ 0);␊
30+
`
31+
32+
> Error 1/1
33+
34+
`␊
35+
> 1 | const string = number.toFixed( /* comment */ );␊
36+
| ^^^^^^^^^^^^^^^^^ Missing the digits argument.␊
37+
`

0 commit comments

Comments
 (0)