prefer-native-coercion-functions.js 4.6 KB

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