Repository: ember-cli/eslint-plugin-ember
Pull request #198 (2017-12-28 06:28:18)

Add new scenarios for `require-super-in-init` rule
This PR fixes issues #193 and #162 


Patch:
diff --git a/lib/rules/require-super-in-init.js b/lib/rules/require-super-in-init.js
index 8b0f6670..be3a8f9a 100644
--- a/lib/rules/require-super-in-init.js
+++ b/lib/rules/require-super-in-init.js
@@ -3,6 +3,58 @@
 const ember = require('../utils/ember');
 const utils = require('../utils/utils');
 
+/**
+ * Locates nodes with either an ExpressionStatement or ReturnStatement
+ * given name.
+ * @param {Array} nodeBody Array of nodes.
+ * @returns {Array} Array of nodes with given names.
+ */
+function findStmtNodes(nodeBody) {
+  const nodes = [];
+  const fnExpressions = utils.findNodes(nodeBody, 'ExpressionStatement');
+  const returnStatement = utils.findNodes(nodeBody, 'ReturnStatement');
+
+  if (fnExpressions.length !== 0) {
+    fnExpressions.forEach((item) => {
+      nodes.push(item);
+    });
+  }
+
+  if (returnStatement.length !== 0) {
+    returnStatement.forEach((item) => {
+      nodes.push(item);
+    });
+  }
+
+  return nodes;
+}
+
+/**
+ * Checks whether a node has the '_super' property.
+ * @param {Array} nodes An array of nodes.
+ * @returns {Boolean}
+ */
+function checkForSuper(nodes) {
+  if (nodes.length === 0) return false;
+
+  return nodes.some((n) => {
+    if (utils.isCallExpression(n.expression)) {
+      const fnCallee = n.expression.callee;
+      return utils.isMemberExpression(fnCallee) &&
+        utils.isThisExpression(fnCallee.object) &&
+        utils.isIdentifier(fnCallee.property) &&
+        fnCallee.property.name === '_super';
+    } else if (utils.isReturnStatement(n)) {
+      if (!n.argument || !utils.isCallExpression(n.argument)) return false;
+
+      const fnCallee = n.argument.callee;
+      return fnCallee.property.name === '_super';
+    }
+
+    return false;
+  });
+}
+
 //----------------------------------------------
 // General rule - Call _super in init lifecycle hooks
 //----------------------------------------------
