no-implicit-coercion.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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. allow: options.allow || []
  23. };
  24. }
  25. /**
  26. * Checks whether or not a node is a double logical nigating.
  27. * @param {ASTNode} node An UnaryExpression node to check.
  28. * @returns {boolean} Whether or not the node is a double logical nigating.
  29. */
  30. function isDoubleLogicalNegating(node) {
  31. return (
  32. node.operator === "!" &&
  33. node.argument.type === "UnaryExpression" &&
  34. node.argument.operator === "!"
  35. );
  36. }
  37. /**
  38. * Checks whether or not a node is a binary negating of `.indexOf()` method calling.
  39. * @param {ASTNode} node An UnaryExpression node to check.
  40. * @returns {boolean} Whether or not the node is a binary negating of `.indexOf()` method calling.
  41. */
  42. function isBinaryNegatingOfIndexOf(node) {
  43. if (node.operator !== "~") {
  44. return false;
  45. }
  46. const callNode = astUtils.skipChainExpression(node.argument);
  47. return (
  48. callNode.type === "CallExpression" &&
  49. astUtils.isSpecificMemberAccess(callNode.callee, null, INDEX_OF_PATTERN)
  50. );
  51. }
  52. /**
  53. * Checks whether or not a node is a multiplying by one.
  54. * @param {BinaryExpression} node A BinaryExpression node to check.
  55. * @returns {boolean} Whether or not the node is a multiplying by one.
  56. */
  57. function isMultiplyByOne(node) {
  58. return node.operator === "*" && (
  59. node.left.type === "Literal" && node.left.value === 1 ||
  60. node.right.type === "Literal" && node.right.value === 1
  61. );
  62. }
  63. /**
  64. * Checks whether the result of a node is numeric or not
  65. * @param {ASTNode} node The node to test
  66. * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
  67. */
  68. function isNumeric(node) {
  69. return (
  70. node.type === "Literal" && typeof node.value === "number" ||
  71. node.type === "CallExpression" && (
  72. node.callee.name === "Number" ||
  73. node.callee.name === "parseInt" ||
  74. node.callee.name === "parseFloat"
  75. )
  76. );
  77. }
  78. /**
  79. * Returns the first non-numeric operand in a BinaryExpression. Designed to be
  80. * used from bottom to up since it walks up the BinaryExpression trees using
  81. * node.parent to find the result.
  82. * @param {BinaryExpression} node The BinaryExpression node to be walked up on
  83. * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
  84. */
  85. function getNonNumericOperand(node) {
  86. const left = node.left,
  87. right = node.right;
  88. if (right.type !== "BinaryExpression" && !isNumeric(right)) {
  89. return right;
  90. }
  91. if (left.type !== "BinaryExpression" && !isNumeric(left)) {
  92. return left;
  93. }
  94. return null;
  95. }
  96. /**
  97. * Checks whether a node is an empty string literal or not.
  98. * @param {ASTNode} node The node to check.
  99. * @returns {boolean} Whether or not the passed in node is an
  100. * empty string literal or not.
  101. */
  102. function isEmptyString(node) {
  103. return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
  104. }
  105. /**
  106. * Checks whether or not a node is a concatenating with an empty string.
  107. * @param {ASTNode} node A BinaryExpression node to check.
  108. * @returns {boolean} Whether or not the node is a concatenating with an empty string.
  109. */
  110. function isConcatWithEmptyString(node) {
  111. return node.operator === "+" && (
  112. (isEmptyString(node.left) && !astUtils.isStringLiteral(node.right)) ||
  113. (isEmptyString(node.right) && !astUtils.isStringLiteral(node.left))
  114. );
  115. }
  116. /**
  117. * Checks whether or not a node is appended with an empty string.
  118. * @param {ASTNode} node An AssignmentExpression node to check.
  119. * @returns {boolean} Whether or not the node is appended with an empty string.
  120. */
  121. function isAppendEmptyString(node) {
  122. return node.operator === "+=" && isEmptyString(node.right);
  123. }
  124. /**
  125. * Returns the operand that is not an empty string from a flagged BinaryExpression.
  126. * @param {ASTNode} node The flagged BinaryExpression node to check.
  127. * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
  128. */
  129. function getNonEmptyOperand(node) {
  130. return isEmptyString(node.left) ? node.right : node.left;
  131. }
  132. //------------------------------------------------------------------------------
  133. // Rule Definition
  134. //------------------------------------------------------------------------------
  135. module.exports = {
  136. meta: {
  137. type: "suggestion",
  138. docs: {
  139. description: "disallow shorthand type conversions",
  140. category: "Best Practices",
  141. recommended: false,
  142. url: "https://eslint.org/docs/rules/no-implicit-coercion"
  143. },
  144. fixable: "code",
  145. schema: [{
  146. type: "object",
  147. properties: {
  148. boolean: {
  149. type: "boolean",
  150. default: true
  151. },
  152. number: {
  153. type: "boolean",
  154. default: true
  155. },
  156. string: {
  157. type: "boolean",
  158. default: true
  159. },
  160. allow: {
  161. type: "array",
  162. items: {
  163. enum: ALLOWABLE_OPERATORS
  164. },
  165. uniqueItems: true
  166. }
  167. },
  168. additionalProperties: false
  169. }],
  170. messages: {
  171. useRecommendation: "use `{{recommendation}}` instead."
  172. }
  173. },
  174. create(context) {
  175. const options = parseOptions(context.options[0] || {});
  176. const sourceCode = context.getSourceCode();
  177. /**
  178. * Reports an error and autofixes the node
  179. * @param {ASTNode} node An ast node to report the error on.
  180. * @param {string} recommendation The recommended code for the issue
  181. * @param {bool} shouldFix Whether this report should fix the node
  182. * @returns {void}
  183. */
  184. function report(node, recommendation, shouldFix) {
  185. context.report({
  186. node,
  187. messageId: "useRecommendation",
  188. data: {
  189. recommendation
  190. },
  191. fix(fixer) {
  192. if (!shouldFix) {
  193. return null;
  194. }
  195. const tokenBefore = sourceCode.getTokenBefore(node);
  196. if (
  197. tokenBefore &&
  198. tokenBefore.range[1] === node.range[0] &&
  199. !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
  200. ) {
  201. return fixer.replaceText(node, ` ${recommendation}`);
  202. }
  203. return fixer.replaceText(node, recommendation);
  204. }
  205. });
  206. }
  207. return {
  208. UnaryExpression(node) {
  209. let operatorAllowed;
  210. // !!foo
  211. operatorAllowed = options.allow.indexOf("!!") >= 0;
  212. if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
  213. const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
  214. report(node, recommendation, true);
  215. }
  216. // ~foo.indexOf(bar)
  217. operatorAllowed = options.allow.indexOf("~") >= 0;
  218. if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
  219. // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
  220. const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
  221. const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
  222. report(node, recommendation, false);
  223. }
  224. // +foo
  225. operatorAllowed = options.allow.indexOf("+") >= 0;
  226. if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
  227. const recommendation = `Number(${sourceCode.getText(node.argument)})`;
  228. report(node, recommendation, true);
  229. }
  230. },
  231. // Use `:exit` to prevent double reporting
  232. "BinaryExpression:exit"(node) {
  233. let operatorAllowed;
  234. // 1 * foo
  235. operatorAllowed = options.allow.indexOf("*") >= 0;
  236. const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
  237. if (nonNumericOperand) {
  238. const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
  239. report(node, recommendation, true);
  240. }
  241. // "" + foo
  242. operatorAllowed = options.allow.indexOf("+") >= 0;
  243. if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
  244. const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
  245. report(node, recommendation, true);
  246. }
  247. },
  248. AssignmentExpression(node) {
  249. // foo += ""
  250. const operatorAllowed = options.allow.indexOf("+") >= 0;
  251. if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
  252. const code = sourceCode.getText(getNonEmptyOperand(node));
  253. const recommendation = `${code} = String(${code})`;
  254. report(node, recommendation, true);
  255. }
  256. }
  257. };
  258. }
  259. };