eslint-plugin-unicorn
Version:
More than 300 powerful ESLint rules
147 lines (126 loc) • 4.02 kB
JavaScript
import {isEmptyStringLiteral} from './ast/index.js';
import {
isBoolean,
isString,
isNumber,
isBigInt,
isGlobalIdentifier,
} from './utils/index.js';
import {replaceNodeWithExpression} from './fix/index.js';
/**
@import {TSESTree as ESTree} from '@typescript-eslint/types';
@import * as ESLint from 'eslint';
*/
const MESSAGE_ID = 'no-useless-coercion';
const messages = {
[MESSAGE_ID]: '{{description}} is unnecessary because the value is already a {{type}}.',
};
// `name` -> {type label, predicate checking the argument is already that type}
const coercions = new Map([
['Boolean', {type: 'boolean', isType: isBoolean}],
['String', {type: 'string', isType: isString}],
['BigInt', {type: 'bigint', isType: isBigInt}],
['Number', {type: 'number', isType: (node, context) => isNumber(node, context.sourceCode.getScope(node))}],
]);
/** @param {ESLint.Rule.RuleContext} context */
const create = context => {
const {sourceCode} = context;
// Build a problem that removes the coercion `node` in favor of its inner `expression`.
const getProblem = (node, expression, description, type) => ({
node,
messageId: MESSAGE_ID,
data: {description, type},
/** @param {ESLint.Rule.RuleFixer} fixer */
* fix(fixer, {abort}) {
// Removing the coercion would drop comments inside it.
if (sourceCode.getCommentsInside(node).length > 0) {
return abort();
}
yield replaceNodeWithExpression(fixer, node, expression, context);
},
});
// `Boolean(value)` / `String(value)` / `Number(value)` / `BigInt(value)`
context.on('CallExpression', (/** @type {ESTree.CallExpression} */ callExpression) => {
if (
callExpression.optional
|| callExpression.callee.type !== 'Identifier'
|| callExpression.arguments.length !== 1
|| callExpression.arguments[0].type === 'SpreadElement'
) {
return;
}
const {name} = callExpression.callee;
const coercion = coercions.get(name);
if (!coercion) {
return;
}
if (!isGlobalIdentifier(callExpression.callee, context)) {
return;
}
const [argument] = callExpression.arguments;
if (!coercion.isType(argument, context)) {
return;
}
return getProblem(callExpression, argument, `\`${name}()\``, coercion.type);
});
// `+value` on a value already a number. `~~` and `-` change the value, so they are excluded.
context.on('UnaryExpression', (/** @type {ESTree.UnaryExpression} */ node) => {
if (
node.operator !== '+'
|| !isNumber(node.argument, sourceCode.getScope(node.argument))
) {
return;
}
return getProblem(node, node.argument, '`+`', 'number');
});
// `value + ''` / `'' + value` on a value already a string. We intentionally do not special-case rare directive prologue creation.
context.on('BinaryExpression', (/** @type {ESTree.BinaryExpression} */ node) => {
if (node.operator !== '+') {
return;
}
const {left, right} = node;
let value;
if (isEmptyStringLiteral(right) && isString(left, context)) {
value = left;
} else if (isEmptyStringLiteral(left) && isString(right, context)) {
value = right;
} else {
return;
}
return getProblem(node, value, 'Concatenation with `\'\'`', 'string');
});
// `value.toString()` on a value already a string.
context.on('CallExpression', (/** @type {ESTree.CallExpression} */ node) => {
const {callee} = node;
if (
node.optional
|| node.arguments.length > 0
|| callee.type !== 'MemberExpression'
|| callee.optional
|| callee.computed
|| callee.property.type !== 'Identifier'
|| callee.property.name !== 'toString'
|| !isString(callee.object, context)
) {
return;
}
return getProblem(node, callee.object, '`.toString()`', 'string');
});
};
/** @type {ESLint.Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow useless type coercions of values that are already of the target type.',
recommended: 'unopinionated',
},
fixable: 'code',
messages,
languages: [
'js/js',
],
},
};
export default config;