Repository: ember-cli/eslint-plugin-ember
Pull request #1619 (2022-10-07 17:39:38)

Add `mapBy` to `map` autofixer for `no-array-prototype-extensions` rule
## Summary
Added auto-fixer for `no-array-prototype-extensions` which replaces `mapBy` with `map`.

## 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 `mapBy` with the native array method `map`.

PS: Added `/* eslint complexity: ["error", 50]*/` as the applyFix function has crossed the default threshold of 20.

## Testing
Modified test case to check if the right output is generated after the fix.



Patch:
diff --git a/lib/rules/no-array-prototype-extensions.js b/lib/rules/no-array-prototype-extensions.js
index a056e317..be50c9d9 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -160,6 +160,7 @@ function variableNameToWords(name) {
  * @param {String} [options.importedGetName] The name of the imported get specifier from @ember/object package
  * @returns {Object|[]}
  */
+// eslint-disable-next-line complexity
 function applyFix(callExpressionNode, fixer, context, options = {}) {
   const calleeProp = callExpressionNode.callee.property;
   const propertyName = calleeProp.name;
@@ -167,6 +168,19 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
   const callArgs = callExpressionNode.arguments;
   const sourceCode = context.getSourceCode();
 
+  // 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 === ')';
+    },
+  });
+
   switch (propertyName) {
     case 'any': {
       return fixer.replaceText(calleeProp, 'some');
@@ -174,19 +188,6 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
     case 'compact': {
       const fixes = [];
 
-      // 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 (openParenToken && closeParenToken && callArgs.length === 0) {
         fixes.push(
           fixer.replaceText(calleeProp, 'filter'),
@@ -211,19 +212,6 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
         // default to `get` if the `get` hasn't already been imported.
         const importedGetName = options.importedGetName ?? 'get';
 
-        // 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 === ')';
-          },
-        });
-
         fixes.push(
           fixer.replaceText(calleeProp, propertyName === 'findBy' ? 'find' : 'filter'),
           // Replacing the content starting from open parenthesis to close parenthesis
@@ -245,6 +233,32 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
 
       return fixes;
     }
+    case 'mapBy': {
+      if (callArgs.length === 1) {
+        const argText = sourceCode.getText(callArgs[0]);
+        const fixes = [];
+
+        // 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}))`
+          )
+        );
+
+        // Add `get` import statement only if it is not imported already
+        if (!options.importedGetName) {
+          fixes.push(insertImportDeclaration(sourceCode, fixer, '@ember/object', importedGetName));
+        }
+
+        return fixes;
+      }
+      return [];
+    }
     case 'objectAt': {
       if (callArgs.length === 1) {
         return [
@@ -269,19 +283,6 @@ 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);
@@ -294,12 +295,13 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
           // 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}`),
+          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})`
           ),
+          fixer.insertTextBefore(callExpressionNode, '('),
         ];
       }
 


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index 7f5937ce..3ae2e582 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -481,10 +481,46 @@ 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.mapBy()',
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      // When unexpected number of arguments are passed, auto-fixer will not run
+      code: 'something.mapBy(1, 2)',
+      output: null,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: "something.mapBy('abc')",
+      output: `import { get } from '@ember/object';
+something.map(item => get(item, 'abc'))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      code: 'something.mapBy(def)',
+      output: `import { get } from '@ember/object';
+something.map(item => get(item, def))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // @ember/object's `get` is already imported
+      code: `import { get } from '@ember/object';
+      something.mapBy('abc')`,
+      output: `import { get } from '@ember/object';
+      something.map(item => get(item, 'abc'))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // `get` is imported from package other than @ember/object
+      code: `import { get as g } from 'dummy';
+      something.mapBy('abc')`,
+      output: `import { get } from '@ember/object';
+import { get as g } from 'dummy';
+      something.map(item => get(item, 'abc'))`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       // When unexpected number of arguments are passed, auto-fixer will not run
       code: 'something.objectAt()',
@@ -503,6 +539,7 @@ import { get as g } from 'dummy';
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
+      // Function call in chain
       code: 'something.somethingElse.objectAt(1)',
       output: 'something.somethingElse[1]',
       errors: [{ messageId: 'main', type: 'CallExpression' }],
@@ -572,13 +609,13 @@ import { get as g } from 'dummy';
     },
     {
       code: 'something.without(1)',
-      output: 'something.indexOf(1) > -1 ? something.filter(item => item !== 1) : something',
+      output: '(something.indexOf(1) > -1 ? something.filter(item => item !== 1) : something)',
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
-      code: 'something.somethingElse.without(1)',
+      code: "something.somethingElse.without('abc')",
       output:
-        'something.somethingElse.indexOf(1) > -1 ? something.somethingElse.filter(item => item !== 1) : something.somethingElse',
+        "(something.somethingElse.indexOf('abc') > -1 ? something.somethingElse.filter(item => item !== 'abc') : something.somethingElse)",
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {