Repository: ember-cli/eslint-plugin-ember
Pull request #773 (2020-04-13 03:29:52)

Add autofixer to `no-duplicate-dependent-keys` rule


Patch:
diff --git a/README.md b/README.md
index e87a72a8..973d9654 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | :white_check_mark: | [no-arrow-function-computed-properties](./docs/rules/no-arrow-function-computed-properties.md) | disallow arrow functions in computed properties |
 | :car: | [no-computed-properties-in-native-classes](./docs/rules/no-computed-properties-in-native-classes.md) | disallow using computed properties in native classes |
 | :white_check_mark: | [no-deeply-nested-dependent-keys-with-each](./docs/rules/no-deeply-nested-dependent-keys-with-each.md) | disallow usage of deeply-nested computed property dependent keys with `@each` |
-| :white_check_mark: | [no-duplicate-dependent-keys](./docs/rules/no-duplicate-dependent-keys.md) | disallow repeating computed property dependent keys |
+| :white_check_mark::wrench: | [no-duplicate-dependent-keys](./docs/rules/no-duplicate-dependent-keys.md) | disallow repeating computed property dependent keys |
 | :white_check_mark::wrench: | [no-incorrect-computed-macros](./docs/rules/no-incorrect-computed-macros.md) | disallow incorrect usage of computed property macros |
 | :white_check_mark::wrench: | [no-invalid-dependent-keys](./docs/rules/no-invalid-dependent-keys.md) | disallow invalid dependent keys in computed properties |
 | :white_check_mark: | [no-side-effects](./docs/rules/no-side-effects.md) | disallow unexpected side effects in computed properties |
diff --git a/docs/rules/no-duplicate-dependent-keys.md b/docs/rules/no-duplicate-dependent-keys.md
index d2292622..534db9ec 100644
--- a/docs/rules/no-duplicate-dependent-keys.md
+++ b/docs/rules/no-duplicate-dependent-keys.md
@@ -2,6 +2,8 @@
 
 :white_check_mark: The `"extends": "plugin:ember/recommended"` property in a configuration file enables this rule.
 
+:wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
 Disallow repeating dependent keys.
 
 ## Rule Details
diff --git a/lib/rules/no-duplicate-dependent-keys.js b/lib/rules/no-duplicate-dependent-keys.js
index e8650bbf..29d9a5a1 100644
--- a/lib/rules/no-duplicate-dependent-keys.js
+++ b/lib/rules/no-duplicate-dependent-keys.js
@@ -1,6 +1,9 @@
 'use strict';
 
 const emberUtils = require('../utils/ember');
+const types = require('../utils/types');
+const fixerUtils = require('../utils/fixer');
+const utils = require('../utils/utils');
 
 const ERROR_MESSAGE = 'Dependent keys should not be repeated';
 //------------------------------------------------------------------------------
@@ -17,7 +20,7 @@ module.exports = {
       url:
         'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-duplicate-dependent-keys.md',
     },
-    fixable: null,
+    fixable: 'code',
     schema: [],
   },
 
@@ -27,9 +30,43 @@ module.exports = {
     return {
       CallExpression(node) {
         if (emberUtils.hasDuplicateDependentKeys(node)) {
-          context.report(node, ERROR_MESSAGE);
+          context.report({
+            node,
+            message: ERROR_MESSAGE,
+            fix(fixer) {
+              const sourceCode = context.getSourceCode();
+
+              const stringNodes = node.arguments.filter((arg) => types.isStringLiteral(arg));
+              const duplicateNodes = findDuplicateStringNodes(stringNodes);
+
+              if (duplicateNodes.length === 0) {
+                return null;
+              }
+
+              return utils.flatten(
+                duplicateNodes.map((duplicateNode) =>
+                  fixerUtils.removeCommaSeparatedNode(duplicateNode, sourceCode, fixer)
+                )
+              );
+            },
+          });
         }
       },
     };
   },
 };
+
+function findDuplicateStringNodes(stringNodes) {
+  const seenNodes = new Set();
+  const duplicateNodes = [];
+
+  stringNodes.forEach((node) => {
+    if (seenNodes.has(node.value)) {
+      duplicateNodes.push(node);
+    } else {
+      seenNodes.add(node.value);
+    }
+  });
+
+  return duplicateNodes;
+}
diff --git a/lib/rules/no-unnecessary-route-path-option.js b/lib/rules/no-unnecessary-route-path-option.js
index fc7568d4..2f83793b 100644
--- a/lib/rules/no-unnecessary-route-path-option.js
+++ b/lib/rules/no-unnecessary-route-path-option.js
@@ -2,6 +2,7 @@
 
 const types = require('../utils/types');
 const ember = require('../utils/ember');
