Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prefer-native-coercion-functions rule #1767

Merged
Merged
1 change: 1 addition & 0 deletions configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module.exports = {
'unicorn/prefer-math-trunc': 'error',
'unicorn/prefer-modern-dom-apis': 'error',
'unicorn/prefer-module': 'error',
'unicorn/prefer-native-coercion-functions': 'error',
'unicorn/prefer-negative-index': 'error',
'unicorn/prefer-node-protocol': 'error',
'unicorn/prefer-number-properties': 'error',
Expand Down
68 changes: 68 additions & 0 deletions docs/rules/prefer-native-coercion-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Prefer use `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly
fisker marked this conversation as resolved.
Show resolved Hide resolved

<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
<!-- RULE_NOTICE -->
✅ *This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*

🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
<!-- /RULE_NOTICE -->

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), should use the built-in one directly.
fisker marked this conversation as resolved.
Show resolved Hide resolved

## Fail

```js
const toBoolean = value => Boolean(value);
```

```js
function toNumber(value) {
return Number(value);
}

if (toNumber(foo) === 1) {}
```

```js
const hasTruthyValue = array.some(element => element);
```

## Pass

```js
const toBoolean = Boolean;
```

```js
if (Number(foo) === 1) {}
```

```js
const hasTruthyValue = array.some(Boolean);
```

```js
const toStringObject = value => new String(value);
```

```js
const toObject= value => Object(value);
```

## Note

We don't check implicit coercion like:

```js
const toString = value => '' + value;
```

```js
const toNumber = value => +value;
```

```js
const toBoolean = value => !!value;
```

It is recommended to enable the ESLint built-in rule [`no-implicit-coercion`](https://eslint.org/docs/rules/no-implicit-coercion) for better experience.
fisker marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Each rule has emojis denoting:
| [prefer-math-trunc](docs/rules/prefer-math-trunc.md) | Enforce the use of `Math.trunc` instead of bitwise operators. | ✅ | 🔧 | 💡 |
| [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()`. | ✅ | 🔧 | |
| [prefer-module](docs/rules/prefer-module.md) | Prefer JavaScript modules (ESM) over CommonJS. | ✅ | 🔧 | 💡 |
| [prefer-native-coercion-functions](docs/rules/prefer-native-coercion-functions.md) | Prefer use `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly. | ✅ | 🔧 | |
| [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()`. | ✅ | 🔧 | |
| [prefer-node-protocol](docs/rules/prefer-node-protocol.md) | Prefer using the `node:` protocol when importing Node.js builtin modules. | ✅ | 🔧 | |
| [prefer-number-properties](docs/rules/prefer-number-properties.md) | Prefer `Number` static properties over global ones. | ✅ | 🔧 | 💡 |
Expand Down
178 changes: 178 additions & 0 deletions rules/prefer-native-coercion-functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
'use strict';
const {getFunctionHeadLocation, getFunctionNameWithKind} = require('eslint-utils');
const {not} = require('./selectors/index.js');

const MESSAGE_ID = 'prefer-native-coercion-functions';
const messages = {
[MESSAGE_ID]: '{{functionNameWithKind}} is equivalent of `{{replacementFunction}}`, should use `{{replacementFunction}}` directly.',
fisker marked this conversation as resolved.
Show resolved Hide resolved
};

const nativeCoercionFunctionNames = new Set(['String', 'Number', 'BigInt', 'Boolean', 'Symbol']);
const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findIndex', 'some']);

const isNativeCoercionFunctionCall = (node, firstArgumentName) =>
node
&& node.type === 'CallExpression'
&& !node.optional
&& node.callee.type === 'Identifier'
&& nativeCoercionFunctionNames.has(node.callee.name)
&& node.arguments[0]
&& node.arguments[0].type === 'Identifier'
&& node.arguments[0].name === firstArgumentName;

const isIdentityFunction = node =>
(
// `v => v`
node.type === 'ArrowFunctionExpression'
&& node.body.type === 'Identifier'
&& node.body.name === node.params[0].name
)
|| (
// `(v) => {return v;}`
// `function (v) {return v;}`
node.body.type === 'BlockStatement'
&& node.body.body.length === 1
&& node.body.body[0].type === 'ReturnStatement'
&& node.body.body[0].argument
&& node.body.body[0].argument.type === 'Identifier'
&& node.body.body[0].argument.name === node.params[0].name
);

const isArrayIdentityCallback = node =>
isIdentityFunction(node)
&& node.parent.type === 'CallExpression'
&& !node.parent.optional
&& node.parent.arguments[0] === node
&& node.parent.callee.type === 'MemberExpression'
&& !node.parent.callee.computed
&& !node.parent.callee.optional
&& node.parent.callee.property.type === 'Identifier'
&& arrayMethodsWithBooleanCallback.has(node.parent.callee.property.name);

function getCallExpression(node) {
const firstParameterName = node.params[0].name;

// `(v) => String(v)`
if (
node.type === 'ArrowFunctionExpression'
&& isNativeCoercionFunctionCall(node.body, firstParameterName)
) {
return node.body;
}

// `(v) => {return String(v);}`
// `function (v) {return String(v);}`
if (
node.body.type === 'BlockStatement'
&& node.body.body.length === 1
&& node.body.body[0].type === 'ReturnStatement'
&& isNativeCoercionFunctionCall(node.body.body[0].argument, firstParameterName)
) {
return node.body.body[0].argument;
}
}

const functionsSelector = [
':function',
'[async!=true]',
'[generator!=true]',
'[params.length>0]',
'[params.0.type="Identifier"]',
not([
'MethodDefinition[kind="constructor"] > .value',
'MethodDefinition[kind="set"] > .value',
'Property[kind="set"] > .value',
]),
].join('');

function getArrayCallbackProblem(node) {
if (!isArrayIdentityCallback(node)) {
return;
}

return {
replacementFunction: 'Boolean',
fix: fixer => fixer.replaceText(node, 'Boolean'),
};
}

function getCoercionFunctionProblem(node) {
const callExpression = getCallExpression(node);

if (!callExpression) {
return;
}

const {name} = callExpression.callee;

const problem = {replacementFunction: name};

if (node.type === 'FunctionDeclaration' || callExpression.arguments.length !== 1) {
return problem;
}

/** @param {import('eslint').Rule.RuleFixer} fixer */
problem.fix = fixer => {
let text = name;

if (
node.parent.type === 'Property'
&& node.parent.method
&& node.parent.value === node
) {
text = `: ${text}`;
} else if (node.parent.type === 'MethodDefinition') {
text = ` = ${text};`;
}

return fixer.replaceText(node, text);
};

return problem;
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
[functionsSelector](node) {
let problem = getArrayCallbackProblem(node) || getCoercionFunctionProblem(node);

if (!problem) {
return;
}

const sourceCode = context.getSourceCode();
const {replacementFunction, fix} = problem;

problem = {
node,
loc: getFunctionHeadLocation(node, sourceCode),
messageId: MESSAGE_ID,
data: {
functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
replacementFunction,
},
};

// Do not fix, if there are comments or extra parameters
sindresorhus marked this conversation as resolved.
Show resolved Hide resolved
if (!fix || node.params.length !== 1 || sourceCode.getCommentsInside(node).length > 0) {
return problem;
}

problem.fix = fix;

return problem;
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer use `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly.',
},
fixable: 'code',
messages,
},
};