prefer-native-coercion-functions.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. 'use strict';
  2. const {getFunctionHeadLocation, getFunctionNameWithKind} = require('@eslint-community/eslint-utils');
  3. const {not} = require('./selectors/index.js');
  4. const MESSAGE_ID = 'prefer-native-coercion-functions';
  5. const messages = {
  6. [MESSAGE_ID]: '{{functionNameWithKind}} is equivalent to `{{replacementFunction}}`. Use `{{replacementFunction}}` directly.',
  7. };
  8. const nativeCoercionFunctionNames = new Set(['String', 'Number', 'BigInt', 'Boolean', 'Symbol']);
  9. const arrayMethodsWithBooleanCallback = new Set(['every', 'filter', 'find', 'findLast', 'findIndex', 'findLastIndex', 'some']);
  10. const isNativeCoercionFunctionCall = (node, firstArgumentName) =>
  11. node?.type === 'CallExpression'
  12. && !node.optional
  13. && node.callee.type === 'Identifier'
  14. && nativeCoercionFunctionNames.has(node.callee.name)
  15. && node.arguments[0]?.type === 'Identifier'
  16. && node.arguments[0].name === firstArgumentName;
  17. const isIdentityFunction = node =>
  18. (
  19. // `v => v`
  20. node.type === 'ArrowFunctionExpression'
  21. && node.body.type === 'Identifier'
  22. && node.body.name === node.params[0].name
  23. )
  24. || (
  25. // `(v) => {return v;}`
  26. // `function (v) {return v;}`
  27. node.body.type === 'BlockStatement'
  28. && node.body.body.length === 1
  29. && node.body.body[0].type === 'ReturnStatement'
  30. && node.body.body[0].argument?.type === 'Identifier'
  31. && node.body.body[0].argument.name === node.params[0].name
  32. );
  33. const isArrayIdentityCallback = node =>
  34. isIdentityFunction(node)
  35. && node.parent.type === 'CallExpression'
  36. && !node.parent.optional
  37. && node.parent.arguments[0] === node
  38. && node.parent.callee.type === 'MemberExpression'
  39. && !node.parent.callee.computed
  40. && !node.parent.callee.optional
  41. && node.parent.callee.property.type === 'Identifier'
  42. && arrayMethodsWithBooleanCallback.has(node.parent.callee.property.name);
  43. function getCallExpression(node) {
  44. const firstParameterName = node.params[0].name;
  45. // `(v) => String(v)`
  46. if (
  47. node.type === 'ArrowFunctionExpression'
  48. && isNativeCoercionFunctionCall(node.body, firstParameterName)
  49. ) {
  50. return node.body;
  51. }
  52. // `(v) => {return String(v);}`
  53. // `function (v) {return String(v);}`
  54. if (
  55. node.body.type === 'BlockStatement'
  56. && node.body.body.length === 1
  57. && node.body.body[0].type === 'ReturnStatement'
  58. && isNativeCoercionFunctionCall(node.body.body[0].argument, firstParameterName)
  59. ) {
  60. return node.body.body[0].argument;
  61. }
  62. }
  63. const functionsSelector = [
  64. ':function',
  65. '[async!=true]',
  66. '[generator!=true]',
  67. '[params.length>0]',
  68. '[params.0.type="Identifier"]',
  69. not([
  70. 'MethodDefinition[kind="constructor"] > .value',
  71. 'MethodDefinition[kind="set"] > .value',
  72. 'Property[kind="set"] > .value',
  73. ]),
  74. ].join('');
  75. function getArrayCallbackProblem(node) {
  76. if (!isArrayIdentityCallback(node)) {
  77. return;
  78. }
  79. return {
  80. replacementFunction: 'Boolean',
  81. fix: fixer => fixer.replaceText(node, 'Boolean'),
  82. };
  83. }
  84. function getCoercionFunctionProblem(node) {
  85. const callExpression = getCallExpression(node);
  86. if (!callExpression) {
  87. return;
  88. }
  89. const {name} = callExpression.callee;
  90. const problem = {replacementFunction: name};
  91. if (node.type === 'FunctionDeclaration' || callExpression.arguments.length !== 1) {
  92. return problem;
  93. }
  94. /** @param {import('eslint').Rule.RuleFixer} fixer */
  95. problem.fix = fixer => {
  96. let text = name;
  97. if (
  98. node.parent.type === 'Property'
  99. && node.parent.method
  100. && node.parent.value === node
  101. ) {
  102. text = `: ${text}`;
  103. } else if (node.parent.type === 'MethodDefinition') {
  104. text = ` = ${text};`;
  105. }
  106. return fixer.replaceText(node, text);
  107. };
  108. return problem;
  109. }
  110. /** @param {import('eslint').Rule.RuleContext} context */
  111. const create = context => ({
  112. [functionsSelector](node) {
  113. let problem = getArrayCallbackProblem(node) || getCoercionFunctionProblem(node);
  114. if (!problem) {
  115. return;
  116. }
  117. const sourceCode = context.getSourceCode();
  118. const {replacementFunction, fix} = problem;
  119. problem = {
  120. node,
  121. loc: getFunctionHeadLocation(node, sourceCode),
  122. messageId: MESSAGE_ID,
  123. data: {
  124. functionNameWithKind: getFunctionNameWithKind(node, sourceCode),
  125. replacementFunction,
  126. },
  127. };
  128. /*
  129. We do not fix if there are:
  130. - Comments: No proper place to put them.
  131. - Extra parameters: Removing them may break types.
  132. */
  133. if (!fix || node.params.length !== 1 || sourceCode.getCommentsInside(node).length > 0) {
  134. return problem;
  135. }
  136. problem.fix = fix;
  137. return problem;
  138. },
  139. });
  140. /** @type {import('eslint').Rule.RuleModule} */
  141. module.exports = {
  142. create,
  143. meta: {
  144. type: 'suggestion',
  145. docs: {
  146. description: 'Prefer using `String`, `Number`, `BigInt`, `Boolean`, and `Symbol` directly.',
  147. },
  148. fixable: 'code',
  149. messages,
  150. },
  151. };