no-implicit-coercion.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. /**
  2. * @fileoverview A rule to disallow the type conversions with shorter notations.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. const astUtils = require("./utils/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Helpers
  9. //------------------------------------------------------------------------------
  10. const INDEX_OF_PATTERN = /^(?:i|lastI)ndexOf$/u;
  11. const ALLOWABLE_OPERATORS = ["~", "!!", "+", "- -", "-", "*"];
  12. /**
  13. * Parses and normalizes an option object.
  14. * @param {Object} options An option object to parse.
  15. * @returns {Object} The parsed and normalized option object.
  16. */
  17. function parseOptions(options) {
  18. return {
  19. boolean: "boolean" in options ? options.boolean : true,
  20. number: "number" in options ? options.number : true,
  21. string: "string" in options ? options.string : true,
  22. disallowTemplateShorthand: "disallowTemplateShorthand" in options ? options.disallowTemplateShorthand : false,
  23. allow: options.allow || []
  24. };
  25. }
  26. /**
  27. * Checks whether or not a node is a double logical negating.
  28. * @param {ASTNode} node An UnaryExpression node to check.
  29. * @returns {boolean} Whether or not the node is a double logical negating.
  30. */
  31. function isDoubleLogicalNegating(node) {
  32. return (
  33. node.operator === "!" &&
  34. node.argument.type === "UnaryExpression" &&
  35. node.argument.operator === "!"
  36. );
  37. }
  38. /**
  39. * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
  40. * @param {ASTNode} node An UnaryExpression node to check.
  41. * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
  42. */
  43. function isBinaryNegatingOfIndexOf(node) {
  44. if (node.operator !== "~") {
  45. return false;
  46. }
  47. const callNode = astUtils.skipChainExpression(node.argument);
  48. return (
  49. callNode.type === "CallExpression" &&
  50. astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
  51. );
  52. }
  53. /**
  54. * Checks whether or not a node is a multiplying by one.
  55. * @param {BinaryExpression} node A BinaryExpression node to check.
  56. * @returns {boolean} Whether or not the node is a multiplying by one.
  57. */
  58. function isMultiplyByOne(node) {
  59. return node.operator === "*" && (
  60. node.left.type === "Literal" && node.left.value === 1 ||
  61. node.right.type === "Literal" && node.right.value === 1
  62. );
  63. }
  64. /**
  65. * Checks whether the given node logically represents multiplication by a fraction of `1`.
  66. * For example, `a * 1` in `a * 1 / b` is technically multiplication by `1`, but the
  67. * whole expression can be logically interpreted as `a * (1 / b)` rather than `(a * 1) / b`.
  68. * @param {BinaryExpression} node A BinaryExpression node to check.
  69. * @param {SourceCode} sourceCode The source code object.
  70. * @returns {boolean} Whether or not the node is a multiplying by a fraction of `1`.
  71. */
  72. function isMultiplyByFractionOfOne(node, sourceCode) {
  73. return node.type === "BinaryExpression" &&
  74. node.operator === "*" &&
  75. (node.right.type === "Literal" && node.right.value === 1) &&
  76. node.parent.type === "BinaryExpression" &&
  77. node.parent.operator === "/" &&
  78. node.parent.left === node &&
  79. !astUtils.isParenthesised(sourceCode, node);
  80. }
  81. /**
  82. * Checks whether the result of a node is numeric or not
  83. * @param {ASTNode} node The node to test
  84. * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
  85. */
  86. function isNumeric(node) {
  87. return (
  88. node.type === "Literal" && typeof node.value === "number" ||
  89. node.type === "CallExpression" && (
  90. node.callee.name === "Number" ||
  91. node.callee.name === "parseInt" ||
  92. node.callee.name === "parseFloat"
  93. )
  94. );
  95. }
  96. /**
  97. * Returns the first non-numeric operand in a BinaryExpression. Designed to be
  98. * used from bottom to up since it walks up the BinaryExpression trees using
  99. * node.parent to find the result.
  100. * @param {BinaryExpression} node The BinaryExpression node to be walked up on
  101. * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
  102. */
  103. function getNonNumericOperand(node) {
  104. const left = node.left,
  105. right = node.right;
  106. if (right.type !== "BinaryExpression" && !isNumeric(right)) {
  107. return right;
  108. }
  109. if (left.type !== "BinaryExpression" && !isNumeric(left)) {
  110. return left;
  111. }
  112. return null;
  113. }
  114. /**
  115. * Checks whether an expression evaluates to a string.
  116. * @param {ASTNode} node node that represents the expression to check.
  117. * @returns {boolean} Whether or not the expression evaluates to a string.
  118. */
  119. function isStringType(node) {
  120. return astUtils.isStringLiteral(node) ||
  121. (
  122. node.type === "CallExpression" &&
  123. node.callee.type === "Identifier" &&
  124. node.callee.name === "String"
  125. );
  126. }
  127. /**
  128. * Checks whether a node is an empty string literal or not.
  129. * @param {ASTNode} node The node to check.
  130. * @returns {boolean} Whether or not the passed in node is an
  131. * empty string literal or not.
  132. */
  133. function isEmptyString(node) {
  134. return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
  135. }
  136. /**
  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.
  140. */
  141. function isConcatWithEmptyString(node) {
  142. return node.operator === "+" && (
  143. (isEmptyString(node.left) && !isStringType(node.right)) ||
  144. (isEmptyString(node.right) && !isStringType(node.left))
  145. );
  146. }
  147. /**
  148. * Checks whether or not a node is appended with an empty string.
  149. * @param {ASTNode} node An AssignmentExpression node to check.
  150. * @returns {boolean} Whether or not the node is appended with an empty string.
  151. */
  152. function isAppendEmptyString(node) {
  153. return node.operator === "+=" && isEmptyString(node.right);
  154. }
  155. /**
  156. * Returns the operand that is not an empty string from a flagged BinaryExpression.
  157. * @param {ASTNode} node The flagged BinaryExpression node to check.
  158. * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
  159. */
  160. function getNonEmptyOperand(node) {
  161. return isEmptyString(node.left) ? node.right : node.left;
  162. }
  163. //------------------------------------------------------------------------------
  164. // Rule Definition
  165. //------------------------------------------------------------------------------
  166. /** @type {import('../shared/types').Rule} */
  167. module.exports = {
  168. meta: {
  169. hasSuggestions: true,
  170. type: "suggestion",
  171. docs: {
  172. description: "Disallow shorthand type conversions",
  173. recommended: false,
  174. url: "https://eslint.org/docs/latest/rules/no-implicit-coercion"
  175. },
  176. fixable: "code",
  177. schema: [{
  178. type: "object",
  179. properties: {
  180. boolean: {
  181. type: "boolean",
  182. default: true
  183. },
  184. number: {
  185. type: "boolean",
  186. default: true
  187. },
  188. string: {
  189. type: "boolean",
  190. default: true
  191. },
  192. disallowTemplateShorthand: {
  193. type: "boolean",
  194. default: false
  195. },
  196. allow: {
  197. type: "array",
  198. items: {
  199. enum: ALLOWABLE_OPERATORS
  200. },
  201. uniqueItems: true
  202. }
  203. },
  204. additionalProperties: false
  205. }],
  206. messages: {
  207. implicitCoercion: "Unexpected implicit coercion encountered. Use `{{recommendation}}` instead.",
  208. useRecommendation: "Use `{{recommendation}}` instead."
  209. }
  210. },
  211. create(context) {
  212. const options = parseOptions(context.options[0] || {});
  213. const sourceCode = context.sourceCode;
  214. /**
  215. * Reports an error and autofixes the node
  216. * @param {ASTNode} node An ast node to report the error on.
  217. * @param {string} recommendation The recommended code for the issue
  218. * @param {bool} shouldSuggest Whether this report should offer a suggestion
  219. * @param {bool} shouldFix Whether this report should fix the node
  220. * @returns {void}
  221. */
  222. function report(node, recommendation, shouldSuggest, shouldFix) {
  223. /**
  224. * Fix function
  225. * @param {RuleFixer} fixer The fixer to fix.
  226. * @returns {Fix} The fix object.
  227. */
  228. function fix(fixer) {
  229. const tokenBefore = sourceCode.getTokenBefore(node);
  230. if (
  231. tokenBefore?.range[1] === node.range[0] &&
  232. !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
  233. ) {
  234. return fixer.replaceText(node, ` ${recommendation}`);
  235. }
  236. return fixer.replaceText(node, recommendation);
  237. }
  238. context.report({
  239. node,
  240. messageId: "implicitCoercion",
  241. data: { recommendation },
  242. fix(fixer) {
  243. if (!shouldFix) {
  244. return null;
  245. }
  246. return fix(fixer);
  247. },
  248. suggest: [
  249. {
  250. messageId: "useRecommendation",
  251. data: { recommendation },
  252. fix(fixer) {
  253. if (shouldFix || !shouldSuggest) {
  254. return null;
  255. }
  256. return fix(fixer);
  257. }
  258. }
  259. ]
  260. });
  261. }
  262. return {
  263. UnaryExpression(node) {
  264. let operatorAllowed;
  265. // !!foo
  266. operatorAllowed = options.allow.includes("!!");
  267. if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
  268. const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
  269. const variable = astUtils.getVariableByName(sourceCode.getScope(node), "Boolean");
  270. const booleanExists = variable?.identifiers.length === 0;
  271. report(node, recommendation, true, booleanExists);
  272. }
  273. // ~foo.indexOf(bar)
  274. operatorAllowed = options.allow.includes("~");
  275. if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
  276. // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
  277. const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
  278. const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
  279. report(node, recommendation, false, false);
  280. }
  281. // +foo
  282. operatorAllowed = options.allow.includes("+");
  283. if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
  284. const recommendation = `Number(${sourceCode.getText(node.argument)})`;
  285. report(node, recommendation, true, false);
  286. }
  287. // -(-foo)
  288. operatorAllowed = options.allow.includes("- -");
  289. if (!operatorAllowed && options.number && node.operator === "-" && node.argument.type === "UnaryExpression" && node.argument.operator === "-" && !isNumeric(node.argument.argument)) {
  290. const recommendation = `Number(${sourceCode.getText(node.argument.argument)})`;
  291. report(node, recommendation, true, false);
  292. }
  293. },
  294. // Use `:exit` to prevent double reporting
  295. "BinaryExpression:exit"(node) {
  296. let operatorAllowed;
  297. // 1 * foo
  298. operatorAllowed = options.allow.includes("*");
  299. const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && !isMultiplyByFractionOfOne(node, sourceCode) &&
  300. getNonNumericOperand(node);
  301. if (nonNumericOperand) {
  302. const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
  303. report(node, recommendation, true, false);
  304. }
  305. // foo - 0
  306. operatorAllowed = options.allow.includes("-");
  307. if (!operatorAllowed && options.number && node.operator === "-" && node.right.type === "Literal" && node.right.value === 0 && !isNumeric(node.left)) {
  308. const recommendation = `Number(${sourceCode.getText(node.left)})`;
  309. report(node, recommendation, true, false);
  310. }
  311. // "" + foo
  312. operatorAllowed = options.allow.includes("+");
  313. if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
  314. const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
  315. report(node, recommendation, true, false);
  316. }
  317. },
  318. AssignmentExpression(node) {
  319. // foo += ""
  320. const operatorAllowed = options.allow.includes("+");
  321. if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
  322. const code = sourceCode.getText(getNonEmptyOperand(node));
  323. const recommendation = `${code} = String(${code})`;
  324. report(node, recommendation, true, false);
  325. }
  326. },
  327. TemplateLiteral(node) {
  328. if (!options.disallowTemplateShorthand) {
  329. return;
  330. }
  331. // tag`${foo}`
  332. if (node.parent.type === "TaggedTemplateExpression") {
  333. return;
  334. }
  335. // `` or `${foo}${bar}`
  336. if (node.expressions.length !== 1) {
  337. return;
  338. }
  339. // `prefix${foo}`
  340. if (node.quasis[0].value.cooked !== "") {
  341. return;
  342. }
  343. // `${foo}postfix`
  344. if (node.quasis[1].value.cooked !== "") {
  345. return;
  346. }
  347. // if the expression is already a string, then this isn't a coercion
  348. if (isStringType(node.expressions[0])) {
  349. return;
  350. }
  351. const code = sourceCode.getText(node.expressions[0]);
  352. const recommendation = `String(${code})`;
  353. report(node, recommendation, true, false);
  354. }
  355. };
  356. }
  357. };