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 //------------------------------------------------------------------------------
170 /** @type {import('../shared/types').Rule} */
176 description
: "disallow shorthand type conversions",
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);