prefer-negative-index.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. 'use strict';
  2. const isLiteralValue = require('./utils/is-literal-value.js');
  3. const {
  4. getNegativeIndexLengthNode,
  5. removeLengthNode,
  6. } = require('./shared/negative-index.js');
  7. const typedArray = require('./shared/typed-array.js');
  8. const MESSAGE_ID = 'prefer-negative-index';
  9. const messages = {
  10. [MESSAGE_ID]: 'Prefer negative index over length minus index for `{{method}}`.',
  11. };
  12. const methods = new Map([
  13. [
  14. 'slice',
  15. {
  16. argumentsIndexes: [0, 1],
  17. supportObjects: new Set([
  18. 'Array',
  19. 'String',
  20. 'ArrayBuffer',
  21. ...typedArray,
  22. // `{Blob,File}#slice()` are not generally used
  23. // 'Blob'
  24. // 'File'
  25. ]),
  26. },
  27. ],
  28. [
  29. 'splice',
  30. {
  31. argumentsIndexes: [0],
  32. supportObjects: new Set([
  33. 'Array',
  34. ]),
  35. },
  36. ],
  37. [
  38. 'at',
  39. {
  40. argumentsIndexes: [0],
  41. supportObjects: new Set([
  42. 'Array',
  43. ]),
  44. },
  45. ],
  46. ]);
  47. const getMemberName = node => {
  48. const {type, property} = node;
  49. if (
  50. type === 'MemberExpression'
  51. && property
  52. && property.type === 'Identifier'
  53. ) {
  54. return property.name;
  55. }
  56. };
  57. function parse(node) {
  58. const {callee, arguments: originalArguments} = node;
  59. let method = callee.property.name;
  60. let target = callee.object;
  61. let argumentsNodes = originalArguments;
  62. if (methods.has(method)) {
  63. return {
  64. method,
  65. target,
  66. argumentsNodes,
  67. };
  68. }
  69. if (method !== 'call' && method !== 'apply') {
  70. return;
  71. }
  72. const isApply = method === 'apply';
  73. method = getMemberName(callee.object);
  74. if (!methods.has(method)) {
  75. return;
  76. }
  77. const {supportObjects} = methods.get(method);
  78. const parentCallee = callee.object.object;
  79. if (
  80. // [].{slice,splice}
  81. (
  82. parentCallee.type === 'ArrayExpression'
  83. && parentCallee.elements.length === 0
  84. )
  85. // ''.slice
  86. || (
  87. method === 'slice'
  88. && isLiteralValue(parentCallee, '')
  89. )
  90. // {Array,String...}.prototype.slice
  91. // Array.prototype.splice
  92. || (
  93. getMemberName(parentCallee) === 'prototype'
  94. && parentCallee.object.type === 'Identifier'
  95. && supportObjects.has(parentCallee.object.name)
  96. )
  97. ) {
  98. [target] = originalArguments;
  99. if (isApply) {
  100. const [, secondArgument] = originalArguments;
  101. if (!secondArgument || secondArgument.type !== 'ArrayExpression') {
  102. return;
  103. }
  104. argumentsNodes = secondArgument.elements;
  105. } else {
  106. argumentsNodes = originalArguments.slice(1);
  107. }
  108. return {
  109. method,
  110. target,
  111. argumentsNodes,
  112. };
  113. }
  114. }
  115. /** @param {import('eslint').Rule.RuleContext} context */
  116. const create = context => ({
  117. 'CallExpression[callee.type="MemberExpression"]': node => {
  118. const parsed = parse(node);
  119. if (!parsed) {
  120. return;
  121. }
  122. const {
  123. method,
  124. target,
  125. argumentsNodes,
  126. } = parsed;
  127. const {argumentsIndexes} = methods.get(method);
  128. const removableNodes = argumentsIndexes
  129. .map(index => getNegativeIndexLengthNode(argumentsNodes[index], target))
  130. .filter(Boolean);
  131. if (removableNodes.length === 0) {
  132. return;
  133. }
  134. return {
  135. node,
  136. messageId: MESSAGE_ID,
  137. data: {method},
  138. * fix(fixer) {
  139. const sourceCode = context.getSourceCode();
  140. for (const node of removableNodes) {
  141. yield removeLengthNode(node, fixer, sourceCode);
  142. }
  143. },
  144. };
  145. },
  146. });
  147. /** @type {import('eslint').Rule.RuleModule} */
  148. module.exports = {
  149. create,
  150. meta: {
  151. type: 'suggestion',
  152. docs: {
  153. description: 'Prefer negative index over `.length - index` for `{String,Array,TypedArray}#slice()`, `Array#splice()` and `Array#at()`.',
  154. },
  155. fixable: 'code',
  156. messages,
  157. },
  158. };