Repository: ember-cli/eslint-plugin-ember
Pull request #795 (2020-04-17 18:37:57)

Disallow accessing `_routerMicrolib` in `no-private-routing-service` rule
Fixes 1/2 of #762

Disallows accessing private `_routerMicrolib`. Note this rule disallows any identifier to be called `_routerMicrolib`, which is simpler and faster to run over checking whether the Identifier's parent is `router`.


Patch:
diff --git a/docs/rules/no-private-routing-service.md b/docs/rules/no-private-routing-service.md
index fbb25917..4d8ae3b5 100644
--- a/docs/rules/no-private-routing-service.md
+++ b/docs/rules/no-private-routing-service.md
@@ -2,7 +2,7 @@
 
 :white_check_mark: The `"extends": "plugin:ember/recommended"` property in a configuration file enables this rule.
 
-Disallow the use of the private `-routing` service.
+Disallow the use of the private `-routing` service and accessing `_routerMicrolib`.
 
 There has been a public `router` service since Ember 2.16 and using the private routing service should be unnecessary.
 
@@ -27,6 +27,18 @@ export default class MyComponent extends Component {
 }
 ```
 
+```javascript
+import Component from '@ember/component';
+
+export default class MyComponent extends Component {
+  @service('router') router;
+
+  get someMethod() {
+    return this.router._routerMicrolib.activeTransition;
+  }
+}
+```
+
 Examples of **correct** code for this rule:
 
 ```javascript
diff --git a/lib/rules/no-duplicate-dependent-keys.js b/lib/rules/no-duplicate-dependent-keys.js
index e5881ceb..29d9a5a1 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 javascriptUtils = require('../utils/javascript');
+const utils = require('../utils/utils');
 
 const ERROR_MESSAGE = 'Dependent keys should not be repeated';
 //------------------------------------------------------------------------------
@@ -43,7 +43,7 @@ module.exports = {
                 return null;
               }
 
