Repository: ember-cli/eslint-plugin-ember
Pull request #1610 (2022-10-06 18:27:33)

Add `filterBy` to `filter` autofixer for `no-array-prototype-extensions` rule
## Summary
Added auto-fixer for `no-array-prototype-extensions` which replaces `filterBy` 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 `filterBy` with the native array method `filter`.

## Testing
Modified test case to check if the right output is generated after the fix.


tgvrssanthosh: @bmish Added autofixer for `filterBy`. This PR also contains the other PR changes (#1609 ) as well. Once it goes in, i can rebase the changes.

Patch:
diff --git a/lib/rules/no-array-prototype-extensions.js b/lib/rules/no-array-prototype-extensions.js
index b3349ac3..518fbb0a 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -2,6 +2,7 @@
 
 const { getName, getNodeOrNodeFromVariable } = require('../utils/utils');
 const { isClassPropertyOrPropertyDefinition } = require('../utils/types');
+const { getImportIdentifier } = require('../utils/import');
 const Stack = require('../utils/stack');
 
 const ERROR_MESSAGE = "Don't use Ember's array prototype extensions";
@@ -150,14 +151,59 @@ function variableNameToWords(name) {
  *
  * @param {Object} callExpressionNode The call expression AST node.
  * @param {Object} fixer The ESLint fixer object which will be used to apply fixes.
+ * @param {Object} context The ESlint context object which contains some helper utils
  * @returns {Object|[]}
  */
-function applyFix(callExpressionNode, fixer) {
+function applyFix(callExpressionNode, fixer, context, options = {}) {
   const propertyName = callExpressionNode.callee.property.name;
 
   switch (propertyName) {
     case 'any':
       return fixer.replaceText(callExpressionNode.callee.property, 'some');
+    case 'filterBy': {
+      const callArgs = callExpressionNode.arguments;
+      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 fixes = [
+        fixer.replaceText(callExpressionNode.callee.property, 'filter'),
+        // Replace the first argument with the necessary condition
+        // If the filterBy contains two arguments, the property (first argument) value will be compared against the second argument
+        // If the filterBy contains only one argument, the property's truthy value is used to filter
+        fixer.replaceText(
+          firstArg,
+          hasSecondArg
+            ? `item => ${importedGetName}(item, ${sourceCode.getText(
+                firstArg
+              )}) === ${sourceCode.getText(secondArg)}`
+            : `item => ${importedGetName}(item, ${sourceCode.getText(firstArg)})`
+        ),
+      ];
+
+      // Add `get` import statement only if it is not imported already
+      if (!options.importedGetName) {
+        fixes.push(
+          fixer.insertTextBefore(
+            sourceCode.ast,
+            `import { ${importedGetName} } from '@ember/object';\n`
+          )
+        );
+      }
+
+      if (hasSecondArg) {
+        fixes.push(
+          // Required to get rid of `, ` followed by the first argument since the second argument will be removed
+          fixer.removeRange([firstArg.range[1], secondArg.range[0]]),
+          fixer.remove(secondArg)
+        );
+      }
+
+      return fixes;
+    }
     default:
       return [];
   }
@@ -187,11 +233,17 @@ module.exports = {
   create(context) {
     const sourceCode = context.getSourceCode();
     const { scopeManager } = sourceCode;
+    let importedGetName;
 
     // Track some information about the current class we're inside.
     const classStack = new Stack();
 
     return {
+      ImportDeclaration(node) {
+        if (node.source.value === '@ember/object') {
+          importedGetName = importedGetName || getImportIdentifier(node, '@ember/object', 'get');
+        }
+      },
       /**
        * Cover cases when `EXTENSION_METHODS` is getting called.
        * Example: something.filterBy();
@@ -275,7 +327,9 @@ module.exports = {
             node,
             messageId: 'main',
             fix(fixer) {
-              return applyFix(node, fixer);
+              return applyFix(node, fixer, context, {
+                importedGetName,
+              });
             },
           });
         }


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index 3f9247c8..6996848f 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -233,10 +233,82 @@ ruleTester.run('no-array-prototype-extensions', rule, {
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      // filterBy with two arguments
+      code: `
+      const arr = [];
+
+      function getAge() {
+        return 16;
+      }
+
+      arr.filterBy("age", getAge());
+      `,
+      output: `
+      import { get } from '@ember/object';
+const arr = [];
+
+      function getAge() {
+        return 16;
+      }
+
+      arr.filter(item => get(item, "age") === getAge());
+      `,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // filterBy with one argument
+      code: `
+      const arr = [];
+
+      arr.filterBy("age");
+      `,
+      output: `
+      import { get } from '@ember/object';
+const arr = [];
+
+      arr.filter(item => get(item, "age"));
+      `,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // filterBy with one argument and `get` import statement already imported
+      code: `
+      import { get as g } from '@ember/object';
+      const arr = [];
+
+      arr.filterBy("age");
+      `,
+      output: `
+      import { get as g } from '@ember/object';
+      const arr = [];
+
+      arr.filter(item => g(item, "age"));
+      `,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // filterBy with one argument and `get` import statement imported from a different package
+      code: `
+      import { get as g } from 'dummy';
+      const arr = [];
+
+      arr.filterBy("age");
+      `,
+      output: `
+      import { get } from '@ember/object';
+import { get as g } from 'dummy';
+      const arr = [];
+
+      arr.filter(item => get(item, "age"));
+      `,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       // Set in variable name but not a Set function.
-      code: 'set.filterBy()',
-      output: null,
+      code: 'set.filterBy("age", 18);',
+      output: `import { get } from '@ember/object';
+set.filter(item => get(item, "age") === 18);`,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {