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 disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
28 allow: options.allow || []
33 * Checks whether or not a node is a double logical nigating.
34 * @param {ASTNode} node An UnaryExpression node to check.
35 * @returns {boolean} Whether or not the node is a double logical nigating.
37 function isDoubleLogicalNegating(node) {
39 node.operator === "!" &&
40 node.argument.type === "UnaryExpression" &&
41 node.argument.operator === "!"
46 * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
47 * @param {ASTNode} node An UnaryExpression node to check.
48 * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
50 function isBinaryNegatingOfIndexOf(node) {
51 if (node.operator !== "~") {
54 const callNode = astUtils.skipChainExpression(node.argument);
57 callNode.type === "CallExpression" &&
58 astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
63 * Checks whether or not a node is a multiplying by one.
64 * @param {BinaryExpression} node A BinaryExpression node to check.
65 * @returns {boolean} Whether or not the node is a multiplying by one.
67 function isMultiplyByOne(node) {
68 return node.operator === "*" && (
69 node.left.type === "Literal" && node.left.value === 1 ||
70 node.right.type === "Literal" && node.right.value === 1
75 * Checks whether the result of a node is numeric or not
76 * @param {ASTNode} node The node to test
77 * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
79 function isNumeric(node) {
81 node.type === "Literal" && typeof node.value === "number" ||
82 node.type === "CallExpression" && (
83 node.callee.name === "Number" ||
84 node.callee.name === "parseInt" ||
85 node.callee.name === "parseFloat"
91 * Returns the first non-numeric operand in a BinaryExpression. Designed to be
92 * used from bottom to up since it walks up the BinaryExpression trees using
93 * node.parent to find the result.
94 * @param {BinaryExpression} node The BinaryExpression node to be walked up on
95 * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
97 function getNonNumericOperand(node) {
98 const left = node.left,
101 if (right.type !== "BinaryExpression" && !isNumeric(right)) {
105 if (left.type !== "BinaryExpression" && !isNumeric(left)) {
113 * Checks whether an expression evaluates to a string.
114 * @param {ASTNode} node node that represents the expression to check.
115 * @returns {boolean} Whether or not the expression evaluates to a string.
117 function isStringType(node) {
118 return astUtils.isStringLiteral(node) ||
120 node.type === "CallExpression" &&
121 node.callee.type === "Identifier" &&
122 node.callee.name === "String"
127 * Checks whether a node is an empty string literal or not.
128 * @param {ASTNode} node The node to check.
129 * @returns {boolean} Whether or not the passed in node is an
130 * empty string literal or not.
132 function isEmptyString(node) {
133 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
137 * Checks whether or not a node is a concatenating with an empty string.
138 * @param {ASTNode} node A BinaryExpression node to check.
139 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
141 function isConcatWithEmptyString(node) {
142 return node.operator === "+" && (
143 (isEmptyString(node.left) && !isStringType(node.right)) ||
144 (isEmptyString(node.right) && !isStringType(node.left))
149 * Checks whether or not a node is appended with an empty string.
150 * @param {ASTNode} node An AssignmentExpression node to check.
151 * @returns {boolean} Whether or not the node is appended with an empty string.
153 function isAppendEmptyString(node) {
154 return node.operator === "+=" && isEmptyString(node.right);
158 * Returns the operand that is not an empty string from a flagged BinaryExpression.
159 * @param {ASTNode} node The flagged BinaryExpression node to check.
160 * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
162 function getNonEmptyOperand(node) {
163 return isEmptyString(node.left) ? node.right : node.left;
166 //------------------------------------------------------------------------------
168 //------------------------------------------------------------------------------
175 description: "disallow shorthand type conversions",
176 category: "Best Practices",
178 url: "https://eslint.org/docs/rules/no-implicit-coercion"
198 disallowTemplateShorthand: {
205 enum: ALLOWABLE_OPERATORS
210 additionalProperties: false
214 useRecommendation: "use `{{recommendation}}` instead."
219 const options = parseOptions(context.options[0] || {});
220 const sourceCode = context.getSourceCode();
223 * Reports an error and autofixes the node
224 * @param {ASTNode} node An ast node to report the error on.
225 * @param {string} recommendation The recommended code for the issue
226 * @param {bool} shouldFix Whether this report should fix the node
229 function report(node, recommendation, shouldFix) {
232 messageId: "useRecommendation",
241 const tokenBefore = sourceCode.getTokenBefore(node);
245 tokenBefore.range[1] === node.range[0] &&
246 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
248 return fixer.replaceText(node, ` ${recommendation}`);
250 return fixer.replaceText(node, recommendation);
256 UnaryExpression(node) {
260 operatorAllowed = options.allow.indexOf("!!") >= 0;
261 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
262 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
264 report(node, recommendation, true);
268 operatorAllowed = options.allow.indexOf("~") >= 0;
269 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
271 // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
272 const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
273 const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
275 report(node, recommendation, false);
279 operatorAllowed = options.allow.indexOf("+") >= 0;
280 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
281 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
283 report(node, recommendation, true);
287 // Use `:exit` to prevent double reporting
288 "BinaryExpression:exit"(node) {
292 operatorAllowed = options.allow.indexOf("*") >= 0;
293 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
295 if (nonNumericOperand) {
296 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
298 report(node, recommendation, true);
302 operatorAllowed = options.allow.indexOf("+") >= 0;
303 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
304 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
306 report(node, recommendation, true);
310 AssignmentExpression(node) {
313 const operatorAllowed = options.allow.indexOf("+") >= 0;
315 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
316 const code = sourceCode.getText(getNonEmptyOperand(node));
317 const recommendation = `${code} = String(${code})`;
319 report(node, recommendation, true);
323 TemplateLiteral(node) {
324 if (!options.disallowTemplateShorthand) {
329 if (node.parent.type === "TaggedTemplateExpression") {
333 // `` or `${foo}${bar}`
334 if (node.expressions.length !== 1) {
340 if (node.quasis[0].value.cooked !== "") {
345 if (node.quasis[1].value.cooked !== "") {
349 // if the expression is already a string, then this isn't a coercion
350 if (isStringType(node.expressions[0])) {
354 const code = sourceCode.getText(node.expressions[0]);
355 const recommendation = `String(${code})`;
357 report(node, recommendation, true);