no-implicit-coercion.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. type: "suggestion",
  170. docs: {
  171. description: "Disallow shorthand type conversions",
  172. recommended: false,
  173. url: "https://eslint.org/docs/rules/no-implicit-coercion"
  174. },
  175. fixable: "code",
  176. schema: [{
  177. type: "object",
  178. properties: {
  179. boolean: {
  180. type: "boolean",
  181. default: true
  182. },
  183. number: {
  184. type: "boolean",
  185. default: true
  186. },
  187. string: {
  188. type: "boolean",
  189. default: true
  190. },
  191. disallowTemplateShorthand: {
  192. type: "boolean",
  193. default: false
  194. },
  195. allow: {
  196. type: "array",
  197. items: {
  198. enum: ALLOWABLE_OPERATORS
  199. },
  200. uniqueItems: true
  201. }
  202. },
  203. additionalProperties: false
  204. }],
  205. messages: {
  206. useRecommendation: "use `{{recommendation}}` instead."
  207. }
  208. },
  209. create(context) {
  210. const options = parseOptions(context.options[0] || {});
  211. const sourceCode = context.getSourceCode();
  212. /**
  213. * Reports an error and autofixes the node
  214. * @param {ASTNode} node An ast node to report the error on.
  215. * @param {string} recommendation The recommended code for the issue
  216. * @param {bool} shouldFix Whether this report should fix the node
  217. * @returns {void}
  218. */
  219. function report(node, recommendation, shouldFix) {
  220. context.report({
  221. node,
  222. messageId: "useRecommendation",
  223. data: {
  224. recommendation
  225. },
  226. fix(fixer) {
  227. if (!shouldFix) {
  228. return null;
  229. }
  230. const tokenBefore = sourceCode.getTokenBefore(node);
  231. if (
  232. tokenBefore &&
  233. tokenBefore.range[1] === node.range[0] &&
  234. !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
  235. ) {
  236. return fixer.replaceText(node, ` ${recommendation}`);
  237. }
  238. return fixer.replaceText(node, recommendation);
  239. }
  240. });
  241. }
  242. return {
  243. UnaryExpression(node) {
  244. let operatorAllowed;
  245. // !!foo
  246. operatorAllowed = options.allow.includes("!!");
  247. if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
  248. const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
  249. report(node, recommendation, true);
  250. }
  251. // ~foo.indexOf(bar)
  252. operatorAllowed = options.allow.includes("~");
  253. if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
  254. // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
  255. const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
  256. const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
  257. report(node, recommendation, false);
  258. }
  259. // +foo
  260. operatorAllowed = options.allow.includes("+");
  261. if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
  262. const recommendation = `Number(${sourceCode.getText(node.argument)})`;
  263. report(node, recommendation, true);
  264. }
  265. },
  266. // Use `:exit` to prevent double reporting
  267. "BinaryExpression:exit"(node) {
  268. let operatorAllowed;
  269. // 1 * foo
  270. operatorAllowed = options.allow.includes("*");
  271. const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && !isMultiplyByFractionOfOne(node, sourceCode) &&
  272. getNonNumericOperand(node);
  273. if (nonNumericOperand) {
  274. const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
  275. report(node, recommendation, true);
  276. }
  277. // "" + foo
  278. operatorAllowed = options.allow.includes("+");
  279. if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
  280. const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
  281. report(node, recommendation, true);
  282. }
  283. },
  284. AssignmentExpression(node) {
  285. // foo += ""
  286. const operatorAllowed = options.allow.includes("+");
  287. if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
  288. const code = sourceCode.getText(getNonEmptyOperand(node));
  289. const recommendation = `${code} = String(${code})`;
  290. report(node, recommendation, true);
  291. }
  292. },
  293. TemplateLiteral(node) {
  294. if (!options.disallowTemplateShorthand) {
  295. return;
  296. }
  297. // tag`${foo}`
  298. if (node.parent.type === "TaggedTemplateExpression") {
  299. return;
  300. }
  301. // `` or `${foo}${bar}`
  302. if (node.expressions.length !== 1) {
  303. return;
  304. }
  305. // `prefix${foo}`
  306. if (node.quasis[0].value.cooked !== "") {
  307. return;
  308. }
  309. // `${foo}postfix`
  310. if (node.quasis[1].value.cooked !== "") {
  311. return;
  312. }
  313. // if the expression is already a string, then this isn't a coercion
  314. if (isStringType(node.expressions[0])) {
  315. return;
  316. }
  317. const code = sourceCode.getText(node.expressions[0]);
  318. const recommendation = `String(${code})`;
  319. report(node, recommendation, true);
  320. }
  321. };
  322. }
  323. };