2 * @fileoverview A rule to disallow the type conversions with shorter notations.
3 * @author Toru Nagashima
8 const astUtils = require("./utils/ast-utils");
10 //------------------------------------------------------------------------------
12 //------------------------------------------------------------------------------
14 const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
15 const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"];
18 * Parses and normalizes an option object.
19 * @param {Object} options An option object to parse.
20 * @returns {Object} The parsed and normalized option object.
22 function parseOptions(options) {
24 boolean: "boolean" in options ? options.boolean : true,
25 number: "number" in options ? options.number : true,
26 string: "string" in options ? options.string : true,
27 allow: options.allow || []
32 * Checks whether or not a node is a double logical nigating.
33 * @param {ASTNode} node An UnaryExpression node to check.
34 * @returns {boolean} Whether or not the node is a double logical nigating.
36 function isDoubleLogicalNegating(node) {
38 node.operator === "!" &&
39 node.argument.type === "UnaryExpression" &&
40 node.argument.operator === "!"
45 * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
46 * @param {ASTNode} node An UnaryExpression node to check.
47 * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
49 function isBinaryNegatingOfIndexOf(node) {
51 node.operator === "~" &&
52 node.argument.type === "CallExpression" &&
53 node.argument.callee.type === "MemberExpression" &&
54 node.argument.callee.property.type === "Identifier" &&
55 INDEX_OF_PATTERN.test(node.argument.callee.property.name)
60 * Checks whether or not a node is a multiplying by one.
61 * @param {BinaryExpression} node A BinaryExpression node to check.
62 * @returns {boolean} Whether or not the node is a multiplying by one.
64 function isMultiplyByOne(node) {
65 return node.operator === "*" && (
66 node.left.type === "Literal" && node.left.value === 1 ||
67 node.right.type === "Literal" && node.right.value === 1
72 * Checks whether the result of a node is numeric or not
73 * @param {ASTNode} node The node to test
74 * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
76 function isNumeric(node) {
78 node.type === "Literal" && typeof node.value === "number" ||
79 node.type === "CallExpression" && (
80 node.callee.name === "Number" ||
81 node.callee.name === "parseInt" ||
82 node.callee.name === "parseFloat"
88 * Returns the first non-numeric operand in a BinaryExpression. Designed to be
89 * used from bottom to up since it walks up the BinaryExpression trees using
90 * node.parent to find the result.
91 * @param {BinaryExpression} node The BinaryExpression node to be walked up on
92 * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
94 function getNonNumericOperand(node) {
95 const left = node.left,
98 if (right.type !== "BinaryExpression" && !isNumeric(right)) {
102 if (left.type !== "BinaryExpression" && !isNumeric(left)) {
110 * Checks whether a node is an empty string literal or not.
111 * @param {ASTNode} node The node to check.
112 * @returns {boolean} Whether or not the passed in node is an
113 * empty string literal or not.
115 function isEmptyString(node) {
116 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
120 * Checks whether or not a node is a concatenating with an empty string.
121 * @param {ASTNode} node A BinaryExpression node to check.
122 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
124 function isConcatWithEmptyString(node) {
125 return node.operator === "+" && (
126 (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
127 (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
132 * Checks whether or not a node is appended with an empty string.
133 * @param {ASTNode} node An AssignmentExpression node to check.
134 * @returns {boolean} Whether or not the node is appended with an empty string.
136 function isAppendEmptyString(node) {
137 return node.operator === "+=" && isEmptyString(node.right);
141 * Returns the operand that is not an empty string from a flagged BinaryExpression.
142 * @param {ASTNode} node The flagged BinaryExpression node to check.
143 * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
145 function getNonEmptyOperand(node) {
146 return isEmptyString(node.left) ? node.right : node.left;
149 //------------------------------------------------------------------------------
151 //------------------------------------------------------------------------------
158 description: "disallow shorthand type conversions",
159 category: "Best Practices",
161 url: "https://eslint.org/docs/rules/no-implicit-coercion"
184 enum: ALLOWABLE_OPERATORS
189 additionalProperties: false
194 const options = parseOptions(context.options[0] || {});
195 const sourceCode = context.getSourceCode();
198 * Reports an error and autofixes the node
199 * @param {ASTNode} node An ast node to report the error on.
200 * @param {string} recommendation The recommended code for the issue
201 * @param {bool} shouldFix Whether this report should fix the node
204 function report(node, recommendation, shouldFix) {
207 message: "use `{{recommendation}}` instead.",
216 const tokenBefore = sourceCode.getTokenBefore(node);
220 tokenBefore.range[1] === node.range[0] &&
221 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
223 return fixer.replaceText(node, ` ${recommendation}`);
225 return fixer.replaceText(node, recommendation);
231 UnaryExpression(node) {
235 operatorAllowed = options.allow.indexOf("!!") >= 0;
236 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
237 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
239 report(node, recommendation, true);
243 operatorAllowed = options.allow.indexOf("~") >= 0;
244 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
245 const recommendation = `${sourceCode.getText(node.argument)} !== -1`;
247 report(node, recommendation, false);
251 operatorAllowed = options.allow.indexOf("+") >= 0;
252 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
253 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
255 report(node, recommendation, true);
259 // Use `:exit` to prevent double reporting
260 "BinaryExpression:exit"(node) {
264 operatorAllowed = options.allow.indexOf("*") >= 0;
265 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
267 if (nonNumericOperand) {
268 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
270 report(node, recommendation, true);
274 operatorAllowed = options.allow.indexOf("+") >= 0;
275 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
276 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
278 report(node, recommendation, true);
282 AssignmentExpression(node) {
285 const operatorAllowed = options.allow.indexOf("+") >= 0;
287 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
288 const code = sourceCode.getText(getNonEmptyOperand(node));
289 const recommendation = `${code} = String(${code})`;
291 report(node, recommendation, true);