1/** 2 * @fileoverview A rule to disallow the type conversions with shorter notations. 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8const astUtils = require("./utils/ast-utils"); 9 10//------------------------------------------------------------------------------ 11// Helpers 12//------------------------------------------------------------------------------ 13 14const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u; 15const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"]; 16 17/** 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. 21 */ 22function parseOptions(options) { 23 return { 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 || [] 28 }; 29} 30 31/** 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. 35 */ 36function isDoubleLogicalNegating(node) { 37 return ( 38 node.operator === "!" && 39 node.argument.type === "UnaryExpression" && 40 node.argument.operator === "!" 41 ); 42} 43 44/** 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. 48 */ 49function isBinaryNegatingOfIndexOf(node) { 50 if (node.operator !== "~") { 51 return false; 52 } 53 const callNode = astUtils.skipChainExpression(node.argument); 54 55 return ( 56 callNode.type === "CallExpression" && 57 astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN) 58 ); 59} 60 61/** 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. 65 */ 66function isMultiplyByOne(node) { 67 return node.operator === "*" && ( 68 node.left.type === "Literal" && node.left.value === 1 || 69 node.right.type === "Literal" && node.right.value === 1 70 ); 71} 72 73/** 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 77 */ 78function isNumeric(node) { 79 return ( 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" 85 ) 86 ); 87} 88 89/** 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 95 */ 96function getNonNumericOperand(node) { 97 const left = node.left, 98 right = node.right; 99 100 if (right.type !== "BinaryExpression" && !isNumeric(right)) { 101 return right; 102 } 103 104 if (left.type !== "BinaryExpression" && !isNumeric(left)) { 105 return left; 106 } 107 108 return null; 109} 110 111/** 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. 116 */ 117function isEmptyString(node) { 118 return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === "")); 119} 120 121/** 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. 125 */ 126function isConcatWithEmptyString(node) { 127 return node.operator === "+" && ( 128 (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) || 129 (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left)) 130 ); 131} 132 133/** 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. 137 */ 138function isAppendEmptyString(node) { 139 return node.operator === "+=" && isEmptyString(node.right); 140} 141 142/** 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. 146 */ 147function getNonEmptyOperand(node) { 148 return isEmptyString(node.left) ? node.right : node.left; 149} 150 151//------------------------------------------------------------------------------ 152// Rule Definition 153//------------------------------------------------------------------------------ 154 155module.exports = { 156 meta: { 157 type: "suggestion", 158 159 docs: { 160 description: "disallow shorthand type conversions", 161 category: "Best Practices", 162 recommended: false, 163 url: "https://eslint.org/docs/rules/no-implicit-coercion" 164 }, 165 166 fixable: "code", 167 168 schema: [{ 169 type: "object", 170 properties: { 171 boolean: { 172 type: "boolean", 173 default: true 174 }, 175 number: { 176 type: "boolean", 177 default: true 178 }, 179 string: { 180 type: "boolean", 181 default: true 182 }, 183 allow: { 184 type: "array", 185 items: { 186 enum: ALLOWABLE_OPERATORS 187 }, 188 uniqueItems: true 189 } 190 }, 191 additionalProperties: false 192 }], 193 194 messages: { 195 useRecommendation: "use `{{recommendation}}` instead." 196 } 197 }, 198 199 create(context) { 200 const options = parseOptions(context.options[0] || {}); 201 const sourceCode = context.getSourceCode(); 202 203 /** 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 208 * @returns {void} 209 */ 210 function report(node, recommendation, shouldFix) { 211 context.report({ 212 node, 213 messageId: "useRecommendation", 214 data: { 215 recommendation 216 }, 217 fix(fixer) { 218 if (!shouldFix) { 219 return null; 220 } 221 222 const tokenBefore = sourceCode.getTokenBefore(node); 223 224 if ( 225 tokenBefore && 226 tokenBefore.range[1] === node.range[0] && 227 !astUtils.canTokensBeAdjacent(tokenBefore, recommendation) 228 ) { 229 return fixer.replaceText(node, ` ${recommendation}`); 230 } 231 return fixer.replaceText(node, recommendation); 232 } 233 }); 234 } 235 236 return { 237 UnaryExpression(node) { 238 let operatorAllowed; 239 240 // !!foo 241 operatorAllowed = options.allow.indexOf("!!") >= 0; 242 if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) { 243 const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`; 244 245 report(node, recommendation, true); 246 } 247 248 // ~foo.indexOf(bar) 249 operatorAllowed = options.allow.indexOf("~") >= 0; 250 if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { 251 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}`; 255 256 report(node, recommendation, false); 257 } 258 259 // +foo 260 operatorAllowed = options.allow.indexOf("+") >= 0; 261 if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) { 262 const recommendation = `Number(${sourceCode.getText(node.argument)})`; 263 264 report(node, recommendation, true); 265 } 266 }, 267 268 // Use `:exit` to prevent double reporting 269 "BinaryExpression:exit"(node) { 270 let operatorAllowed; 271 272 // 1 * foo 273 operatorAllowed = options.allow.indexOf("*") >= 0; 274 const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node); 275 276 if (nonNumericOperand) { 277 const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`; 278 279 report(node, recommendation, true); 280 } 281 282 // "" + foo 283 operatorAllowed = options.allow.indexOf("+") >= 0; 284 if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) { 285 const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`; 286 287 report(node, recommendation, true); 288 } 289 }, 290 291 AssignmentExpression(node) { 292 293 // foo += "" 294 const operatorAllowed = options.allow.indexOf("+") >= 0; 295 296 if (!operatorAllowed && options.string && isAppendEmptyString(node)) { 297 const code = sourceCode.getText(getNonEmptyOperand(node)); 298 const recommendation = `${code} = String(${code})`; 299 300 report(node, recommendation, true); 301 } 302 } 303 }; 304 } 305}; 306