no-implicit-coercion.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  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 nigating.
  28. * @param {ASTNode} node An UnaryExpression node to check.
  29. * @returns {boolean} Whether or not the node is a double logical nigating.
  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 result of a node is numeric or not
  66. * @param {ASTNode} node The node to test
  67. * @returns {boolean} true if the node is a number literal or a `Number()`, `parseInt` or `parseFloat` call
  68. */
  69. function isNumeric(node) {
  70. return (
  71. node.type === "Literal" && typeof node.value === "number" ||
  72. node.type === "CallExpression" && (
  73. node.callee.name === "Number" ||
  74. node.callee.name === "parseInt" ||
  75. node.callee.name === "parseFloat"
  76. )
  77. );
  78. }
  79. /**
  80. * Returns the first non-numeric operand in a BinaryExpression. Designed to be
  81. * used from bottom to up since it walks up the BinaryExpression trees using
  82. * node.parent to find the result.
  83. * @param {BinaryExpression} node The BinaryExpression node to be walked up on
  84. * @returns {ASTNode|null} The first non-numeric item in the BinaryExpression tree or null
  85. */
  86. function getNonNumericOperand(node) {
  87. const left = node.left,
  88. right = node.right;
  89. if (right.type !== "BinaryExpression" && !isNumeric(right)) {
  90. return right;
  91. }
  92. if (left.type !== "BinaryExpression" && !isNumeric(left)) {
  93. return left;
  94. }
  95. return null;
  96. }
  97. /**
  98. * Checks whether an expression evaluates to a string.
  99. * @param {ASTNode} node node that represents the expression to check.
  100. * @returns {boolean} Whether or not the expression evaluates to a string.
  101. */
  102. function isStringType(node) {
  103. return astUtils.isStringLiteral(node) ||
  104. (
  105. node.type === "CallExpression" &&
  106. node.callee.type === "Identifier" &&
  107. node.callee.name === "String"
  108. );
  109. }
  110. /**
  111. * Checks whether a node is an empty string literal or not.
  112. * @param {ASTNode} node The node to check.
  113. * @returns {boolean} Whether or not the passed in node is an
  114. * empty string literal or not.
  115. */
  116. function isEmptyString(node) {
  117. return astUtils.isStringLiteral(node) && (node.value === "" || (node.type === "TemplateLiteral" && node.quasis.length === 1 && node.quasis[0].value.cooked === ""));
  118. }
  119. /**
  120. * Checks whether or not a node is a concatenating with an empty string.
  121. * @param {ASTNode} node A BinaryExpression node to check.
  122. * @returns {boolean} Whether or not the node is a concatenating with an empty string.
  123. */
  124. function isConcatWithEmptyString(node) {
  125. return node.operator === "+" && (
  126. (isEmptyString(node.left) && !isStringType(node.right)) ||
  127. (isEmptyString(node.right) && !isStringType(node.left))
  128. );
  129. }
  130. /**
  131. * Checks whether or not a node is appended with an empty string.
  132. * @param {ASTNode} node An AssignmentExpression node to check.
  133. * @returns {boolean} Whether or not the node is appended with an empty string.
  134. */
  135. function isAppendEmptyString(node) {
  136. return node.operator === "+=" && isEmptyString(node.right);
  137. }
  138. /**
  139. * Returns the operand that is not an empty string from a flagged BinaryExpression.
  140. * @param {ASTNode} node The flagged BinaryExpression node to check.
  141. * @returns {ASTNode} The operand that is not an empty string from a flagged BinaryExpression.
  142. */
  143. function getNonEmptyOperand(node) {
  144. return isEmptyString(node.left) ? node.right : node.left;
  145. }
  146. //------------------------------------------------------------------------------
  147. // Rule Definition
  148. //------------------------------------------------------------------------------
  149. module.exports = {
  150. meta: {
  151. type: "suggestion",
  152. docs: {
  153. description: "disallow shorthand type conversions",
  154. category: "Best Practices",
  155. recommended: false,
  156. url: "https://eslint.org/docs/rules/no-implicit-coercion"
  157. },
  158. fixable: "code",
  159. schema: [{
  160. type: "object",
  161. properties: {
  162. boolean: {
  163. type: "boolean",
  164. default: true
  165. },
  166. number: {
  167. type: "boolean",
  168. default: true
  169. },
  170. string: {
  171. type: "boolean",
  172. default: true
  173. },
  174. disallowTemplateShorthand: {
  175. type: "boolean",
  176. default: false
  177. },
  178. allow: {
  179. type: "array",
  180. items: {
  181. enum: ALLOWABLE_OPERATORS
  182. },
  183. uniqueItems: true
  184. }
  185. },
  186. additionalProperties: false
  187. }],
  188. messages: {
  189. useRecommendation: "use `{{recommendation}}` instead."
  190. }
  191. },
  192. create(context) {
  193. const options = parseOptions(context.options[0] || {});
  194. const sourceCode = context.getSourceCode();
  195. /**
  196. * Reports an error and autofixes the node
  197. * @param {ASTNode} node An ast node to report the error on.
  198. * @param {string} recommendation The recommended code for the issue
  199. * @param {bool} shouldFix Whether this report should fix the node
  200. * @returns {void}
  201. */
  202. function report(node, recommendation, shouldFix) {
  203. context.report({
  204. node,
  205. messageId: "useRecommendation",
  206. data: {
  207. recommendation
  208. },
  209. fix(fixer) {
  210. if (!shouldFix) {
  211. return null;
  212. }
  213. const tokenBefore = sourceCode.getTokenBefore(node);
  214. if (
  215. tokenBefore &&
  216. tokenBefore.range[1] === node.range[0] &&
  217. !astUtils.canTokensBeAdjacent(tokenBefore, recommendation)
  218. ) {
  219. return fixer.replaceText(node, ` ${recommendation}`);
  220. }
  221. return fixer.replaceText(node, recommendation);
  222. }
  223. });
  224. }
  225. return {
  226. UnaryExpression(node) {
  227. let operatorAllowed;
  228. // !!foo
  229. operatorAllowed = options.allow.indexOf("!!") >= 0;
  230. if (!operatorAllowed && options.boolean && isDoubleLogicalNegating(node)) {
  231. const recommendation = `Boolean(${sourceCode.getText(node.argument.argument)})`;
  232. report(node, recommendation, true);
  233. }
  234. // ~foo.indexOf(bar)
  235. operatorAllowed = options.allow.indexOf("~") >= 0;
  236. if (!operatorAllowed && options.boolean && isBinaryNegatingOfIndexOf(node)) {
  237. // `foo?.indexOf(bar) !== -1` will be true (== found) if the `foo` is nullish. So use `>= 0` in that case.
  238. const comparison = node.argument.type === "ChainExpression" ? ">= 0" : "!== -1";
  239. const recommendation = `${sourceCode.getText(node.argument)} ${comparison}`;
  240. report(node, recommendation, false);
  241. }
  242. // +foo
  243. operatorAllowed = options.allow.indexOf("+") >= 0;
  244. if (!operatorAllowed && options.number && node.operator === "+" && !isNumeric(node.argument)) {
  245. const recommendation = `Number(${sourceCode.getText(node.argument)})`;
  246. report(node, recommendation, true);
  247. }
  248. },
  249. // Use `:exit` to prevent double reporting
  250. "BinaryExpression:exit"(node) {
  251. let operatorAllowed;
  252. // 1 * foo
  253. operatorAllowed = options.allow.indexOf("*") >= 0;
  254. const nonNumericOperand = !operatorAllowed && options.number && isMultiplyByOne(node) && getNonNumericOperand(node);
  255. if (nonNumericOperand) {
  256. const recommendation = `Number(${sourceCode.getText(nonNumericOperand)})`;
  257. report(node, recommendation, true);
  258. }
  259. // "" + foo
  260. operatorAllowed = options.allow.indexOf("+") >= 0;
  261. if (!operatorAllowed && options.string && isConcatWithEmptyString(node)) {
  262. const recommendation = `String(${sourceCode.getText(getNonEmptyOperand(node))})`;
  263. report(node, recommendation, true);
  264. }
  265. },
  266. AssignmentExpression(node) {
  267. // foo += ""
  268. const operatorAllowed = options.allow.indexOf("+") >= 0;
  269. if (!operatorAllowed && options.string && isAppendEmptyString(node)) {
  270. const code = sourceCode.getText(getNonEmptyOperand(node));
  271. const recommendation = `${code} = String(${code})`;
  272. report(node, recommendation, true);
  273. }
  274. },
  275. TemplateLiteral(node) {
  276. if (!options.disallowTemplateShorthand) {
  277. return;
  278. }
  279. // tag`${foo}`
  280. if (node.parent.type === "TaggedTemplateExpression") {
  281. return;
  282. }
  283. // `` or `${foo}${bar}`
  284. if (node.expressions.length !== 1) {
  285. return;
  286. }
  287. // `prefix${foo}`
  288. if (node.quasis[0].value.cooked !== "") {
  289. return;
  290. }
  291. // `${foo}postfix`
  292. if (node.quasis[1].value.cooked !== "") {
  293. return;
  294. }
  295. // if the expression is already a string, then this isn't a coercion
  296. if (isStringType(node.expressions[0])) {
  297. return;
  298. }
  299. const code = sourceCode.getText(node.expressions[0]);
  300. const recommendation = `String(${code})`;
  301. report(node, recommendation, true);
  302. }
  303. };
  304. }
  305. };