Repository: ember-cli/eslint-plugin-ember
Pull request #1626 (2022-10-10 16:57:26)

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

tgvrssanthosh: @bmish Can you help review this PR?
tgvrssanthosh: @bmish Addressed your comments. Please take a relook. Thank you!

Patch:
diff --git a/lib/rules/no-array-prototype-extensions.js b/lib/rules/no-array-prototype-extensions.js
index 3565428c..1f70e915 100644
--- a/lib/rules/no-array-prototype-extensions.js
+++ b/lib/rules/no-array-prototype-extensions.js
@@ -335,6 +335,71 @@ function applyFix(callExpressionNode, fixer, context, options = {}) {
 
       return [];
     }
+    case 'uniqBy': {
+      const fixes = [];
+
+      if (callArgs.length === 1) {
+        const argInString = sourceCode.getText(callArgs[0]);
+        // default to `get` if the `get` hasn't already been imported.
+        const importedGetName = options.importedGetName ?? 'get';
+
+        let isFunctionArg;
+        let isLiteralArg;
+
+        switch (callArgs[0].type) {
+          case 'ArrowFunctionExpression':
+          case 'FunctionExpression': {
+            isFunctionArg = true;
+            break;
+          }
+          case 'Literal': {
+            isLiteralArg = true;
+            break;
+          }
+          default: {
+            break;
+          }
+        }
+
+        const uniqByFn = `([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }`;
+
+        let getterFnInText;
+        if (isLiteralArg) {
+          getterFnInText = `(item) => ${importedGetName}(item, ${argInString})`;
+        } else if (isFunctionArg) {
+          getterFnInText = argInString;
+        } else {
+          getterFnInText = `typeof ${argInString} === 'function' ? ${argInString} : (item) => ${importedGetName}(item, ${argInString})`;
+        }
+
+        const reducerInitialValueArr = ['[]', 'new Set()', getterFnInText];
+
+        fixes.push(
+          fixer.replaceText(calleeProp, 'reduce'),
+          // Replacing the content starting from open parenthesis to close parenthesis
+          fixer.replaceTextRange(
+            [openParenToken.range[0], closeParenToken.range[1]],
+            `(${uniqByFn}, [${reducerInitialValueArr.join(', ')}])[0]`
+          )
+        );
+
+        // 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 'without': {
       if (callArgs.length === 1) {
         const argText = sourceCode.getText(callArgs[0]);


Test patch:
diff --git a/tests/lib/rules/no-array-prototype-extensions.js b/tests/lib/rules/no-array-prototype-extensions.js
index b9415ec4..d74c0831 100644
--- a/tests/lib/rules/no-array-prototype-extensions.js
+++ b/tests/lib/rules/no-array-prototype-extensions.js
@@ -740,10 +740,132 @@ import { compare as comp } from '@custom/utils';
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
     {
+      // When unexpected number of params are passed, we will skip auto-fixing
       code: 'something.uniqBy()',
       output: null,
       errors: [{ messageId: 'main', type: 'CallExpression' }],
     },
+    {
+      // Primitive value as an argument
+      code: 'something.uniqBy(1)',
+      output: `import { get } from '@ember/object';
+something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), (item) => get(item, 1)])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // Dynamic value as an argument
+      code: 'something.uniqBy(abc)',
+      output: `import { get } from '@ember/object';
+something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), typeof abc === 'function' ? abc : (item) => get(item, abc)])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // get method is already imported from @ember/object package.
+      code: `import { get } from '@ember/object';
+      something.uniqBy('abc').sort()`,
+      output: `import { get } from '@ember/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), (item) => get(item, 'abc')])[0].sort()`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // get method is already imported from a package other than @ember/object.
+      code: `import { get as g } from '@custom/object';
+      something.uniqBy('abc').sort()`,
+      output: `import { get } from '@ember/object';
+import { get as g } from '@custom/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), (item) => get(item, 'abc')])[0].sort()`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // get method is already imported with an alias from @ember/object package.
+      code: `import { get as g } from '@ember/object';
+      something.uniqBy('abc')`,
+      output: `import { get as g } from '@ember/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), (item) => g(item, 'abc')])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When arrow function expression is passed as argument
+      code: `import { get as g } from '@ember/object';
+      something.uniqBy(() => true)`,
+      output: `import { get as g } from '@ember/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), () => true])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When function expression is passed as argument
+      code: `import { get as g } from '@ember/object';
+      something.uniqBy(function test() { return true; })`,
+      output: `import { get as g } from '@ember/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), function test() { return true; }])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
+    {
+      // When function expression is passed as argument
+      code: `function test() { return true; }
+      import { get as g } from '@ember/object';
+      something.uniqBy(test)`,
+      output: `function test() { return true; }
+      import { get as g } from '@ember/object';
+      something.reduce(([uniqArr, itemsSet, getterFn], item) => {
+          const val = getterFn(item);
+          if (!itemsSet.has(val)) {
+            itemsSet.add(val);
+            uniqArr.push(item);
+          }
+          return [uniqArr, itemsSet, getterFn];
+        }, [[], new Set(), typeof test === 'function' ? test : (item) => g(item, test)])[0]`,
+      errors: [{ messageId: 'main', type: 'CallExpression' }],
+    },
     {
       // When unexpected number of params are passed, we will skip auto-fixing
       code: 'something.without()',