Repository: ember-cli/eslint-plugin-ember
Pull request #555 (2019-10-25 10:01:23)

Add new `no-actions-hash` rule
Fixes https://github.com/ember-cli/eslint-plugin-ember/issues/538

bmish: Planning to release a new version as soon as we get this rule merged :) 
laurmurclar: @bmish sweet, I'll try get it done today


Patch:
diff --git a/README.md b/README.md
index 1271e1a7..10afcad7 100644
--- a/README.md
+++ b/README.md
@@ -142,6 +142,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 
 |    | Rule ID | Description |
 |:---|:--------|:------------|
+|  | [no-actions-hash](./docs/rules/no-actions-hash.md) | Disallows the actions hash in components, controllers and routes |
 |  | [no-classic-classes](./docs/rules/no-classic-classes.md) | Disallow "classic" classes in favor of native JS classes |
 |  | [no-classic-components](./docs/rules/no-classic-components.md) | Enforces Glimmer components |
 |  | [no-computed-properties-in-native-classes](./docs/rules/no-computed-properties-in-native-classes.md) | Disallows using computed properties in native classes |
diff --git a/docs/rules/no-actions-hash.md b/docs/rules/no-actions-hash.md
new file mode 100644
index 00000000..c10908b7
--- /dev/null
+++ b/docs/rules/no-actions-hash.md
@@ -0,0 +1,60 @@
+# Disallows the actions hash in components and controllers (no-actions-hash)
+
+Ember Octane includes a rethink of event handling in Ember. The `actions` hash and `{{action}}` modifier and helper are no longer needed. To provide the correct context to functions (binding), you should now use the `@action` decorator. In templates, the `{{on}}` modifier can be used to set up event handlers and the `{{fn}}` helper can be used for partial application.
+
+
+## Rule Detail
+
+Use the `@action` decorator or `foo: action(function() {}))` syntax instead of an `actions` hash.
+
+Examples of **incorrect** code for this rule:
+
+```js
+// Bad, with classic class
+import Component from '@ember/component';
+
+export default Component.extend({
+  actions: {
+    foo() {
+    }
+  },
+});
+
+// Bad, with native class
+import Component from '@ember/component';
+
+export class MyComponent extends Component {
+  actions = {
+    foo() {
+    }
+  }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+// Good, with classic class
+import Component from '@ember/component';
+import { action } from '@ember/object';
+
+export default Component.extend({
+  foo: action(function() {
+  })
+});
+
+// Good, with native class
+import Component from '@ember/component';
+import { action } from '@ember/object';
+
+export class MyComponent extends Component {
+  @action
+  foo() {
+  }
+}
+```
+
+## Further Reading
+- [`{{on}}` Modifier RFC](https://github.com/emberjs/rfcs/pull/471)
+- [`{{fn}}` Helper RFC](https://github.com/emberjs/rfcs/pull/470)
+- [Ember Octane Update: What's up with `@action`?](https://www.pzuraq.com/ember-octane-update-action/)
diff --git a/lib/index.js b/lib/index.js
index 1b932a8b..18c5fcc6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -12,6 +12,7 @@ module.exports = {
     'jquery-ember-run': require('./rules/jquery-ember-run'),
     'named-functions-in-promises': require('./rules/named-functions-in-promises'),
     'new-module-imports': require('./rules/new-module-imports'),
+    'no-actions-hash': require('./rules/no-actions-hash'),
     'no-arrow-function-computed-properties': require('./rules/no-arrow-function-computed-properties'),
     'no-attrs-in-components': require('./rules/no-attrs-in-components'),
     'no-attrs-snapshot': require('./rules/no-attrs-snapshot'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 82b38c2c..3d339ec1 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -15,6 +15,7 @@ module.exports = {
   "ember/jquery-ember-run": "error",
   "ember/named-functions-in-promises": "off",
   "ember/new-module-imports": "error",
+  "ember/no-actions-hash": "off",
   "ember/no-arrow-function-computed-properties": "error",
   "ember/no-attrs-in-components": "error",
   "ember/no-attrs-snapshot": "error",
diff --git a/lib/rules/no-actions-hash.js b/lib/rules/no-actions-hash.js
new file mode 100644
index 00000000..5c44276a
--- /dev/null
+++ b/lib/rules/no-actions-hash.js
@@ -0,0 +1,48 @@
+const ember = require('../utils/ember');
+const utils = require('../utils/utils');
+
+const ERROR_MESSAGE = 'Use the @action decorator instead of declaring an actions hash';
+
+module.exports = {
+  ERROR_MESSAGE,
+  meta: {
+    docs: {
+      description: 'Disallows the actions hash in components, controllers and routes',
+      category: 'Ember Octane',
+      recommended: false,
+    },
+    fixable: null, // or "code" or "whitespace"
+    url:
+      'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-actions-hash.md',
+  },
+
+  create: context => {
+    function inClassWhichCanContainActions(context, node) {
+      return (
+        ember.isEmberComponent(context, node) ||
+        ember.isEmberController(context, node) ||
+        ember.isEmberRoute(context, node)
+      );
+    }
+
+    function reportActionsProp(properties) {
+      const actionsProp = properties.find(property => ember.isActionsProp(property));
+      if (actionsProp) {
+        context.report(actionsProp, ERROR_MESSAGE);
+      }
+    }
+
+    return {
+      ClassDeclaration(node) {
+        if (inClassWhichCanContainActions(context, node)) {
+          reportActionsProp(utils.findNodes(node.body.body, 'ClassProperty'));
+        }
+      },
+      CallExpression(node) {
+        if (inClassWhichCanContainActions(context, node)) {
+          reportActionsProp(ember.getModuleProperties(node));
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/no-actions-hash.js b/tests/lib/rules/no-actions-hash.js
new file mode 100644
index 00000000..eece3e3c
--- /dev/null
+++ b/tests/lib/rules/no-actions-hash.js
@@ -0,0 +1,134 @@
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-actions-hash');
+const RuleTester = require('eslint').RuleTester;
+
+const { ERROR_MESSAGE } = rule;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('babel-eslint'),
+  parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+});
+ruleTester.run('no-actions-hash', rule, {
+  valid: [
+    `
+      export default Component.extend({
+        foo: action(function() {})
+      });
+    `,
+    `
+      export default class MyComponent extends Component {
+        @action
+        foo() {}
+      }
+    `,
+    `
+      export default Controller.extend({
+        foo: action(function() {})
+      });
+    `,
+    `
+      export default class MyController extends Controller {
+        @action
+        foo() {}
+      }
+    `,
+    `
+      export default Route.extend({
+        foo: action(function() {})
+      });
+    `,
+    `
+      export default class MyRoute extends Route {
+        @action
+        foo() {}
+      }
+    `,
+    `
+      export default class MyLovelyClass extends LovelyClass {
+        actions = {
+          foo() {
+          }
+        }
+      }
+    `,
+    `
+      export default Service.extend({
+        actions: {
+        },
+      });
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `
+        export default Component.extend({
+          actions: {
+          },
+        });
+      `,
+      errors: [{ type: 'Property', message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import Component from '@ember/component';
+        export default class MyComponent extends Component {
+          actions = {
+            foo() {
+            }
+          }
+        }
+      `,
+      errors: [{ type: 'ClassProperty', message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        export default Controller.extend({
+          actions: {
+          },
+        });
+      `,
+      errors: [{ type: 'Property', message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import Controller from '@ember/controller';
+        export default class MyController extends Controller {
+          actions = {
+            foo() {
+            }
+          }
+        }
+      `,
+      errors: [{ type: 'ClassProperty', message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        export default Route.extend({
+          actions: {
+          },
+        });
+      `,
+      errors: [{ type: 'Property', message: ERROR_MESSAGE }],
+    },
+    {
+      code: `
+        import Route from '@ember/routing/route';
+        export default class MyRoute extends Route {
+          actions = {
+            foo() {
+            }
+          }
+        }
+      `,
+      errors: [{ type: 'ClassProperty', message: ERROR_MESSAGE }],
+    },
+  ],
+});