+const fixerUtils = require('../utils/fixer');
 
 //------------------------------------------------------------------------------
 // Rule Definition
@@ -54,7 +55,7 @@ module.exports = {
               const shouldRemoveObject = node.arguments[1].properties.length === 1;
               const nodeToRemove = shouldRemoveObject ? node.arguments[1] : pathOptionNode;
 
-              return removeCommaSeparatedNode(nodeToRemove, sourceCode, fixer);
+              return fixerUtils.removeCommaSeparatedNode(nodeToRemove, sourceCode, fixer);
             },
           });
         }
@@ -79,19 +80,3 @@ function pathMatchesRouteName(path, routeName) {
   const pathWithoutOptionalLeadingSlash = path.slice(0, 1) === '/' ? path.slice(1) : path;
   return pathWithoutOptionalLeadingSlash === routeName;
 }
-
-function removeCommaSeparatedNode(node, sourceCode, fixer) {
-  const tokenBefore = sourceCode.getTokenBefore(node);
-  const tokenAfter = sourceCode.getTokenAfter(node);
-
-  const removeComma = isCommaToken(tokenAfter)
-    ? fixer.remove(tokenAfter)
-    : fixer.remove(tokenBefore);
-  const removeNode = fixer.remove(node);
-
-  return [removeComma, removeNode];
-}
-
-function isCommaToken(token) {
-  return token.value === ',' && token.type === 'Punctuator';
-}
diff --git a/lib/utils/fixer.js b/lib/utils/fixer.js
new file mode 100644
index 00000000..88b2c237
--- /dev/null
+++ b/lib/utils/fixer.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const types = require('./types');
+
+module.exports = {
+  removeCommaSeparatedNode,
+};
+
+function removeCommaSeparatedNode(node, sourceCode, fixer) {
+  const tokenBefore = sourceCode.getTokenBefore(node);
+  const tokenAfter = sourceCode.getTokenAfter(node);
+
+  const removeComma = types.isCommaToken(tokenAfter)
+    ? fixer.remove(tokenAfter)
+    : fixer.remove(tokenBefore);
+  const removeNode = fixer.remove(node);
+
+  return [removeComma, removeNode];
+}
diff --git a/lib/utils/types.js b/lib/utils/types.js
index d0b8f4c4..3c8543c5 100644
--- a/lib/utils/types.js
+++ b/lib/utils/types.js
@@ -10,6 +10,7 @@ module.exports = {
   isClassDeclaration,
   isClassProperty,
   isClassPropertyWithDecorator,
+  isCommaToken,
   isConciseArrowFunctionWithCallExpression,
   isConditionalExpression,
   isDecorator,
@@ -145,6 +146,10 @@ function isClassPropertyWithDecorator(node, decoratorName) {
   );
 }
 
