prefer-string-slice.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. 'use strict';
  2. const eslintTemplateVisitor = require('eslint-template-visitor');
  3. const {getParenthesizedText} = require('./utils/parentheses.js');
  4. const MESSAGE_ID_SUBSTR = 'substr';
  5. const MESSAGE_ID_SUBSTRING = 'substring';
  6. const messages = {
  7. [MESSAGE_ID_SUBSTR]: 'Prefer `String#slice()` over `String#substr()`.',
  8. [MESSAGE_ID_SUBSTRING]: 'Prefer `String#slice()` over `String#substring()`.',
  9. };
  10. const templates = eslintTemplateVisitor();
  11. const objectVariable = templates.variable();
  12. const argumentsVariable = templates.spreadVariable();
  13. const substrCallTemplate = templates.template`${objectVariable}.substr(${argumentsVariable})`;
  14. const substringCallTemplate = templates.template`${objectVariable}.substring(${argumentsVariable})`;
  15. const isLiteralNumber = node => node && node.type === 'Literal' && typeof node.value === 'number';
  16. const getNumericValue = node => {
  17. if (isLiteralNumber(node)) {
  18. return node.value;
  19. }
  20. if (node.type === 'UnaryExpression' && node.operator === '-') {
  21. return -getNumericValue(node.argument);
  22. }
  23. };
  24. // This handles cases where the argument is very likely to be a number, such as `.substring('foo'.length)`.
  25. const isLengthProperty = node => (
  26. node
  27. && node.type === 'MemberExpression'
  28. && node.computed === false
  29. && node.property.type === 'Identifier'
  30. && node.property.name === 'length'
  31. );
  32. const isLikelyNumeric = node => isLiteralNumber(node) || isLengthProperty(node);
  33. /** @param {import('eslint').Rule.RuleContext} context */
  34. const create = context => {
  35. const sourceCode = context.getSourceCode();
  36. return templates.visitor({
  37. [substrCallTemplate](node) {
  38. const objectNode = substrCallTemplate.context.getMatch(objectVariable);
  39. const argumentNodes = substrCallTemplate.context.getMatch(argumentsVariable);
  40. const problem = {
  41. node,
  42. messageId: MESSAGE_ID_SUBSTR,
  43. };
  44. const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
  45. const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
  46. let sliceArguments;
  47. switch (argumentNodes.length) {
  48. case 0: {
  49. sliceArguments = [];
  50. break;
  51. }
  52. case 1: {
  53. sliceArguments = [firstArgument];
  54. break;
  55. }
  56. case 2: {
  57. if (firstArgument === '0') {
  58. sliceArguments = [firstArgument];
  59. if (isLiteralNumber(secondArgument) || isLengthProperty(argumentNodes[1])) {
  60. sliceArguments.push(secondArgument);
  61. } else if (typeof getNumericValue(argumentNodes[1]) === 'number') {
  62. sliceArguments.push(Math.max(0, getNumericValue(argumentNodes[1])));
  63. } else {
  64. sliceArguments.push(`Math.max(0, ${secondArgument})`);
  65. }
  66. } else if (
  67. isLiteralNumber(argumentNodes[0])
  68. && isLiteralNumber(argumentNodes[1])
  69. ) {
  70. sliceArguments = [
  71. firstArgument,
  72. argumentNodes[0].value + argumentNodes[1].value,
  73. ];
  74. } else if (
  75. isLikelyNumeric(argumentNodes[0])
  76. && isLikelyNumeric(argumentNodes[1])
  77. ) {
  78. sliceArguments = [firstArgument, firstArgument + ' + ' + secondArgument];
  79. }
  80. break;
  81. }
  82. // No default
  83. }
  84. if (sliceArguments) {
  85. const objectText = getParenthesizedText(objectNode, sourceCode);
  86. const optionalMemberSuffix = node.callee.optional ? '?' : '';
  87. const optionalCallSuffix = node.optional ? '?.' : '';
  88. problem.fix = fixer => fixer.replaceText(node, `${objectText}${optionalMemberSuffix}.slice${optionalCallSuffix}(${sliceArguments.join(', ')})`);
  89. }
  90. context.report(problem);
  91. },
  92. [substringCallTemplate](node) {
  93. const objectNode = substringCallTemplate.context.getMatch(objectVariable);
  94. const argumentNodes = substringCallTemplate.context.getMatch(argumentsVariable);
  95. const problem = {
  96. node,
  97. messageId: MESSAGE_ID_SUBSTRING,
  98. };
  99. const firstArgument = argumentNodes[0] ? sourceCode.getText(argumentNodes[0]) : undefined;
  100. const secondArgument = argumentNodes[1] ? sourceCode.getText(argumentNodes[1]) : undefined;
  101. const firstNumber = argumentNodes[0] ? getNumericValue(argumentNodes[0]) : undefined;
  102. let sliceArguments;
  103. switch (argumentNodes.length) {
  104. case 0: {
  105. sliceArguments = [];
  106. break;
  107. }
  108. case 1: {
  109. if (firstNumber !== undefined) {
  110. sliceArguments = [Math.max(0, firstNumber)];
  111. } else if (isLengthProperty(argumentNodes[0])) {
  112. sliceArguments = [firstArgument];
  113. } else {
  114. sliceArguments = [`Math.max(0, ${firstArgument})`];
  115. }
  116. break;
  117. }
  118. case 2: {
  119. const secondNumber = getNumericValue(argumentNodes[1]);
  120. if (firstNumber !== undefined && secondNumber !== undefined) {
  121. sliceArguments = firstNumber > secondNumber
  122. ? [Math.max(0, secondNumber), Math.max(0, firstNumber)]
  123. : [Math.max(0, firstNumber), Math.max(0, secondNumber)];
  124. } else if (firstNumber === 0 || secondNumber === 0) {
  125. sliceArguments = [0, `Math.max(0, ${firstNumber === 0 ? secondArgument : firstArgument})`];
  126. } else {
  127. // As values aren't Literal, we can not know whether secondArgument will become smaller than the first or not, causing an issue:
  128. // .substring(0, 2) and .substring(2, 0) returns the same result
  129. // .slice(0, 2) and .slice(2, 0) doesn't return the same result
  130. // There's also an issue with us now knowing whether the value will be negative or not, due to:
  131. // .substring() treats a negative number the same as it treats a zero.
  132. // The latter issue could be solved by wrapping all dynamic numbers in Math.max(0, <value>), but the resulting code would not be nice
  133. }
  134. break;
  135. }
  136. // No default
  137. }
  138. if (sliceArguments) {
  139. const objectText = getParenthesizedText(objectNode, sourceCode);
  140. const optionalMemberSuffix = node.callee.optional ? '?' : '';
  141. const optionalCallSuffix = node.optional ? '?.' : '';
  142. problem.fix = fixer => fixer.replaceText(node, `${objectText}${optionalMemberSuffix}.slice${optionalCallSuffix}(${sliceArguments.join(', ')})`);
  143. }
  144. context.report(problem);
  145. },
  146. });
  147. };
  148. /** @type {import('eslint').Rule.RuleModule} */
  149. module.exports = {
  150. create,
  151. meta: {
  152. type: 'suggestion',
  153. docs: {
  154. description: 'Prefer `String#slice()` over `String#substr()` and `String#substring()`.',
  155. },
  156. fixable: 'code',
  157. messages,
  158. },
  159. };