Repository: ember-cli/eslint-plugin-ember
Pull request #1633 (2022-10-14 12:21:05)

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

tgvrssanthosh: @bmish 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 5dc4f2d0..01bdff79 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -158,6 +158,8 @@ function variableNameToWords(name) {
  * @param {Object} context The ESlint context object which contains some helper utils
  * @param {Object} [options] An object that contains additional information
  * @param {String} [options.importedGetName] The name of the imported get specifier from @ember/object package
+ * @param {String} [options.importedSetName] The name of the imported set specifier from @ember/object package
+ * @param {String} [options.importedCompareName] The name of the imported compare specifier from @ember/utils package
  * @returns {Object|[]}
  */
 // eslint-disable-next-line complexity
@@ -255,6 +257,35 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
 
       return fixes;
     }
+    case 'invoke': {
+      const fixes = [];
+
+      if (callArgs.length > 0) {
+        const argText = sourceCode.getText(callArgs[0]);
+        const restOfArgs = callArgs
+          .slice(1)
+          .map((arg) => sourceCode.getText(arg))
+          .join(', ');
+
+        // default to `get` if the `get` hasn't already been imported.
+        const importedGetName = options.importedGetName ?? 'get';
+
+        fixes.push(
+          fixer.replaceText(calleeProp, 'map'),
+          // Replacing the content starting from open parenthesis to close parenthesis
+          fixer.replaceTextRange(
+            [openParenToken.range[0], closeParenToken.range[1]],
+            `(item => ${importedGetName}(item, ${argText})?.(${restOfArgs}))`
+          )
+        );
+
+        // Add `get` import statement only if it is not imported already
+        if (!options.importedGetName) {
+          fixes.push(insertImportDeclaration(sourceCode, fixer, '@ember/object', importedGetName));
+        }
+      }
+      return fixes;
+    }
     case 'mapBy': {
       if (callArgs.length === 1) {
         const argText = sourceCode.getText(callArgs[0]);
@@ -355,6 +386,34 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
       }
       return fixes;
     }
+    case 'setEach': {
+      const fixes = [];
+
+      if (callArgs.length === 2) {
+        // default to `set` if the `set` hasn't already been imported.
+        const importedSetName = options.importedSetName ?? 'set';
+
+        fixes.push(
+          fixer.replaceText(calleeProp, 'forEach'),
+          // Replacing the content starting from open parenthesis to close parenthesis
+          fixer.replaceTextRange(
+            [openParenToken.range[0], closeParenToken.range[1]],
+            `(item => ${importedSetName}(item, ${callArgs
+              .map((arg) => sourceCode.getText(arg))
+              .join(', ')}))`
+          )
+        );
+
+        // Add `set` import statement only if it is not imported already
+        if (!options.importedSetName) {
+          fixes.push(insertImportDeclaration(sourceCode, fixer, '@ember/object', importedSetName));
+        }
+
+        return fixes;
+      }
+
+      return [];
+    }
     case 'sortBy': {
       const fixes = [];
 
@@ -539,6 +598,7 @@ module.exports = {
     const sourceCode = context.getSourceCode();
     const { scopeManager } = sourceCode;
     let importedGetName;
+    let importedSetName;
     let importedCompareName;
 
     // Track some information about the current class we're inside.
@@ -548,6 +608,7 @@ module.exports = {
       ImportDeclaration(node) {
         if (node.source.value === '@ember/object') {
           importedGetName = importedGetName || getImportIdentifier(node, '@ember/object', 'get');
+          importedSetName = importedSetName || getImportIdentifier(node, '@ember/object', 'set');
         }
         if (node.source.value === '@ember/utils') {
           importedCompareName =
@@ -640,6 +701,7 @@ module.exports = {
               return applyFix(node, fixer, context, {
                 importedCompareName,
                 importedGetName,
+                importedSetName,
               });
             },
           });


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index 85c5525e..c9eb648a 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -466,10 +466,48 @@ import { get as g } from 'dummy';
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
+      // When unexpected number of arguments are passed, auto-fixer will not run
       code: 'something.invoke()',
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      code: 'something.invoke("abc")',
+      output: `import { get } from '@ember/object';
+something.map(item => get(item, "abc")?.())`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: 'something.invoke("abc", "def")',
+      output: `import { get } from '@ember/object';
+something.map(item => get(item, "abc")?.("def"))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When `get` method is already imported from '@ember/object' package
+      code: `import { get } from '@ember/object';
+      something.invoke('abc', {})`,
+      output: `import { get } from '@ember/object';
+      something.map(item => get(item, 'abc')?.({}))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When `get` method is already imported with alias from '@ember/object' package
+      code: `import { get as g } from '@ember/object';
+      something.invoke('abc', {})`,
+      output: `import { get as g } from '@ember/object';
+      something.map(item => g(item, 'abc')?.({}))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When `get` method is already imported from a package other than '@ember/object'
+      code: `import { get as g } from '@custom/object';
+      something.invoke('abc', {}, 'test', true)`,
+      output: `import { get } from '@ember/object';
+import { get as g } from '@custom/object';
+      something.map(item => get(item, 'abc')?.({}, 'test', true))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       // When unexpected number of arguments are passed, auto-fixer will not run
       code: 'something.isAny()',
@@ -697,10 +735,48 @@ import { get as g } from '@custom/object';
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
+      // When unexpected number of arguments are passed, auto-fixer will not run
       code: 'something.setEach()',
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      // When unexpected number of arguments are passed, auto-fixer will not run
+      code: 'something.setEach("abc")',
+      output: null,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: 'something.setEach("abc", 2)',
+      output: `import { set } from '@ember/object';
+something.forEach(item => set(item, "abc", 2))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When get method is already imported from @ember/object package.
+      code: `import { set } from '@ember/object';
+      something.setEach("abc", 2)`,
+      output: `import { set } from '@ember/object';
+      something.forEach(item => set(item, "abc", 2))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When get method is already imported as alias from @ember/object package.
+      code: `import { set as s } from '@ember/object';
+      something.setEach("abc", 2)`,
+      output: `import { set as s } from '@ember/object';
+      something.forEach(item => s(item, "abc", 2))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When get method is already imported from a package other than @ember/object.
+      code: `import { set as s } from '@custom/object';
+      something.setEach("abc", 2)`,
+      output: `import { set } from '@ember/object';
+import { set as s } from '@custom/object';
+      something.forEach(item => set(item, "abc", 2))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       // When unexpected number of arguments are passed, auto-fixer will not run
       code: 'something.sortBy()',