-              return javascriptUtils.flatten(
+              return utils.flatten(
                 duplicateNodes.map((duplicateNode) =>
                   fixerUtils.removeCommaSeparatedNode(duplicateNode, sourceCode, fixer)
                 )
diff --git a/lib/rules/no-private-routing-service.js b/lib/rules/no-private-routing-service.js
index 18afa6a9..7d590f23 100644
--- a/lib/rules/no-private-routing-service.js
+++ b/lib/rules/no-private-routing-service.js
@@ -7,9 +7,11 @@ const types = require('../utils/types');
 // Rule Definition
 //------------------------------------------------------------------------------
 
-const ERROR_MESSAGE =
+const PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE =
   "Don't inject the private '-routing' service. Instead use the public 'router' service.";
 
+const ROUTER_MICROLIB_ERROR_MESSAGE = "Don't access the `_routerMicrolib` as it is private API";
+
 module.exports = {
   meta: {
     type: 'suggestion',
@@ -24,10 +26,12 @@ module.exports = {
     schema: [],
   },
 
-  ERROR_MESSAGE,
+  PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE,
+  ROUTER_MICROLIB_ERROR_MESSAGE,
 
   create(context) {
     const ROUTING_SERVICE_NAME = '-routing';
+    const ROUTER_MICROLIB_NAME = '_routerMicrolib';
 
     return {
       Property(node) {
@@ -36,9 +40,10 @@ module.exports = {
           node.value.arguments.length > 0 &&
           node.value.arguments[0].value === ROUTING_SERVICE_NAME
         ) {
-          context.report({ node, message: ERROR_MESSAGE });
+          context.report({ node, message: PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE });
         }
       },
+
       ClassProperty(node) {
         if (!node.decorators || !types.isClassPropertyWithDecorator(node, 'service')) {
           return;
@@ -55,7 +60,19 @@ module.exports = {
         });
 
         if (hasRoutingServiceDecorator) {
-          context.report({ node, message: ERROR_MESSAGE });
+          context.report({ node, message: PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE });
+        }
+      },
+
+      Literal(node) {
+        if (typeof node.value === 'string' && node.value.includes(ROUTER_MICROLIB_NAME)) {
+          context.report({ node, message: ROUTER_MICROLIB_ERROR_MESSAGE });
+        }
+      },
+
+      Identifier(node) {
+        if (node.name === ROUTER_MICROLIB_NAME) {
+          context.report({ node, message: ROUTER_MICROLIB_ERROR_MESSAGE });
         }
       },
     };
diff --git a/lib/rules/require-computed-property-dependencies.js b/lib/rules/require-computed-property-dependencies.js
index e257e1bc..ebc4ba34 100644
--- a/lib/rules/require-computed-property-dependencies.js
+++ b/lib/rules/require-computed-property-dependencies.js
@@ -3,10 +3,8 @@
 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');
 
 /**
@@ -92,6 +90,18 @@ 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.
  *
@@ -391,21 +401,16 @@ module.exports = {
             node
           );
 
-          const usedKeys1 = javascriptUtils.flatMap(
+          const usedKeys1 = flatMap(
             findEmberGetCalls(computedPropertyFunctionBody),
             extractEmberGetDependencies
           );
-          const usedKeys2 = javascriptUtils.flatMap(
-            findThisGetCalls(computedPropertyFunctionBody),
-            (node) => {
-              return extractThisGetDependencies(node, context);
-            }
-          );
+          const usedKeys2 = flatMap(findThisGetCalls(computedPropertyFunctionBody), (node) => {
+            return extractThisGetDependencies(node, context);
+          });
           const usedKeys = [...usedKeys1, ...usedKeys2];
 
-          const expandedDeclaredKeys = computedPropertyDependentKeyUtils.expandKeys(
-            declaredDependencies.keys.map(nodeToStringValue)
-          );
+          const expandedDeclaredKeys = expandKeys(declaredDependencies.keys.map(nodeToStringValue));
 
           const undeclaredKeysBeforeServiceCheck = removeRedundantKeys(
             usedKeys
@@ -440,7 +445,7 @@ module.exports = {
                 const missingDependenciesAsArgumentsForDynamicKeys = declaredDependencies.dynamicKeys.map(
                   (dynamicKey) => sourceCode.getText(dynamicKey)
                 );
-                const missingDependenciesAsArgumentsForStringKeys = computedPropertyDependentKeyUtils.collapseKeys(
+                const missingDependenciesAsArgumentsForStringKeys = collapseKeys(
                   removeRedundantKeys([...undeclaredKeys, ...expandedDeclaredKeys])
                 );
 
@@ -499,3 +504,107 @@ 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
deleted file mode 100644
index 18c806e9..00000000
--- a/lib/utils/computed-property-dependent-keys.js
+++ /dev/null
@@ -1,99 +0,0 @@
-'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.filter((part) => part !== '').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
deleted file mode 100644
index 80dfffd0..00000000
--- a/lib/utils/javascript.js
+++ /dev/null
@@ -1,56 +0,0 @@
-'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 f1d5ff8c..ba7c781a 100644
--- a/lib/utils/new-module.js
+++ b/lib/utils/new-module.js
@@ -2,13 +2,6 @@
 
 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
@@ -171,3 +164,10 @@ 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 7cc12794..a3f7bcb5 100644
--- a/lib/utils/property-getter.js
+++ b/lib/utils/property-getter.js
@@ -1,11 +1,4 @@
 const types = require('./types');
-const javascriptUtils = require('./javascript');
-
-module.exports = {
-  nodeToDependentKey,
-  isSimpleThisExpression,
-  isThisGetCall,
-};
 
 /**
  * Checks if a CallExpression node looks like `this.get('property')`.
@@ -61,6 +54,12 @@ 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.
  *
@@ -77,7 +76,11 @@ function nodeToDependentKey(nodeWithThisExpression, context) {
   }
 
   const sourceCode = context.getSourceCode();
-  return javascriptUtils.removeWhitespace(
-    sourceCode.getText(nodeWithThisExpression).replace(/^this\./, '')
-  );
+  return removeWhitespace(sourceCode.getText(nodeWithThisExpression).replace(/^this\./, ''));
 }
+
+module.exports = {
+  nodeToDependentKey,
+  isSimpleThisExpression,
+  isThisGetCall,
+};
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-invalid-debug-function-arguments.js b/tests/lib/rules/no-invalid-debug-function-arguments.js
index e8bcda8c..816b9315 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 javascriptUtils = require('../../../lib/utils/javascript');
+const utils = require('../../../lib/utils/utils');
 
 const { DEBUG_FUNCTIONS, ERROR_MESSAGE } = rule;
 
@@ -21,7 +21,7 @@ const VALID_USAGES_BASIC = [
   },
 ];
 
-const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = javascriptUtils.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.');`,
@@ -78,7 +78,7 @@ const VALID_USAGES_FOR_EACH_DEBUG_FUNCTION = javascriptUtils.flatten(
 
 const VALID_USAGES = [...VALID_USAGES_BASIC, ...VALID_USAGES_FOR_EACH_DEBUG_FUNCTION];
 
-const INVALID_USAGES = javascriptUtils.flatten(
+const INVALID_USAGES = utils.flatten(
   DEBUG_FUNCTIONS.map((debugFunction) => [
     {
       code: `import { ${debugFunction} } from '@ember/debug'; ${debugFunction}(true, 'My description.');`,
diff --git a/tests/lib/rules/no-private-routing-service.js b/tests/lib/rules/no-private-routing-service.js
index 22689a0f..e83d1564 100644
--- a/tests/lib/rules/no-private-routing-service.js
+++ b/tests/lib/rules/no-private-routing-service.js
@@ -5,7 +5,7 @@
 const rule = require('../../../lib/rules/no-private-routing-service');
 const RuleTester = require('eslint').RuleTester;
 
-const { ERROR_MESSAGE } = rule;
+const { PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE, ROUTER_MICROLIB_ERROR_MESSAGE } = rule;
 
 //------------------------------------------------------------------------------
 // Tests
@@ -56,14 +56,36 @@ ruleTester.run('no-private-routing-service', rule, {
     {
       code: "export default Component.extend({ routing: service('-routing') });",
       output: null,
-      errors: [{ message: ERROR_MESSAGE, type: 'Property' }],
+      errors: [{ message: PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE, type: 'Property' }],
     },
 
     // Octane
     {
       code: "export default class MyComponent extends Component { @service('-routing') routing; }",
       output: null,
-      errors: [{ message: ERROR_MESSAGE, type: 'ClassProperty' }],
+      errors: [{ message: PRIVATE_ROUTING_SERVICE_ERROR_MESSAGE, type: 'ClassProperty' }],
+    },
+
+    // _routerMicrolib
+    {
+      code: "get(this, 'router._routerMicrolib');",
+      output: null,
+      errors: [{ message: ROUTER_MICROLIB_ERROR_MESSAGE, type: 'Literal' }],
+    },
+    {
+      code: "get(this, 'router._router._routerMicrolib');",
+      output: null,
+      errors: [{ message: ROUTER_MICROLIB_ERROR_MESSAGE, type: 'Literal' }],
+    },
+    {
+      code: 'this.router._routerMicrolib;',
+      output: null,
+      errors: [{ message: ROUTER_MICROLIB_ERROR_MESSAGE, type: 'Identifier' }],
+    },
+    {
+      code: 'this.router._router._routerMicrolib;',
+      output: null,
+      errors: [{ message: ROUTER_MICROLIB_ERROR_MESSAGE, type: 'Identifier' }],
     },
   ],
 });
diff --git a/tests/lib/rules/require-computed-property-dependencies.js b/tests/lib/rules/require-computed-property-dependencies.js
index 0e225e9e..8890d711 100644
--- a/tests/lib/rules/require-computed-property-dependencies.js
+++ b/tests/lib/rules/require-computed-property-dependencies.js
@@ -95,12 +95,6 @@ ruleTester.run('require-computed-property-dependencies', rule, {
         return this.article.title + someFunction(this.article.comments.innerProperty);
       });
     `,
-    // Braces but no nesting:
-    `
-      Ember.computed('{foo,bar}', function() {
-        return this.get('foo') + this.get('bar');
-      });
-    `,
     // Computed macro that should be ignored:
     `
       Ember.computed.someMacro(function() {
diff --git a/tests/lib/utils/computed-property-dependent-keys-test.js b/tests/lib/utils/computed-property-dependent-keys-test.js
deleted file mode 100644
index dbf6402c..00000000
--- a/tests/lib/utils/computed-property-dependent-keys-test.js
+++ /dev/null
@@ -1,37 +0,0 @@
-'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',
-    ]);
-    expect(cpdkUtils.expandKey('{foo,bar}')).toStrictEqual(['foo', 'bar']);
-  });
-});
diff --git a/tests/lib/utils/javascript-test.js b/tests/lib/utils/javascript-test.js
deleted file mode 100644
index b46cc92f..00000000
--- a/tests/lib/utils/javascript-test.js
+++ /dev/null
@@ -1,43 +0,0 @@
-'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 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(