Skip to content

Commit 51166f4

Browse files
authored
Add prefer-native-coercion-functions rule (#1767)
1 parent 1d32db4 commit 51166f4

7 files changed

+1007
-0
lines changed

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ module.exports = {
8787
'unicorn/prefer-modern-dom-apis': 'error',
8888
'unicorn/prefer-modern-math-apis': 'error',
8989
'unicorn/prefer-module': 'error',
90+
'unicorn/prefer-native-coercion-functions': 'error',
9091
'unicorn/prefer-negative-index': 'error',
9192
'unicorn/prefer-node-protocol': 'error',
9293
'unicorn/prefer-number-properties': 'error',
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly
2+
3+
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
4+
<!-- RULE_NOTICE -->
5+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
6+
7+
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
8+
<!-- /RULE_NOTICE -->
9+
10+
If a function is equivalent to [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt), [`Boolean`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean), or [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol), you should use the built-in one directly. Wrapping the built-in in a function is moot.
11+
12+
## Fail
13+
14+
```js
15+
const toBoolean = value => Boolean(value);
16+
```
17+
18+
```js
19+
function toNumber(value) {
20+
return Number(value);
21+
}
22+
23+
if (toNumber(foo) === 1) {}
24+
```
25+
26+
```js
27+
const hasTruthyValue = array.some(element => element);
28+
```
29+
30+
## Pass
31+
32+
```js
33+
const toBoolean = Boolean;
34+
```
35+
36+
```js
37+
if (Number(foo) === 1) {}
38+
```
39+
40+
```js
41+
const hasTruthyValue = array.some(Boolean);
42+
```
43+
44+
```js
45+
const toStringObject = value => new String(value);
46+
```
47+
48+
```js
49+
const toObject= value => Object(value);
50+
```
51+
52+
## Note
53+
54+
We don't check implicit coercion like:
55+
56+
```js
57+
const toString = value => '' + value;
58+
```
59+
60+
```js
61+
const toNumber = value => +value;
62+
```
63+
64+
```js
65+
const toBoolean = value => !!value;
66+
```
67+
68+
It is recommended to enable the built-in ESLint rule [`no-implicit-coercion`](https://eslint.org/docs/rules/no-implicit-coercion) for a better experience.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Each rule has emojis denoting:
127127
| [prefer-modern-dom-apis](docs/rules/prefer-modern-dom-apis.md) | Prefer `.before()` over `.insertBefore()`, `.replaceWith()` over `.replaceChild()`, prefer one of `.before()`, `.after()`, `.append()` or `.prepend()` over `insertAdjacentText()` and `insertAdjacentElement()`. || 🔧 | |
128128
| [prefer-modern-math-apis](docs/rules/prefer-modern-math-apis.md) | Prefer modern `Math` APIs over legacy patterns. || 🔧 | |
129129
| [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. || 🔧 | 💡 |
130+
| [prefer-native-coercion-functions](docs/rules/prefer-native-coercion-functions.md) | Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly. || 🔧 | |
130131
| [prefer-negative-index](docs/rules/prefer-negative-index.md) | Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`. || 🔧 | |
131132
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. || 🔧 | |
132133
| [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. || 🔧 | 💡 |
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
'use strict';
2+
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils');
3+
const {not} = require('./selectors/index.js');
4+
5+
const MESSAGE_ID = 'prefer-native-coercion-functions';
6+
const messages = {
7+
[MESSAGE_ID]: '{{functionNameWithKind}} is equivalent to `{{replacementFunction}}`. Use `{{replacementFunction}}` directly.',
8+
};
9+
10+
const nativeCoercionFunctionNames = new Set(['String', 'Number', 'BigInt', 'Boolean', 'Symbol']);
11+
const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findIndex', 'some']);
12+
13+
const isNativeCoercionFunctionCall = (node, firstArgumentName) =>
14+
node
15+
&& node.type === 'CallExpression'
16+
&& !node.optional
17+
&& node.callee.type === 'Identifier'
18+
&& nativeCoercionFunctionNames.has(node.callee.name)
19+
&& node.arguments[0]
20+
&& node.arguments[0].type === 'Identifier'
21+
&& node.arguments[0].name === firstArgumentName;
22+
23+
const isIdentityFunction = node =>
24+
(
25+
// `v => v`
26+
node.type === 'ArrowFunctionExpression'
27+
&& node.body.type === 'Identifier'
28+
&& node.body.name === node.params[0].name
29+
)
30+
|| (
31+
// `(v) => {return v;}`
32+
// `function (v) {return v;}`
33+
node.body.type === 'BlockStatement'
34+
&& node.body.body.length === 1
35+
&& node.body.body[0].type === 'ReturnStatement'
36+
&& node.body.body[0].argument
37+
&& node.body.body[0].argument.type === 'Identifier'
38+
&& node.body.body[0].argument.name === node.params[0].name
39+
);
40+
41+
const isArrayIdentityCallback = node =>
42+
isIdentityFunction(node)
43+
&& node.parent.type === 'CallExpression'
44+
&& !node.parent.optional
45+
&& node.parent.arguments[0] === node
46+
&& node.parent.callee.type === 'MemberExpression'
47+
&& !node.parent.callee.computed
48+
&& !node.parent.callee.optional
49+
&& node.parent.callee.property.type === 'Identifier'
50+
&& arrayMethodsWithBooleanCallback.has(node.parent.callee.property.name);
51+
52+
function getCallExpression(node) {
53+
const firstParameterName = node.params[0].name;
54+
55+
// `(v) => String(v)`
56+
if (
57+
node.type === 'ArrowFunctionExpression'
58+
&& isNativeCoercionFunctionCall(node.body, firstParameterName)
59+
) {
60+
return node.body;
61+
}
62+
63+
// `(v) => {return String(v);}`
64+
// `function (v) {return String(v);}`
65+
if (
66+
node.body.type === 'BlockStatement'
67+
&& node.body.body.length === 1
68+
&& node.body.body[0].type === 'ReturnStatement'
69+
&& isNativeCoercionFunctionCall(node.body.body[0].argument, firstParameterName)
70+
) {
71+
return node.body.body[0].argument;
72+
}
73+
}
74+
75+
const functionsSelector = [
76+
':function',
77+
'[async!=true]',
78+
'[generator!=true]',
79+
'[params.length>0]',
80+
'[params.0.type="Identifier"]',
81+
not([
82+
'MethodDefinition[kind="constructor"] > .value',
83+
'MethodDefinition[kind="set"] > .value',
84+
'Property[kind="set"] > .value',
85+
]),
86+
].join('');
87+
88+
function getArrayCallbackProblem(node) {
89+
if (!isArrayIdentityCallback(node)) {
90+
return;
91+
}
92+
93+
return {
94+
replacementFunction: 'Boolean',
95+
fix: fixer => fixer.replaceText(node, 'Boolean'),
96+
};
97+
}
98+
99+
function getCoercionFunctionProblem(node) {
100+
const callExpression = getCallExpression(node);
101+
102+
if (!callExpression) {
103+
return;
104+
}
105+
106+
const {name} = callExpression.callee;
107+
108+
const problem = {replacementFunction: name};
109+
110+
if (node.type === 'FunctionDeclaration' || callExpression.arguments.length !== 1) {
111+
return problem;
112+
}
113+
114+
/** @param {import('eslint').Rule.RuleFixer} fixer */
115+
problem.fix = fixer => {
116+
let text = name;
117+
118+
if (
119+
node.parent.type === 'Property'
120+
&& node.parent.method
121+
&& node.parent.value === node
122+
) {
123+
text = `: ${text}`;
124+
} else if (node.parent.type === 'MethodDefinition') {
125+
text = ` = ${text};`;
126+
}
127+
128+
return fixer.replaceText(node, text);
129+
};
130+
131+
return problem;
132+
}
133+
134+
/** @param {import('eslint').Rule.RuleContext} context */
135+
const create = context => ({
136+
[functionsSelector](node) {
137+
let problem = getArrayCallbackProblem(node) || getCoercionFunctionProblem(node);
138+
139+
if (!problem) {
140+
return;
141+
}
142+
143+
const sourceCode = context.getSourceCode();
144+
const {replacementFunction, fix} = problem;
145+
146+
problem = {
147+
node,
148+
loc: getFunctionHeadLocation(node, sourceCode),
149+
messageId: MESSAGE_ID,
150+
data: {
151+
functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
152+
replacementFunction,
153+
},
154+
};
155+
156+
/*
157+
We do not fix if there are:
158+
- Comments: No proper place to put them.
159+
- Extra parameters: Removing them may break types.
160+
*/
161+
if (!fix || node.params.length !== 1 || sourceCode.getCommentsInside(node).length > 0) {
162+
return problem;
163+
}
164+
165+
problem.fix = fix;
166+
167+
return problem;
168+
},
169+
});
170+
171+
/** @type {import('eslint').Rule.RuleModule} */
172+
module.exports = {
173+
create,
174+
meta: {
175+
type: 'suggestion',
176+
docs: {
177+
description: 'Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly.',
178+
},
179+
fixable: 'code',
180+
messages,
181+
},
182+
};

0 commit comments

Comments
 (0)