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) {
50 if (node.operator !== "~") {
53 const callNode = astUtils.skipChainExpression(node.argument);
56 callNode.type === "CallExpression" &&
57 astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
62 * Checks whether or not a node is a multiplying by one.
63 * @param {BinaryExpression} node A BinaryExpression node to check.
64 * @returns {boolean} Whether or not the node is a multiplying by one.
66 function isMultiplyByOne(node) {
67 return node.operator === "*" && (
68 node.left.type === "Literal" && node.left.value === 1 ||
69 node.right.type === "Literal" && node.right.value === 1
74 * Checks whether the result of a node is numeric or not
75 * @param {ASTNode} node The node to test
76 * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
78 function isNumeric(node) {
80 node.type === "Literal" && typeof node.value === "number" ||
81 node.type === "CallExpression" && (
82 node.callee.name === "Number" ||
83 node.callee.name === "parseInt" ||
84 node.callee.name === "parseFloat"
90 * Returns the first non-numeric operand in a BinaryExpression. Designed to be
91 * used from bottom to up since it walks up the BinaryExpression trees using
92 * node.parent to find the result.
93 * @param {BinaryExpression} node The BinaryExpression node to be walked up on
94 * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
96 function getNonNumericOperand(node) {
97 const left = node.left,
100 if (right.type !== "BinaryExpression" && !isNumeric(right)) {
104 if (left.type !== "BinaryExpression" && !isNumeric(left)) {
112 * Checks whether a node is an empty string literal or not.
113 * @param {ASTNode} node The node to check.
114 * @returns {boolean} Whether or not the passed in node is an
115 * empty string literal or not.
117 function isEmptyString(node) {
118 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
122 * Checks whether or not a node is a concatenating with an empty string.
123 * @param {ASTNode} node A BinaryExpression node to check.
124 * @returns {boolean} Whether or not the node is a concatenating with an empty string.
126 function isConcatWithEmptyString(node) {
127 return node.operator === "+" && (
128 (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
129 (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
134 * Checks whether or not a node is appended with an empty string.
135 * @param {ASTNode} node An AssignmentExpression node to check.
136 * @returns {boolean} Whether or not the node is appended with an empty string.
138 function isAppendEmptyString(node) {
139 return node.operator === "+=" && isEmptyString(node.right);
143 * Returns the operand that is not an empty string from a flagged BinaryExpression.
144 * @param {ASTNode} node The flagged BinaryExpression node to check.
145 * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
147 function getNonEmptyOperand(node) {
148 return isEmptyString(node.left) ? node.right : node.left;
151 //------------------------------------------------------------------------------
153 //------------------------------------------------------------------------------
160 description: "disallow shorthand type conversions",
161 category: "Best Practices",
163 url: "https://eslint.org/docs/rules/no-implicit-coercion"
186 enum: ALLOWABLE_OPERATORS
191 additionalProperties: false
195 useRecommendation: "use `{{recommendation}}` instead."
200 const options = parseOptions(context.options[0] || {});
201 const sourceCode = context.getSourceCode();
204 * Reports an error and autofixes the node
205 * @param {ASTNode} node An ast node to report the error on.
206 * @param {string} recommendation The recommended code for the issue
207 * @param {bool} shouldFix Whether this report should fix the node
210 function report(node, recommendation, shouldFix) {
213 messageId: "useRecommendation",
222 const tokenBefore = sourceCode.getTokenBefore(node);
226 tokenBefore.range[1] === node.range[0] &&
227 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
229 return fixer.replaceText(node, ` ${recommendation}`);
231 return fixer.replaceText(node, recommendation);
237 UnaryExpression(node) {
241 operatorAllowed = options.allow.indexOf("!!") >= 0;
242 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
243 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
245 report(node, recommendation, true);
249 operatorAllowed = options.allow.indexOf("~") >= 0;
250 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
252 // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
253 const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
254 const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
256 report(node, recommendation, false);
260 operatorAllowed = options.allow.indexOf("+") >= 0;
261 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
262 const recommendation = `Number(${sourceCode.getText(node.argument)})`;
264 report(node, recommendation, true);
268 // Use `:exit` to prevent double reporting
269 "BinaryExpression:exit"(node) {
273 operatorAllowed = options.allow.indexOf("*") >= 0;
274 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
276 if (nonNumericOperand) {
277 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
279 report(node, recommendation, true);
283 operatorAllowed = options.allow.indexOf("+") >= 0;
284 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
285 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
287 report(node, recommendation, true);
291 AssignmentExpression(node) {
294 const operatorAllowed = options.allow.indexOf("+") >= 0;
296 if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
297 const code = sourceCode.getText(getNonEmptyOperand(node));
298 const recommendation = `${code} = String(${code})`;
300 report(node, recommendation, true);