Repository: ember-cli/eslint-plugin-ember
Pull request #793 (2020-04-17 15:07:01)

Add some util function tests and reorganize some util files


Patch:
diff --git a/lib/rules/no-duplicate-dependent-keys.js b/lib/rules/no-duplicate-dependent-keys.js
index 29d9a5a1..e5881ceb 100644
--- a/lib/rules/no-duplicate-dependent-keys.js
+++ b/lib/rules/no-duplicate-dependent-keys.js
@@ -3,7 +3,7 @@
 const emberUtils = require('../utils/ember');
 const types = require('../utils/types');
 const fixerUtils = require('../utils/fixer');
-const utils = require('../utils/utils');
+const javascriptUtils = require('../utils/javascript');
 
 const ERROR_MESSAGE = 'Dependent keys should not be repeated';
 //------------------------------------------------------------------------------
@@ -43,7 +43,7 @@ module.exports = {
                 return null;
               }
 
-              return utils.flatten(
+              return javascriptUtils.flatten(
                 duplicateNodes.map((duplicateNode) =>
                   fixerUtils.removeCommaSeparatedNode(duplicateNode, sourceCode, fixer)
                 )
diff --git a/lib/rules/require-computed-property-dependencies.js b/lib/rules/require-computed-property-dependencies.js
index ebc4ba34..e257e1bc 100644
--- a/lib/rules/require-computed-property-dependencies.js
+++ b/lib/rules/require-computed-property-dependencies.js
@@ -3,8 +3,10 @@
 const Traverser = require('../utils/traverser');
 const emberUtils = require('../utils/ember');
 const computedPropertyUtils = require('../utils/computed-properties');
+const javascriptUtils = require('../utils/javascript');
 const types = require('../utils/types');
 const propertyGetterUtils = require('../utils/property-getter');
+const computedPropertyDependentKeyUtils = require('../utils/computed-property-dependent-keys');
 const assert = require('assert');
 
 /**
@@ -90,18 +92,6 @@ function nodeToStringValue(node) {
   }
 }
 
-/**
- * Builds an array by concatenating the results of a map.
- *
- * @template T, U
- * @param {Array<T>} array
- * @param {function(T): Array<U>} callback
- * @returns {Array<U>}
- */
-function flatMap(array, callback) {
-  return array.reduce((result, item) => result.concat(callback(item)), []);
-}
-
 /**
  * Splits arguments to `Ember.computed` into string keys and dynamic keys.
  *
@@ -401,16 +391,21 @@ module.exports = {
             node
           );
 
-          const usedKeys1 = flatMap(
+          const usedKeys1 = javascriptUtils.flatMap(
             findEmberGetCalls(computedPropertyFunctionBody),
             extractEmberGetDependencies
           );
-          const usedKeys2 = flatMap(findThisGetCalls(computedPropertyFunctionBody), (node) => {
-            return extractThisGetDependencies(node, context);
-          });
+          const usedKeys2 = javascriptUtils.flatMap(
+            findThisGetCalls(computedPropertyFunctionBody),
+            (node) => {
+              return extractThisGetDependencies(node, context);
+            }
+          );
           const usedKeys = [...usedKeys1, ...usedKeys2];
 
-          const expandedDeclaredKeys = expandKeys(declaredDependencies.keys.map(nodeToStringValue));
+          const expandedDeclaredKeys = computedPropertyDependentKeyUtils.expandKeys(
+            declaredDependencies.keys.map(nodeToStringValue)
+          );
 
           const undeclaredKeysBeforeServiceCheck = removeRedundantKeys(
             usedKeys
@@ -445,7 +440,7 @@ module.exports = {
                 const missingDependenciesAsArgumentsForDynamicKeys = declaredDependencies.dynamicKeys.map(
                   (dynamicKey) => sourceCode.getText(dynamicKey)
                 );
-                const missingDependenciesAsArgumentsForStringKeys = collapseKeys(
+                const missingDependenciesAsArgumentsForStringKeys = computedPropertyDependentKeyUtils.collapseKeys(
                   removeRedundantKeys([...undeclaredKeys, ...expandedDeclaredKeys])
                 );
 
@@ -504,107 +499,3 @@ module.exports = {
     };
   },
 };
-
-function isBare(key) {
-  return !key.includes('.') || key.endsWith('[]');
-}
-
-/**
- * Collapse dependency keys with braces if possible.
- *
- * Example:
- * Input: ["foo.bar", "foo.baz", "quux.[]"]
- * Output: ["foo.{bar,baz}", "quux.[]"]
- *
- * @param {Array<string>} keys
- * @returns string
- */
-function collapseKeys(keys) {
-  const uniqueKeys = [...new Set(keys)];
-
-  const bareKeys = uniqueKeys.filter(isBare);
-  const rest = uniqueKeys.filter((key) => !isBare(key));
-
-  const mapByParent = rest.reduce((mapByParent, key) => {
-    const [head, ...rest] = key.split('.').reverse();
-    const parent = rest.reverse().join('.');
-
-    mapByParent.set(parent, mapByParent.get(parent) || []);
-    mapByParent.get(parent).push(head);
-
-    return mapByParent;
-  }, new Map());
-
-  const joined = [...mapByParent.keys()].map((parent) => {
-    const children = mapByParent.get(parent);
-    if (children.length > 1) {
-      return `${parent}.{${children.sort().join(',')}}`;
-    }
-    return `${parent}.${children[0]}`;
-  });
-
-  return [...bareKeys, ...joined].sort().map((key) => `'${key}'`);
-}
-
-/**
- * ["foo.{bar,baz}", "quux"] => ["foo.bar", "foo.baz", "quux"]
- * @param {Array<string>} keys
- * @returns {Array<string>}
- */
-function expandKeys(keys) {
-  // aka flat map
-  return [].concat(...keys.map(expandKey)); // eslint-disable-line unicorn/prefer-flat-map
-}
-
-/**
- * Expand any brace usage in a dependency key.
- *
- * Example:
- * Input: "foo.{bar,baz}"
- * Output: ["foo.bar", "foo.baz"]
- *
- * @param {string} key
- * @returns {Array<string>}
- */
-function expandKey(key) {
-  if (key.includes('{')) {
-    // key = "foo.{bar,baz}"
-    const keyParts = key.split('{'); // ["foo", "{bar,baz}"]
-    const keyBeforeCurly = keyParts[0].slice(0, Math.max(0, keyParts[0].length - 1)); // "foo"
-    const keyAfterCurly = keyParts[1]; // "{bar,baz}"
-    const keyAfterCurlySplitByCommas = keyAfterCurly.replace(/{|}/g, '').split(','); // ["bar", "baz"]
-    const keyRecombined = [[keyBeforeCurly], keyAfterCurlySplitByCommas]; // [["foo"], ["baz", "baz"]]
-    return keyRecombined
-      .reduce(
-        (acc, nextParts) =>
-          // iteration 1 (["foo"]): do nothing (duplicate 0 times), resulting in acc === [["foo"]]
-          // iteration 2 (["bar", "baz"]): duplicate acc once, resulting in `[["foo"], ["foo"]]
-          duplicateArrays(acc, nextParts.length - 1).map((base, index) =>
-            // evenly distribute the parts across the repeated base keys.
-            // nextParts[0 % 2] => "bar"
-            // nextParts[1 % 2] => "baz"
-            base.concat(nextParts[index % nextParts.length])
-          ),
-        [[]]
-      ) // [["foo", "bar"], ["foo", "baz"]]
-      .map((expanded) => expanded.join('.')); // ["foo.bar", "foo.baz"]
-  } else {
-    // No braces.
-    // Example: "hello.world"
-    return key;
-  }
-}
-
-/**
- * duplicateArrays([["a", "b"]], 2) -> [["a", "b"], ["a", "b"], ["a", "b"]]
- * @param {Array<Array>} arr
- * @param {number} times
- * @returns {Array<Array>}
- */
-function duplicateArrays(arr, times) {
-  const result = [];
-  for (let i = 0; i <= times; i++) {
-    result.push(...arr.map((a) => a.slice(0)));
-  }
-  return result;
-}
diff --git a/lib/utils/computed-property-dependent-keys.js b/lib/utils/computed-property-dependent-keys.js
new file mode 100644
index 00000000..505cee31
--- /dev/null
+++ b/lib/utils/computed-property-dependent-keys.js
@@ -0,0 +1,99 @@
+'use strict';
+
+const javascriptUtils = require('./javascript');
+
+module.exports = {
+  collapseKeys,
+  expandKeys,
+  expandKey,
+};
+
+function isBare(key) {
+  return !key.includes('.') || key.endsWith('[]');
+}
+
+/**
+ * Collapse dependency keys with braces if possible.
+ *
+ * Example:
+ * Input: ["foo.bar", "foo.baz", "quux.[]"]
+ * Output: ["foo.{bar,baz}", "quux.[]"]
+ *
+ * @param {Array<string>} keys
+ * @returns string
+ */
+function collapseKeys(keys) {
+  const uniqueKeys = [...new Set(keys)];
+
+  const bareKeys = uniqueKeys.filter(isBare);
+  const rest = uniqueKeys.filter((key) => !isBare(key));
+
+  const mapByParent = rest.reduce((mapByParent, key) => {
+    const [head, ...rest] = key.split('.').reverse();
+    const parent = rest.reverse().join('.');
+
+    mapByParent.set(parent, mapByParent.get(parent) || []);
+    mapByParent.get(parent).push(head);
+
+    return mapByParent;
+  }, new Map());
+
+  const joined = [...mapByParent.keys()].map((parent) => {
+    const children = mapByParent.get(parent);
+    if (children.length > 1) {
+      return `${parent}.{${children.sort().join(',')}}`;
+    }
+    return `${parent}.${children[0]}`;
+  });
+
+  return [...bareKeys, ...joined].sort().map((key) => `'${key}'`);
+}
+
+/**
+ * ["foo.{bar,baz}", "quux"] => ["foo.bar", "foo.baz", "quux"]
+ * @param {Array<string>} keys
+ * @returns {Array<string>}
+ */
+function expandKeys(keys) {
+  // aka flat map
+  return [].concat(...keys.map(expandKey)); // eslint-disable-line unicorn/prefer-flat-map
+}
+
+/**
+ * Expand any brace usage in a dependency key.
+ *
+ * Example:
+ * Input: "foo.{bar,baz}"
+ * Output: ["foo.bar", "foo.baz"]
+ *
+ * @param {string} key
+ * @returns {Array<string>}
+ */
+function expandKey(key) {
+  if (key.includes('{')) {
+    // key = "foo.{bar,baz}"
+    const keyParts = key.split('{'); // ["foo", "{bar,baz}"]
+    const keyBeforeCurly = keyParts[0].slice(0, Math.max(0, keyParts[0].length - 1)); // "foo"
+    const keyAfterCurly = keyParts[1]; // "{bar,baz}"
+    const keyAfterCurlySplitByCommas = keyAfterCurly.replace(/{|}/g, '').split(','); // ["bar", "baz"]
+    const keyRecombined = [[keyBeforeCurly], keyAfterCurlySplitByCommas]; // [["foo"], ["baz", "baz"]]
+    return keyRecombined
+      .reduce(
+        (acc, nextParts) =>
+          // iteration 1 (["foo"]): do nothing (duplicate 0 times), resulting in acc === [["foo"]]
+          // iteration 2 (["bar", "baz"]): duplicate acc once, resulting in `[["foo"], ["foo"]]
+          javascriptUtils.duplicateArrays(acc, nextParts.length - 1).map((base, index) =>
+            // evenly distribute the parts across the repeated base keys.
+            // nextParts[0 % 2] => "bar"
+            // nextParts[1 % 2] => "baz"
+            base.concat(nextParts[index % nextParts.length])
+          ),
+        [[]]
+      ) // [["foo", "bar"], ["foo", "baz"]]
+      .map((expanded) => expanded.join('.')); // ["foo.bar", "foo.baz"]
+  } else {
+    // No braces.
+    // Example: "hello.world"
+    return key;
+  }
+}
diff --git a/lib/utils/javascript.js b/lib/utils/javascript.js
new file mode 100644
index 00000000..80dfffd0
--- /dev/null
+++ b/lib/utils/javascript.js
@@ -0,0 +1,56 @@
+'use strict';
+
+module.exports = {
+  duplicateArrays,
+  flatten,
+  flatMap,
+  removeWhitespace,
+};
+
+/**
+ * duplicateArrays([["a", "b"]], 2) -> [["a", "b"], ["a", "b"], ["a", "b"]]
+ * @param {Array<Array>} arr
+ * @param {number} times
+ * @returns {Array<Array>}
+ */
+function duplicateArrays(arr, times) {
+  const result = [];
+  for (let i = 0; i <= times; i++) {
+    result.push(...arr.map((a) => a.slice(0)));
+  }
+  return result;
+}
+
+/**
+ * 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));
+}
+
+/**
+ * Builds an array by concatenating the results of a map.
+ *
+ * @template T, U
+ * @param {Array<T>} array
+ * @param {function(T): Array<U>} callback
+ * @returns {Array<U>}
+ */
+function flatMap(array, callback) {
+  return array.reduce((result, item) => result.concat(callback(item)), []);
+}
+
+function removeWhitespace(str) {
+  // Removes whitespace anywhere inside string.
+  return str.replace(/\s/g, '');
+}
diff --git a/lib/utils/new-module.js b/lib/utils/new-module.js
index ba7c781a..f1d5ff8c 100644
--- a/lib/utils/new-module.js
+++ b/lib/utils/new-module.js
@@ -2,6 +2,13 @@
 
 const { isIdentifier, isObjectPattern, isVariableDeclarator } = require('./types');
 
+module.exports = {
+  buildFix,
+  buildMessage,
+  getFullNames,
+  isDestructuring,
+};
+
 const EMBER_NAMESPACES = new Set(['inject.controller', 'inject.service']);
 
 // Both the Controller module and Service module each make available the
@@ -164,10 +171,3 @@ function isDestructuring(node) {
     isObjectPattern(node.id)
   );
 }
-
-module.exports = {
-  buildFix,
-  buildMessage,
-  getFullNames,
-  isDestructuring,
-};
diff --git a/lib/utils/property-getter.js b/lib/utils/property-getter.js
index a3f7bcb5..7cc12794 100644
--- a/lib/utils/property-getter.js
+++ b/lib/utils/property-getter.js
@@ -1,4 +1,11 @@
 const types = require('./types');
+const javascriptUtils = require('./javascript');
+
+module.exports = {
+  nodeToDependentKey,
+  isSimpleThisExpression,
+  isThisGetCall,
+};
 
 /**
  * Checks if a CallExpression node looks like `this.get('property')`.
@@ -54,12 +61,6 @@ function isSimpleThisExpression(node) {
 
   return false;
 }
-
-function removeWhitespace(str) {
-  // Removes whitespace anywhere inside string.
-  return str.replace(/\s/g, '');
-}
-
 /**
  * Converts a Node containing a ThisExpression to its dependent key.
  *
@@ -76,11 +77,7 @@ function nodeToDependentKey(nodeWithThisExpression, context) {
   }
 
   const sourceCode = context.getSourceCode();
-  return removeWhitespace(sourceCode.getText(nodeWithThisExpression).replace(/^this\./, ''));
+  return javascriptUtils.removeWhitespace(
+    sourceCode.getText(nodeWithThisExpression).replace(/^this\./, '')
+  );
 }
-
-module.exports = {
-  nodeToDependentKey,
-  isSimpleThisExpression,
-  isThisGetCall,
-};
diff --git a/lib/utils/utils.js b/lib/utils/utils.js
index ab1cf2e5..b9783f7d 100644
--- a/lib/utils/utils.js
+++ b/lib/utils/utils.js
@@ -13,7 +13,6 @@ module.exports = {
   collectObjectPatternBindings,
   findNodes,
   findUnorderedProperty,
-  flatten,
   getParent,
   getPropertyValue,
   getSize,
@@ -131,23 +130,6 @@ 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-invalid-debug-function-arguments.js b/tests/lib/rules/no-invalid-debug-function-arguments.js
index 816b9315..e8bcda8c 100644
--- a/tests/lib/rules/no-invalid-debug-function-arguments.js
+++ b/tests/lib/rules/no-invalid-debug-function-arguments.js
@@ -4,7 +4,7 @@
 
 const rule = require('../../../lib/rules/no-invalid-debug-function-arguments');
 const RuleTester = require('eslint').RuleTester;
-const utils = require('../../../lib/utils/utils');
+const javascriptUtils = require('../../../lib/utils/javascript');
 
 const { DEBUG_FUNCTIONS, ERROR_MESSAGE } = rule;
 
@@ -21,7 +21,7 @@ const VALID_USAGES_BASIC = [
   },
 ];
 
-const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = utils.flatten(
+const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = javascriptUtils.flatten(
   DEBUG_FUNCTIONS.map((debugFunction) => [
     {
       code: `import { ${debugFunction} } from '@ember/debug'; OtherClass.${debugFunction}(true, 'My string.');`,
@@ -78,7 +78,7 @@ const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = utils.flatten(
 
 const VALID_USAGES = [...VALID_USAGES_BASIC, ...VALID_USAGES_FOR_EACH_DEBUG_FUNCTION];
 
-const INVALID_USAGES = utils.flatten(
+const INVALID_USAGES = javascriptUtils.flatten(
   DEBUG_FUNCTIONS.map((debugFunction) => [
     {
       code: `import { ${debugFunction} } from '@ember/debug'; ${debugFunction}(true, 'My description.');`,
diff --git a/tests/lib/utils/computed-property-dependent-keys-test.js b/tests/lib/utils/computed-property-dependent-keys-test.js
new file mode 100644
index 00000000..ae465426
--- /dev/null
+++ b/tests/lib/utils/computed-property-dependent-keys-test.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const cpdkUtils = require('../../../lib/utils/computed-property-dependent-keys');
+
+describe('collapseKeys', () => {
+  it('returns the right result', () => {
+    // No opportunity for braces
+    expect(cpdkUtils.collapseKeys([])).toStrictEqual([]);
+    expect(cpdkUtils.collapseKeys(['foo'])).toStrictEqual(["'foo'"]);
+    expect(cpdkUtils.collapseKeys(['foo', 'bar'])).toStrictEqual(["'bar'", "'foo'"]);
+    expect(cpdkUtils.collapseKeys(['foo.abc', 'bar.def'])).toStrictEqual([
+      "'bar.def'",
+      "'foo.abc'",
+    ]);
+
+    // Opportunity for braces
+    expect(cpdkUtils.collapseKeys(['foo.abc', 'foo.def'])).toStrictEqual(["'foo.{abc,def}'"]);
+  });
+});
+
+describe('expandKey', () => {
+  it('returns the right result', () => {
+    // No expansion possible
+    expect(cpdkUtils.expandKey('')).toStrictEqual('');
+    expect(cpdkUtils.expandKey('foo')).toStrictEqual('foo');
+    expect(cpdkUtils.expandKey('foo.bar')).toStrictEqual('foo.bar');
+    expect(cpdkUtils.expandKey('foo.{bar}')).toStrictEqual(['foo.bar']);
+
+    // Expansion possible
+    expect(cpdkUtils.expandKey('foo.{bar1,bar2}')).toStrictEqual(['foo.bar1', 'foo.bar2']);
+    expect(cpdkUtils.expandKey('foo.@each.{bar1,bar2}')).toStrictEqual([
+      'foo.@each.bar1',
+      'foo.@each.bar2',
+    ]);
+  });
+});
diff --git a/tests/lib/utils/javascript-test.js b/tests/lib/utils/javascript-test.js
new file mode 100644
index 00000000..b46cc92f
--- /dev/null
+++ b/tests/lib/utils/javascript-test.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const javascriptUtils = require('../../../lib/utils/javascript');
+
+describe('duplicateArrays', () => {
+  it('returns the right result', () => {
+    expect(javascriptUtils.duplicateArrays([], 2)).toStrictEqual([]);
+    expect(javascriptUtils.duplicateArrays(['abc'], 2)).toStrictEqual(['abc', 'abc', 'abc']);
+    expect(javascriptUtils.duplicateArrays(['first', 'second'], 2)).toStrictEqual([
+      'first',
+      'second',
+      'first',
+      'second',
+      'first',
+      'second',
+    ]);
+  });
+});
+
+describe('flatten', function () {
+  it('correctly flattens arrays', function () {
+    expect(javascriptUtils.flatten([])).toStrictEqual([]);
+    expect(javascriptUtils.flatten(null)).toBeNull();
+    expect(javascriptUtils.flatten([[1], [2]])).toStrictEqual([1, 2]);
+    expect(
+      javascriptUtils.flatten([
+        [1, 2],
+        [3, 4],
+      ])
+    ).toStrictEqual([1, 2, 3, 4]);
+    expect(javascriptUtils.flatten([[1], [2, 3], [4, 5, 6]])).toStrictEqual([1, 2, 3, 4, 5, 6]); // different lengths forward
+    expect(javascriptUtils.flatten([[6, 5, 4], [3, 2], [1]])).toStrictEqual([6, 5, 4, 3, 2, 1]); // different lengths reverse
+    expect(javascriptUtils.flatten([[1], null, [2]])).toStrictEqual([1, null, 2]); // with null array
+  });
+});
+
+describe('removeWhitespace', () => {
+  it('returns the right result', () => {
+    expect(javascriptUtils.removeWhitespace('abcdef')).toStrictEqual('abcdef');
+    expect(javascriptUtils.removeWhitespace('abc def')).toStrictEqual('abcdef');
+    expect(javascriptUtils.removeWhitespace(' abc def ')).toStrictEqual('abcdef');
+  });
+});
diff --git a/tests/lib/utils/utils-test.js b/tests/lib/utils/utils-test.js
index 4c19b1a9..0eda6d0f 100644
--- a/tests/lib/utils/utils-test.js
+++ b/tests/lib/utils/utils-test.js
@@ -80,23 +80,6 @@ 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(