-
-
Notifications
You must be signed in to change notification settings - Fork 367
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
prefer-native-coercion-functions
rule (#1767)
- Loading branch information
Showing
7 changed files
with
1,007 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
# Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly | ||
|
||
<!-- 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), you should use the built-in one directly. Wrapping the built-in in a function is moot. | ||
|
||
## 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 built-in ESLint rule [`no-implicit-coercion`](https://eslint.org/docs/rules/no-implicit-coercion) for a better experience. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
'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 to `{{replacementFunction}}`. Use `{{replacementFunction}}` directly.', | ||
}; | ||
|
||
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, | ||
}, | ||
}; | ||
|
||
/* | ||
We do not fix if there are: | ||
- Comments: No proper place to put them. | ||
- Extra parameters: Removing them may break types. | ||
*/ | ||
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 using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly.', | ||
}, | ||
fixable: 'code', | ||
messages, | ||
}, | ||
}; |
Oops, something went wrong.