| /** |
| * @fileoverview A rule to disallow the type conversions with shorter notations. |
| * @author Toru Nagashima |
| */ |
| |
| "use strict"; |
| |
| const astUtils = require("./utils/ast-utils"); |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u; |
| const ALLOWABLE_OPERATORS = ["~", "!!", "+", "*"]; |
| |
| /** |
| * Parses and normalizes an option object. |
| * @param {Object} options - An option object to parse. |
| * @returns {Object} The parsed and normalized option object. |
| */ |
| function parseOptions(options) { |
| return { |
| boolean: "boolean" in options ? options.boolean : true, |
| number: "number" in options ? options.number : true, |
| string: "string" in options ? options.string : true, |
| allow: options.allow || [] |
| }; |
| } |
| |
| /** |
| * Checks whether or not a node is a double logical nigating. |
| * @param {ASTNode} node - An UnaryExpression node to check. |
| * @returns {boolean} Whether or not the node is a double logical nigating. |
| */ |
| function isDoubleLogicalNegating(node) { |
| return ( |
| node.operator === "!" && |
| node.argument.type === "UnaryExpression" && |
| node.argument.operator === "!" |
| ); |
| } |
| |
| /** |
| * Checks whether or not a node is a binary negating of `.indexOf()` method calling. |
| * @param {ASTNode} node - An UnaryExpression node to check. |
| * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling. |
| */ |
| function isBinaryNegatingOfIndexOf(node) { |
| return ( |
| node.operator === "~" && |
| node.argument.type === "CallExpression" && |
| node.argument.callee.type === "MemberExpression" && |
| node.argument.callee.property.type === "Identifier" && |
| INDEX_OF_PATTERN.test(node.argument.callee.property.name) |
| ); |
| } |
| |
| /** |
| * Checks whether or not a node is a multiplying by one. |
| * @param {BinaryExpression} node - A BinaryExpression node to check. |
| * @returns {boolean} Whether or not the node is a multiplying by one. |
| */ |
| function isMultiplyByOne(node) { |
| return node.operator === "*" && ( |
| node.left.type === "Literal" && node.left.value === 1 || |
| node.right.type === "Literal" && node.right.value === 1 |
| ); |
| } |
| |
| /** |
| * Checks whether the result of a node is numeric or not |
| * @param {ASTNode} node The node to test |
| * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call |
| */ |
| function isNumeric(node) { |
| return ( |
| node.type === "Literal" && typeof node.value === "number" || |
| node.type === "CallExpression" && ( |
| node.callee.name === "Number" || |
| node.callee.name === "parseInt" || |
| node.callee.name === "parseFloat" |
| ) |
| ); |
| } |
| |
| /** |
| * Returns the first non-numeric operand in a BinaryExpression. Designed to be |
| * used from bottom to up since it walks up the BinaryExpression trees using |
| * node.parent to find the result. |
| * @param {BinaryExpression} node The BinaryExpression node to be walked up on |
| * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null |
| */ |
| function getNonNumericOperand(node) { |
| const left = node.left, |
| right = node.right; |
| |
| if (right.type !== "BinaryExpression" && !isNumeric(right)) { |
| return right; |
| } |
| |
| if (left.type !== "BinaryExpression" && !isNumeric(left)) { |
| return left; |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Checks whether a node is an empty string literal or not. |
| * @param {ASTNode} node The node to check. |
| * @returns {boolean} Whether or not the passed in node is an |
| * empty string literal or not. |
| */ |
| function isEmptyString(node) { |
| return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === "")); |
| } |
| |
| /** |
| * Checks whether or not a node is a concatenating with an empty string. |
| * @param {ASTNode} node - A BinaryExpression node to check. |
| * @returns {boolean} Whether or not the node is a concatenating with an empty string. |
| */ |
| function isConcatWithEmptyString(node) { |
| return node.operator === "+" && ( |
| (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) || |
| (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left)) |
| ); |
| } |
| |
| /** |
| * Checks whether or not a node is appended with an empty string. |
| * @param {ASTNode} node - An AssignmentExpression node to check. |
| * @returns {boolean} Whether or not the node is appended with an empty string. |
| */ |
| function isAppendEmptyString(node) { |
| return node.operator === "+=" && isEmptyString(node.right); |
| } |
| |
| /** |
| * Returns the operand that is not an empty string from a flagged BinaryExpression. |
| * @param {ASTNode} node - The flagged BinaryExpression node to check. |
| * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression. |
| */ |
| function getNonEmptyOperand(node) { |
| return isEmptyString(node.left) ? node.right : node.left; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "disallow shorthand type conversions", |
| category: "Best Practices", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/no-implicit-coercion" |
| }, |
| |
| fixable: "code", |
| |
| schema: [{ |
| type: "object", |
| properties: { |
| boolean: { |
| type: "boolean", |
| default: true |
| }, |
| number: { |
| type: "boolean", |
| default: true |
| }, |
| string: { |
| type: "boolean", |
| default: true |
| }, |
| allow: { |
| type: "array", |
| items: { |
| enum: ALLOWABLE_OPERATORS |
| }, |
| uniqueItems: true |
| } |
| }, |
| additionalProperties: false |
| }] |
| }, |
| |
| create(context) { |
| const options = parseOptions(context.options[0] || {}); |
| const sourceCode = context.getSourceCode(); |
| |
| /** |
| * Reports an error and autofixes the node |
| * @param {ASTNode} node - An ast node to report the error on. |
| * @param {string} recommendation - The recommended code for the issue |
| * @param {bool} shouldFix - Whether this report should fix the node |
| * @returns {void} |
| */ |
| function report(node, recommendation, shouldFix) { |
| context.report({ |
| node, |
| message: "use `{{recommendation}}` instead.", |
| data: { |
| recommendation |
| }, |
| fix(fixer) { |
| if (!shouldFix) { |
| return null; |
| } |
| |
| const tokenBefore = sourceCode.getTokenBefore(node); |
| |
| if ( |
| tokenBefore && |
| tokenBefore.range[1] === node.range[0] && |
| !astUtils.canTokensBeAdjacent(tokenBefore, recommendation) |
| ) { |
| return fixer.replaceText(node, ` ${recommendation}`); |
| } |
| return fixer.replaceText(node, recommendation); |
| } |
| }); |
| } |
| |
| return { |
| UnaryExpression(node) { |
| let operatorAllowed; |
| |
| // !!foo |
| operatorAllowed = options.allow.indexOf("!!") >= 0; |
| if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) { |
| const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`; |
| |
| report(node, recommendation, true); |
| } |
| |
| // ~foo.indexOf(bar) |
| operatorAllowed = options.allow.indexOf("~") >= 0; |
| if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) { |
| const recommendation = `${sourceCode.getText(node.argument)} !== -1`; |
| |
| report(node, recommendation, false); |
| } |
| |
| // +foo |
| operatorAllowed = options.allow.indexOf("+") >= 0; |
| if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) { |
| const recommendation = `Number(${sourceCode.getText(node.argument)})`; |
| |
| report(node, recommendation, true); |
| } |
| }, |
| |
| // Use `:exit` to prevent double reporting |
| "BinaryExpression:exit"(node) { |
| let operatorAllowed; |
| |
| // 1 * foo |
| operatorAllowed = options.allow.indexOf("*") >= 0; |
| const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node); |
| |
| if (nonNumericOperand) { |
| const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`; |
| |
| report(node, recommendation, true); |
| } |
| |
| // "" + foo |
| operatorAllowed = options.allow.indexOf("+") >= 0; |
| if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) { |
| const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`; |
| |
| report(node, recommendation, true); |
| } |
| }, |
| |
| AssignmentExpression(node) { |
| |
| // foo += "" |
| const operatorAllowed = options.allow.indexOf("+") >= 0; |
| |
| if (!operatorAllowed && options.string && isAppendEmptyString(node)) { |
| const code = sourceCode.getText(getNonEmptyOperand(node)); |
| const recommendation = `${code} = String(${code})`; |
| |
| report(node, recommendation, true); |
| } |
| } |
| }; |
| } |
| }; |