prefer-negative-index.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. 'use strict';
  2. const {
  3. getNegativeIndexLengthNode,
  4. removeLengthNode,
  5. } = require('./shared/negative-index.js');
  6. const typedArray = require('./shared/typed-array.js');
  7. const {isLiteral} = require('./ast/index.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. 'String',
  44. ...typedArray,
  45. ]),
  46. },
  47. ],
  48. ]);
  49. const getMemberName = node => {
  50. const {type, property} = node;
  51. if (
  52. type === 'MemberExpression'
  53. && property.type === 'Identifier'
  54. ) {
  55. return property.name;
  56. }
  57. };
  58. function parse(node) {
  59. const {callee, arguments: originalArguments} = node;
  60. let method = callee.property.name;
  61. let target = callee.object;
  62. let argumentsNodes = originalArguments;
  63. if (methods.has(method)) {
  64. return {
  65. method,
  66. target,
  67. argumentsNodes,
  68. };
  69. }
  70. if (method !== 'call' && method !== 'apply') {
  71. return;
  72. }
  73. const isApply = method === 'apply';
  74. method = getMemberName(callee.object);
  75. if (!methods.has(method)) {
  76. return;
  77. }
  78. const {supportObjects} = methods.get(method);
  79. const parentCallee = callee.object.object;
  80. if (
  81. // [].{slice,splice}
  82. (
  83. parentCallee.type === 'ArrayExpression'
  84. && parentCallee.elements.length === 0
  85. )
  86. // ''.slice
  87. || (
  88. method === 'slice'
  89. && isLiteral(parentCallee, '')
  90. )
  91. // {Array,String...}.prototype.slice
  92. // Array.prototype.splice
  93. || (
  94. getMemberName(parentCallee) === 'prototype'
  95. && parentCallee.object.type === 'Identifier'
  96. && supportObjects.has(parentCallee.object.name)
  97. )
  98. ) {
  99. [target] = originalArguments;
  100. if (isApply) {
  101. const [, secondArgument] = originalArguments;
  102. if (!secondArgument || secondArgument.type !== 'ArrayExpression') {
  103. return;
  104. }
  105. argumentsNodes = secondArgument.elements;
  106. } else {
  107. argumentsNodes = originalArguments.slice(1);
  108. }
  109. return {
  110. method,
  111. target,
  112. argumentsNodes,
  113. };
  114. }
  115. }
  116. /** @param {import('eslint').Rule.RuleContext} context */
  117. const create = context => ({
  118. 'CallExpression[callee.type="MemberExpression"]'(node) {
  119. const parsed = parse(node);
  120. if (!parsed) {
  121. return;
  122. }
  123. const {
  124. method,
  125. target,
  126. argumentsNodes,
  127. } = parsed;
  128. const {argumentsIndexes} = methods.get(method);
  129. const removableNodes = argumentsIndexes
  130. .map(index => getNegativeIndexLengthNode(argumentsNodes[index], target))
  131. .filter(Boolean);
  132. if (removableNodes.length === 0) {
  133. return;
  134. }
  135. return {
  136. node,
  137. messageId: MESSAGE_ID,
  138. data: {method},
  139. * fix(fixer) {
  140. const sourceCode = context.getSourceCode();
  141. for (const node of removableNodes) {
  142. yield removeLengthNode(node, fixer, sourceCode);
  143. }
  144. },
  145. };
  146. },
  147. });
  148. /** @type {import('eslint').Rule.RuleModule} */
  149. module.exports = {
  150. create,
  151. meta: {
  152. type: 'suggestion',
  153. docs: {
  154. description: 'Prefer negative index over `.length - index` for `{String,Array,TypedArray}#{slice,at}()` and `Array#splice()`.',
  155. },
  156. fixable: 'code',
  157. messages,
  158. },
  159. };