Repository: ember-cli/eslint-plugin-ember
Pull request #359 (2018-12-24 00:55:59)

Add 'no-deeply-nested-dependent-keys-with-each' rule.


Patch:
diff --git a/README.md b/README.md
index e86b86ea..3ae33b4e 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [no-attrs-in-components](./docs/rules/no-attrs-in-components.md) | Disallow usage of this.attrs in components |
 | :white_check_mark: | [no-attrs-snapshot](./docs/rules/no-attrs-snapshot.md) | Disallow use of attrs snapshot in `didReceiveAttrs` and `didUpdateAttrs` |
 | :white_check_mark: | [no-capital-letters-in-routes](./docs/rules/no-capital-letters-in-routes.md) | Raise an error when there is a route with uppercased letters in router.js |
+|  | [no-deeply-nested-dependent-keys-with-each](./docs/rules/no-deeply-nested-dependent-keys-with-each.md) | Disallows 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 dependent keys |
 | :wrench: | [no-ember-super-in-es-classes](./docs/rules/no-ember-super-in-es-classes.md) | Prevents use of `this._super` in ES class methods |
 | :white_check_mark: | [no-ember-testing-in-module-scope](./docs/rules/no-ember-testing-in-module-scope.md) | Prevents use of Ember.testing in module scope |
diff --git a/docs/rules/avoid-leaking-state-in-ember-objects.md b/docs/rules/avoid-leaking-state-in-ember-objects.md
index f0f45dd3..c124d55c 100644
--- a/docs/rules/avoid-leaking-state-in-ember-objects.md
+++ b/docs/rules/avoid-leaking-state-in-ember-objects.md
@@ -7,10 +7,7 @@
 Example configuration:
 
 ```
-const { DEFAULT_IGNORED_PROPERTIES } = require('eslint-plugin-ember/lib/rules/avoid-leaking-state-in-ember-objects');
-
 ember/avoid-leaking-state-in-ember-objects: [1, [
-  ...DEFAULT_IGNORED_PROPERTIES,
   'array',
   'of',
   'ignored',
diff --git a/docs/rules/no-deeply-nested-dependent-keys-with-each.md b/docs/rules/no-deeply-nested-dependent-keys-with-each.md
new file mode 100644
index 00000000..f9d404bc
--- /dev/null
+++ b/docs/rules/no-deeply-nested-dependent-keys-with-each.md
@@ -0,0 +1,31 @@
+# Disallows usage of deeply-nested computed property dependent keys with `@each`.  (no-deeply-nested-dependent-keys-with-each)
+
+For performance / complexity reasons, Ember does allow deeply-nested computed property dependent keys with `@each`. At runtime, it will show a warning about this:
+
+> WARNING: Dependent keys containing @each only work one level deep. You used the key "foo.@each.bar.baz" which is invalid. Please create an intermediary computed property.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```js
+displayNames: computed('todos.@each.owner.name', function() {
+  return this.todos.map(todo => todo.owner.name);
+})
+```
+
+Examples of **correct** code for this rule:
+
+```js
+displayNames: computed('owners.@each.name', function() {
+  return this.owners.mapBy(owners, 'name');
+}),
+
+owners: computed('todos.@each.owner', function() {
+  return this.todos.mapBy(todo, 'owner');
+})
+```
+
+## Further Reading
+
+* See the [documentation](https://guides.emberjs.com/release/object-model/computed-properties-and-aggregate-data/) for Ember computed properties with `@each`
diff --git a/lib/index.js b/lib/index.js
index 6f0bde72..9d971ade 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -15,6 +15,7 @@ module.exports = {
     'no-attrs-in-components': require('./rules/no-attrs-in-components'),
     'no-attrs-snapshot': require('./rules/no-attrs-snapshot'),
     'no-capital-letters-in-routes': require('./rules/no-capital-letters-in-routes'),
+    'no-deeply-nested-dependent-keys-with-each': require('./rules/no-deeply-nested-dependent-keys-with-each'),
     'no-duplicate-dependent-keys': require('./rules/no-duplicate-dependent-keys'),
     'no-ember-super-in-es-classes': require('./rules/no-ember-super-in-es-classes'),
     'no-ember-testing-in-module-scope': require('./rules/no-ember-testing-in-module-scope'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 73b42caa..5580a1f6 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -17,6 +17,7 @@ module.exports = {
   "ember/no-attrs-in-components": "error",
   "ember/no-attrs-snapshot": "error",
   "ember/no-capital-letters-in-routes": "error",
+  "ember/no-deeply-nested-dependent-keys-with-each": "off",
   "ember/no-duplicate-dependent-keys": "error",
   "ember/no-ember-super-in-es-classes": "off",
   "ember/no-ember-testing-in-module-scope": "error",
@@ -39,4 +40,4 @@ module.exports = {
   "ember/routes-segments-snake-case": "error",
   "ember/use-brace-expansion": "error",
   "ember/use-ember-get-and-set": "off"
-}
\ No newline at end of file
+}
diff --git a/lib/rules/no-deeply-nested-dependent-keys-with-each.js b/lib/rules/no-deeply-nested-dependent-keys-with-each.js
new file mode 100644
index 00000000..6ba91302
--- /dev/null
+++ b/lib/rules/no-deeply-nested-dependent-keys-with-each.js
@@ -0,0 +1,43 @@
+'use strict';
+
+const emberUtils = require('../utils/ember');
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+const ERROR_MESSAGE = 'Dependent keys containing `@each` only work one level deep. You cannot use nested forms like: `todos.@each.owner.name`. Please create an intermediary computed property instead.';
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Disallows usage of deeply-nested computed property dependent keys with `@each`.',
+      category: 'Possible Errors',
+      recommended: false
+    },
+    fixable: null,
+    ERROR_MESSAGE
+  },
+
+  create(context) {
+    return {
+      CallExpression(node) {
+        if (emberUtils.isComputedProp(node)) {
+          emberUtils.parseDependentKeys(node).forEach((key) => {
+            const parts = key.split('.');
+            const indexOfAtEach = parts.indexOf('@each');
+            if (indexOfAtEach < 0) {
+              return;
+            }
+            if (parts.length > indexOfAtEach + 2) {
+              context.report({
+                node,
+                message: ERROR_MESSAGE
+              });
+            }
+          });
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/no-deeply-nested-dependent-keys-with-each.js b/tests/lib/rules/no-deeply-nested-dependent-keys-with-each.js
new file mode 100644
index 00000000..8fdd5d8c
--- /dev/null
+++ b/tests/lib/rules/no-deeply-nested-dependent-keys-with-each.js
@@ -0,0 +1,46 @@
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deeply-nested-dependent-keys-with-each');
+const RuleTester = require('eslint').RuleTester;
+
+const { ERROR_MESSAGE } = rule;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester();
+ruleTester.run('no-deeply-nested-dependent-keys-with-each', rule, {
+  valid: [
+    "Ember.computed('foo.bar', function() {})",
+    "Ember.computed('foo.bar.@each.baz', function() {})",
+    "Ember.computed('foo.@each.bar', function() {})",
+    "Ember.computed('foo.@each.{bar,baz}', function() {})",
+    "computed('foo.bar', function() {})",
+    "computed('foo.bar.@each.baz', function() {})",
+    "computed('foo.@each.bar', function() {})",
+    "computed('foo.@each.{bar,baz}', function() {})",
+    "computed('foo.@each.{bar,baz}', function() {})",
+
+    // Not Ember's `computed`:
+    "otherClass.computed('foo.@each.bar.baz', function() {})",
+    "otherClass.myFunction('foo.@each.bar.baz', function() {})",
+    "myFunction('foo.@each.bar.baz', function() {})"
+  ],
+  invalid: [
+    {
+      code: "Ember.computed('foo.@each.bar.baz', function() {})",
+      errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }]
+    },
+    {
+      code: "Ember.computed('foo.@each.bar.[]', function() {})",
+      errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }]
+    },
+    {
+      code: "Ember.computed('foo.@each.bar.@each.baz', function() {})",
+      errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }]
+    }
+  ]
+});