Repository: ember-cli/eslint-plugin-ember
Pull request #122 (2017-08-07 07:53:18)

Add new "call-super-in-init" rule
As discussed in #82, I've added a new rule to make sure `_super` is called whenever the `init` lifecycle hook is being overridden.


Patch:
diff --git a/README.md b/README.md
index 7bc779ab..71f7865c 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [no-observers](./docs/rules/no-observers.md) | Prevents usage of observers |
 | :wrench: | [no-old-shims](./docs/rules/no-old-shims.md) | Prevents usage of old shims for modules |
 | :white_check_mark: | [no-side-effects](./docs/rules/no-side-effects.md) | Warns about unexpected side effects in computed properties |
+|  | [require-super-in-init](./docs/rules/require-super-in-init.md) | Enforces super calls in init hooks |
 | :white_check_mark: | [use-brace-expansion](./docs/rules/use-brace-expansion.md) | Enforces usage of brace expansion |
 | :white_check_mark: :wrench: | [use-ember-get-and-set](./docs/rules/use-ember-get-and-set.md) | Enforces usage of Ember.get and Ember.set |
 
diff --git a/docs/rules/require-super-in-init.md b/docs/rules/require-super-in-init.md
new file mode 100644
index 00000000..208eeb16
--- /dev/null
+++ b/docs/rules/require-super-in-init.md
@@ -0,0 +1,24 @@
+## Call _super in init lifecycle hooks
+
+### Rule name: `require-super-in-init`
+
+When overriding the `init` lifecycle hook inside Ember Components, Controllers, Routes or Mixins, it is necessary to include a call to `_super`.
+
+```javascript
+// BAD
+export default Ember.Component.extend({
+  init() {
+    this.set('items', []);
+  },
+});
+```
+
+```javascript
+// GOOD
+export default Ember.Component.extend({
+  init() {
+    this._super(...arguments);
+    this.set('items', []);
+  },
+});
+```
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 473319cf..0fefaee3 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -25,6 +25,7 @@ module.exports = {
   "ember/order-in-controllers": "error",
   "ember/order-in-models": "error",
   "ember/order-in-routes": "error",
+  "ember/require-super-in-init": "off",
   "ember/routes-segments-snake-case": "error",
   "ember/use-brace-expansion": "error",
   "ember/use-ember-get-and-set": "error"
diff --git a/lib/rules/require-super-in-init.js b/lib/rules/require-super-in-init.js
new file mode 100644
index 00000000..7270fe43
--- /dev/null
+++ b/lib/rules/require-super-in-init.js
@@ -0,0 +1,56 @@
+'use strict';
+
+const ember = require('../utils/ember');
+const utils = require('../utils/utils');
+
+//----------------------------------------------
+// General rule - Call _super in init lifecycle hooks
+//----------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforces super calls in init hooks',
+      category: 'General',
+      recommended: false
+    },
+    fixable: null, // or "code" or "whitespace"
+  },
+
+  create(context) {
+    const message = 'Call this._super(...arguments) in init hook';
+
+    const filePath = context.getFilename();
+
+    const report = function (node) {
+      context.report(node, message);
+    };
+
+    return {
+      CallExpression(node) {
+        if (!ember.isEmberComponent(node, filePath) &&
+            !ember.isEmberController(node, filePath) &&
+            !ember.isEmberRoute(node, filePath) &&
+            !ember.isEmberMixin(node, filePath)) return;
+
+        const initProperty = ember.getModuleProperties(node).find(property => property.key.name === 'init');
+
+        if (initProperty) {
+          const initPropertyBody = initProperty.value.body.body;
+          const fnExpressions = utils.findNodes(initPropertyBody, 'ExpressionStatement');
+
+          const hasSuper = fnExpressions.some((fnExpression) => {
+            if (!utils.isCallExpression(fnExpression.expression)) return false;
+            const fnCallee = fnExpression.expression.callee;
+            return utils.isMemberExpression(fnCallee) &&
+              utils.isThisExpression(fnCallee.object) &&
+              utils.isIdentifier(fnCallee.property) &&
+              fnCallee.property.name === '_super';
+          });
+
+          if (!hasSuper) { report(initProperty); }
+        }
+      },
+    };
+  }
+};
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index 1fa48077..77f6c5bc 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -11,6 +11,7 @@ module.exports = {
   isEmberComponent,
   isEmberController,
   isEmberRoute,
+  isEmberMixin,
 
   isSingleLineFn,
   isMultiLineFn,
@@ -120,6 +121,9 @@ function isEmberController(node, filePath) {
 function isEmberRoute(node, filePath) {
   return isEmberCoreModule(node, 'Route', filePath);
 }
+function isEmberMixin(node, filePath) {
+  return isEmberCoreModule(node, 'Mixin', filePath);
+}
 
 function isInjectedServiceProp(node) {
   return isPropOfType(node, 'service') || isPropOfType(node, 'inject');


Test patch:
diff --git a/tests/lib/rules/require-super-in-init.js b/tests/lib/rules/require-super-in-init.js
new file mode 100644
index 00000000..d7f1295e
--- /dev/null
+++ b/tests/lib/rules/require-super-in-init.js
@@ -0,0 +1,180 @@
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-super-in-init');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const eslintTester = new RuleTester();
+const message = 'Call this._super(...arguments) in init hook';
+
+eslintTester.run('require-super-in-init', rule, {
+  valid: [
+    {
+      code: 'export default Component.extend();',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'export default Route.extend();',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'export default Controller.extend();',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'export default Mixin.extend();',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Component({
+        init() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Route({
+        init() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Controller({
+        init() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Mixin({
+        init() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Component({
+        init: function() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+  ],
+  invalid: [
+    {
+      code: `export default Component.extend({
+        init() {},
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Component.extend({
+        init() {
+          this.set('prop', 'value');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Component.extend({
+        init() {
+          this.set('prop', 'value');
+          this.set('prop2', 'value2');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Route.extend({
+        init() {},
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Route.extend({
+        init() {
+          this.set('prop', 'value');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Route.extend({
+        init() {
+          this.set('prop', 'value');
+          this.set('prop2', 'value2');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Controller.extend({
+        init() {},
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Controller.extend({
+        init() {
+          this.set('prop', 'value');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Controller.extend({
+        init() {
+          this.set('prop', 'value');
+          this.set('prop2', 'value2');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {},
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {
+          this.set('prop', 'value');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {
+          this.set('prop', 'value');
+          this.set('prop2', 'value2');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+  ],
+});