Repository: ember-cli/eslint-plugin-ember
Pull request #1617 (2022-10-07 16:37:46)

Add `without` to `filter` autofixer for `no-array-prototype-extensions` rule
## Summary
Added auto-fixer for `no-array-prototype-extensions` which replaces `without` with `filter`.

## Description
Recently, the proposal to deprecate array prototype extensions has been approved and got merged. The plan is to add autoFixers to replace ember array prototype extensions with the native array methods. In this PR, an auto fixer has been added which will replace the ember array method `without` with the native array method `filter`.

## Testing
Modified test case to check if the right output is generated after the fix.


tgvrssanthosh: @bmish Can you please help review this PR as well? Thank you!

Patch:
diff --git a/lib/rules/no-array-prototype-extensions.js b/lib/rules/no-array-prototype-extensions.js
index f9b19764..a056e317 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -161,20 +161,21 @@ function variableNameToWords(name) {
  * @returns {Object|[]}
  */
 function applyFix(callExpressionNode, fixer, context, options = {}) {
-  const propertyName = callExpressionNode.callee.property.name;
+  const calleeProp = callExpressionNode.callee.property;
+  const propertyName = calleeProp.name;
+  const calleeObj = callExpressionNode.callee.object;
+  const callArgs = callExpressionNode.arguments;
+  const sourceCode = context.getSourceCode();
 
   switch (propertyName) {
     case 'any': {
-      return fixer.replaceText(callExpressionNode.callee.property, 'some');
+      return fixer.replaceText(calleeProp, 'some');
     }
     case 'compact': {
-      const calleePropertyNode = callExpressionNode.callee.property;
-      const sourceCode = context.getSourceCode();
-      const callArgs = callExpressionNode.arguments;
       const fixes = [];
 
       // Get the open parenthesis immediately after the callee name
-      const openParenToken = sourceCode.getTokenAfter(calleePropertyNode, {
+      const openParenToken = sourceCode.getTokenAfter(calleeProp, {
         filter(token) {
           return token.type === TOKEN_TYPES.PUNCTUATOR && token.value === '(';
         },
@@ -188,7 +189,7 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
 
       if (openParenToken && closeParenToken && callArgs.length === 0) {
         fixes.push(
-          fixer.replaceText(calleePropertyNode, 'filter'),
+          fixer.replaceText(calleeProp, 'filter'),
           // Replacing the content starting from open parenthesis to close parenthesis
           fixer.replaceTextRange(
             [openParenToken.range[0], closeParenToken.range[1]],
@@ -201,17 +202,14 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
     case 'filterBy':
     case 'findBy': {
       const fixes = [];
-      const callArgs = callExpressionNode.arguments;
 
       if (callArgs.length > 0 && callArgs.length < 3) {
-        const sourceCode = context.getSourceCode();
         const hasSecondArg = callArgs.length > 1;
         const firstArg = callArgs[0];
         const secondArg = callArgs[1];
 
         // default to `get` if the `get` hasn't already been imported.
         const importedGetName = options.importedGetName ?? 'get';
-        const calleeProp = callExpressionNode.callee.property;
 
         // Get the open parenthesis immediately after the callee name
         const openParenToken = sourceCode.getTokenAfter(calleeProp, {
@@ -248,10 +246,6 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
       return fixes;
     }
     case 'objectAt': {
-      const calleeObj = callExpressionNode.callee.object;
-      const callArgs = callExpressionNode.arguments;
-      const sourceCode = context.getSourceCode();
-
       if (callArgs.length === 1) {
         return [
           fixer.insertTextBefore(
@@ -265,10 +259,6 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
       return [];
     }
     case 'toArray': {
-      const calleeObj = callExpressionNode.callee.object;
-      const callArgs = callExpressionNode.arguments;
-      const sourceCode = context.getSourceCode();
-
       if (callArgs.length === 0) {
         return [
           fixer.insertTextBefore(callExpressionNode, `[...${sourceCode.getText(calleeObj)}]`),
@@ -278,6 +268,43 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
 
       return [];
     }
+    case 'without': {
+      // Get the open parenthesis immediately after the callee name
+      const openParenToken = sourceCode.getTokenAfter(calleeProp, {
+        filter(token) {
+          return token.type === TOKEN_TYPES.PUNCTUATOR && token.value === '(';
+        },
+      });
+      // Get the close parenthesis from the end of the callExpressionNode
+      const closeParenToken = sourceCode.getLastToken(callExpressionNode, {
+        filter(token) {
+          return token.type === TOKEN_TYPES.PUNCTUATOR && token.value === ')';
+        },
+      });
+
+      if (callArgs.length === 1) {
+        const argText = sourceCode.getText(callArgs[0]);
+        const calleeObjText = sourceCode.getText(calleeObj);
+
+        return [
+          // As per https://api.emberjs.com/ember/release/classes/EmberArray/methods/mapBy?anchor=without
+          // when the passed value doesn't not exist in the array, it returns the original array
+          // Hence we first check for the existence of the passed value in the array before calling filter on array
+          // Used indexOf instead of includes since v3.x of ember is committed to supporting IE11
+          // as per the ember browser support policy (https://emberjs.com/browser-support/)
+          // TODO: Switch to includes once Ember v3 LTS support ends: https://emberjs.com/releases/lts/
+          fixer.replaceText(calleeProp, `indexOf(${argText}) > -1 ? ${calleeObjText}.filter`),
+          fixer.insertTextAfter(closeParenToken, ` : ${calleeObjText}`),
+          // Replacing the content starting from open parenthesis to close parenthesis
+          fixer.replaceTextRange(
+            [openParenToken.range[0], closeParenToken.range[1]],
+            `(item => item !== ${argText})`
+          ),
+        ];
+      }
+
+      return [];
+    }
     default: {
       return [];
     }


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index 6acfa612..7f5937ce 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -559,10 +559,28 @@ import { get as g } from 'dummy';
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
+      // When unexpected number of params are passed, we will skip auto-fixing
       code: 'something.without()',
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      // When unexpected number of params are passed, we will skip auto-fixing
+      code: 'something.without(1, 2)',
+      output: null,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: 'something.without(1)',
+      output: 'something.indexOf(1) > -1 ? something.filter(item => item !== 1) : something',
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: 'something.somethingElse.without(1)',
+      output:
+        'something.somethingElse.indexOf(1) > -1 ? something.somethingElse.filter(item => item !== 1) : something.somethingElse',
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       code: 'something.lastObject',
       output: null,