@@ -31,23 +83,15 @@ module.exports = {
         if (!ember.isEmberComponent(node, filePath) &&
             !ember.isEmberController(node, filePath) &&
             !ember.isEmberRoute(node, filePath) &&
-            !ember.isEmberMixin(node, filePath)) return;
+            !ember.isEmberMixin(node, filePath) &&
+            !ember.isEmberService(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';
-          });
-
+          const nodes = findStmtNodes(initPropertyBody);
+          const hasSuper = checkForSuper(nodes);
           if (!hasSuper) { report(initProperty); }
         }
       },
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index 8fa0c448..e0140e88 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -10,8 +10,9 @@ module.exports = {
   isEmberCoreModule,
   isEmberComponent,
   isEmberController,
-  isEmberRoute,
   isEmberMixin,
+  isEmberRoute,
+  isEmberService,
 
   isSingleLineFn,
   isMultiLineFn,
@@ -143,11 +144,16 @@ function isEmberController(node, filePath) {
   return isEmberCoreModule(node, 'Controller', filePath);
 }
 
+function isEmberMixin(node, filePath) {
+  return isEmberCoreModule(node, 'Mixin', filePath);
+}
+
 function isEmberRoute(node, filePath) {
   return isEmberCoreModule(node, 'Route', filePath);
 }
-function isEmberMixin(node, filePath) {
-  return isEmberCoreModule(node, 'Mixin', filePath);
+
+function isEmberService(node, filePath) {
+  return isEmberCoreModule(node, 'Service', filePath);
 }
 
 function isInjectedServiceProp(node) {
diff --git a/lib/utils/utils.js b/lib/utils/utils.js
index b4c8d21e..e3ebaf61 100644
--- a/lib/utils/utils.js
+++ b/lib/utils/utils.js
@@ -28,6 +28,7 @@ module.exports = {
   collectObjectPatternBindings,
   isEmptyMethod,
   isBinaryExpression,
+  isReturnStatement
 };
 
 /**
@@ -196,6 +197,16 @@ function isCallWithFunctionExpression(node) {
     firstArg && isFunctionExpression(firstArg);
 }
 
+/**
+ * Check whether or not a node is a ReturnStatement
+ *
+ * @param {Object} node The node to check.
+ * @return {Boolean} Whether or not the node is a ReturnStatement.
+ */
+function isReturnStatement(node) {
+  return node !== undefined && node.type && node.type === 'ReturnStatement';
+}
+
 /**
  * Check whether or not a node is an ThisExpression.
  *


Test patch:
diff --git a/tests/lib/rules/require-super-in-init.js b/tests/lib/rules/require-super-in-init.js
index d7f1295e..7b1e138b 100644
--- a/tests/lib/rules/require-super-in-init.js
+++ b/tests/lib/rules/require-super-in-init.js
@@ -14,6 +14,86 @@ const message = 'Call this._super(...arguments) in init hook';
 
 eslintTester.run('require-super-in-init', rule, {
   valid: [
+    {
+      code: `export default Component.extend({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Route.extend({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Controller.extend({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Service.extend({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Component({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Route({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Controller({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Mixin({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      code: `export default Service({
+        init() {
+          return this._super(...arguments);
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
     {
       code: 'export default Component.extend();',
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
@@ -30,6 +110,10 @@ eslintTester.run('require-super-in-init', rule, {
       code: 'export default Mixin.extend();',
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
     },
+    {
+      code: 'export default Service.extend();',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
     {
       code: `export default Component({
         init() {
@@ -62,6 +146,14 @@ eslintTester.run('require-super-in-init', rule, {
       });`,
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
     },
+    {
+      code: `export default Service({
+        init() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
     {
       code: `export default Component({
         init: function() {
@@ -70,6 +162,38 @@ eslintTester.run('require-super-in-init', rule, {
       });`,
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
     },
+    {
+      code: `export default Route({
+        init: function() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Controller({
+        init: function() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Mixin({
+        init: function() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: `export default Service({
+        init: function() {
+          this._super(...arguments);
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    }
   ],
   invalid: [
     {
@@ -176,5 +300,211 @@ eslintTester.run('require-super-in-init', rule, {
       parserOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [{ message, line: 2 }],
     },
+    {
+      code: `export default Service.extend({
+        init() {},
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Service.extend({
+        init() {
+          this.set('prop', 'value');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Service.extend({
+        init() {
+          this.set('prop', 'value');
+          this.set('prop2', 'value2');
+        },
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }],
+    },
+    {
+      code: `export default Component.extend({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Route.extend({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Controller.extend({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Service.extend({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Component({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Route({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Controller({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Mixin({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Service({
+        init() {
+          return;
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Component.extend({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Route.extend({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Controller.extend({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Mixin.extend({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Service.extend({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Component({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Route({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Controller({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Mixin({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    },
+    {
+      code: `export default Service({
+        init() {
+          return 'meh';
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{ message, line: 2 }]
+    }
   ],
 });
diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js
index 0ec9568c..a5d49ac3 100644
--- a/tests/lib/utils/ember-test.js
+++ b/tests/lib/utils/ember-test.js
@@ -222,6 +222,39 @@ describe('isEmberRoute', () => {
   });
 });
 
+describe('isEmberService', () => {
+  it('should check if it\'s an Ember Service', () => {
+    let node;
+
+    node = parse('Ember.Service.extend()');
+    expect(
+      emberUtils.isEmberService(node),
+      'it should detect Service when using Ember.Service'
+    ).toBeTruthy();
+
+    node = parse('Service.extend()');
+    expect(
+      emberUtils.isEmberService(node),
+      'it should detect Service when using local module Service'
+    ).toBeTruthy();
+  });
+
+  it('should check if it\'s an Ember Service even if it uses custom name', () => {
+    const node = parse('CustomService.extend()');
+    const filePath = 'example-app/services/path/to/some-feature.js';
+
+    expect(
+      emberUtils.isEmberService(node),
+      'it shouldn\'t detect Service when no file path is proviced'
+    ).toBeFalsy();
+
+    expect(
+      emberUtils.isEmberService(node, filePath),
+      'it should detect Service when file path is provided'
+    ).toBeTruthy();
+  });
+});
+
 describe('isInjectedServiceProp', () => {
   let node;
 
diff --git a/tests/lib/utils/utils-test.js b/tests/lib/utils/utils-test.js
index fc59c1ac..d6486b04 100644
--- a/tests/lib/utils/utils-test.js
+++ b/tests/lib/utils/utils-test.js
@@ -134,6 +134,14 @@ describe('isCallWithFunctionExpression', () => {
   });
 });
 
+describe('isReturnStatement', () => {
+  const node = babelEslint.parse('return').body[0];
+
+  it('should check if node is a return statement', () => {
+    expect(utils.isReturnStatement(node)).toBeTruthy();
+  });
+});
+
 describe('isThisExpression', () => {
   const node = parse('this');