+function isCommaToken(token) {
+  return token.type === 'Punctuator' && token.value === ',';
+}
+
 /**
  * Check whether or not a node is an ArrowFunctionExpression with concise body
  * that contains a call expression.
diff --git a/lib/utils/utils.js b/lib/utils/utils.js
index b9783f7d..ab1cf2e5 100644
--- a/lib/utils/utils.js
+++ b/lib/utils/utils.js
@@ -13,6 +13,7 @@ module.exports = {
   collectObjectPatternBindings,
   findNodes,
   findUnorderedProperty,
+  flatten,
   getParent,
   getPropertyValue,
   getSize,
@@ -130,6 +131,23 @@ function findUnorderedProperty(arr) {
   return null;
 }
 
+/**
+ * Flattens an array.
+ * WARNING: can only handle one level of nested arrays.
+ * Input: [[1], [2]]
+ * Output: [1, 2]
+ *
+ * @param  {Object[]} arr Array to flatten.
+ * @return {Object[]}     Flattened array.
+ */
+function flatten(arr) {
+  if (!arr || arr.length === 0) {
+    return arr;
+  }
+
+  return arr.reduce((acc, val) => acc.concat(val));
+}
+
 /**
  * Gets a property's value either by property path.
  *


Test patch:
diff --git a/tests/lib/rules/no-duplicate-dependent-keys.js b/tests/lib/rules/no-duplicate-dependent-keys.js
index 2682d495..e4a30acb 100644
--- a/tests/lib/rules/no-duplicate-dependent-keys.js
+++ b/tests/lib/rules/no-duplicate-dependent-keys.js
@@ -87,7 +87,11 @@ ruleTester.run('no-duplicate-dependent-keys', rule, {
         foo: computed('model.foo', 'model.bar', 'model.baz', 'model.foo', function() {})
       }
       `,
-      output: null,
+      output: `
+      {
+        foo: computed('model.foo', 'model.bar', 'model.baz',  function() {})
+      }
+      `,
       errors: [
         {
           message: ERROR_MESSAGE,
@@ -126,7 +130,11 @@ ruleTester.run('no-duplicate-dependent-keys', rule, {
         foo: computed('collection.@each.foo', 'model.bar', 'collection.@each.foo', function() {})
       }
       `,
-      output: null,
+      output: `
+      {
+        foo: computed('collection.@each.foo', 'model.bar',  function() {})
+      }
+      `,
       errors: [
         {
           message: ERROR_MESSAGE,
@@ -161,7 +169,7 @@ ruleTester.run('no-duplicate-dependent-keys', rule, {
     },
     {
       code: "class Test { @computed('a', 'a') get someProp() { return true; } }",
-      output: null,
+      output: "class Test { @computed('a' ) get someProp() { return true; } }",
       errors: [
         {
           message: ERROR_MESSAGE,
diff --git a/tests/lib/rules/no-invalid-debug-function-arguments.js b/tests/lib/rules/no-invalid-debug-function-arguments.js
index 19924c5f..816b9315 100644
--- a/tests/lib/rules/no-invalid-debug-function-arguments.js
+++ b/tests/lib/rules/no-invalid-debug-function-arguments.js
@@ -4,6 +4,7 @@
 
 const rule = require('../../../lib/rules/no-invalid-debug-function-arguments');
 const RuleTester = require('eslint').RuleTester;
+const utils = require('../../../lib/utils/utils');
 
 const { DEBUG_FUNCTIONS, ERROR_MESSAGE } = rule;
 
@@ -20,7 +21,7 @@ const VALID_USAGES_BASIC = [
   },
 ];
 
-const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = flatten(
+const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = utils.flatten(
   DEBUG_FUNCTIONS.map((debugFunction) => [
     {
       code: `import { ${debugFunction} } from '@ember/debug'; OtherClass.${debugFunction}(true, 'My string.');`,
@@ -77,7 +78,7 @@ const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = flatten(
 
 const VALID_USAGES = [...VALID_USAGES_BASIC, ...VALID_USAGES_FOR_EACH_DEBUG_FUNCTION];
 
-const INVALID_USAGES = flatten(
+const INVALID_USAGES = utils.flatten(
   DEBUG_FUNCTIONS.map((debugFunction) => [
     {
       code: `import { ${debugFunction} } from '@ember/debug'; ${debugFunction}(true, 'My description.');`,
@@ -128,11 +129,3 @@ ruleTester.run('no-invalid-debug-function-arguments', rule, {
   valid: VALID_USAGES,
   invalid: INVALID_USAGES,
 });
-
-//------------------------------------------------------------------------------
-// Helpers
-//------------------------------------------------------------------------------
-
-function flatten(arr) {
-  return arr.reduce((acc, val) => acc.concat(val));
-}
diff --git a/tests/lib/utils/utils-test.js b/tests/lib/utils/utils-test.js
index 0eda6d0f..4c19b1a9 100644
--- a/tests/lib/utils/utils-test.js
+++ b/tests/lib/utils/utils-test.js
@@ -80,6 +80,23 @@ describe('findNodes', () => {
   });
 });
 
+describe('flatten', function () {
+  it('correctly flattens arrays', function () {
+    expect(utils.flatten([])).toStrictEqual([]);
+    expect(utils.flatten(null)).toBeNull();
+    expect(utils.flatten([[1], [2]])).toStrictEqual([1, 2]);
+    expect(
+      utils.flatten([
+        [1, 2],
+        [3, 4],
+      ])
+    ).toStrictEqual([1, 2, 3, 4]);
+    expect(utils.flatten([[1], [2, 3], [4, 5, 6]])).toStrictEqual([1, 2, 3, 4, 5, 6]); // different lengths forward
+    expect(utils.flatten([[6, 5, 4], [3, 2], [1]])).toStrictEqual([6, 5, 4, 3, 2, 1]); // different lengths reverse
+    expect(utils.flatten([[1], null, [2]])).toStrictEqual([1, null, 2]); // with null array
+  });
+});
+
 describe('function sort order', function () {
   it('has exported functions in sorted order', function () {
     expect(Object.getOwnPropertyNames(utils)).toStrictEqual(