no-implicit-coercion.js 12 KB

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