Repository: ember-cli/eslint-plugin-ember
Pull request #558 (2019-10-28 12:40:56)

Update `isEmberCoreModule` util function to also handle native classes (in addition to classic classes)
The utils for checking whether a node is an ember core module (eg. Component, Controller, etc) currently only work for `CallExpressions` ie. "classic" classes. This PR adds functionality which also make them compatible with `ClassDeclaration`s (as used in native classes).

https://github.com/ember-cli/eslint-plugin-ember/pull/555 depends on this functionality

bmish: Can you add tests for at least one of our rules to ensure that the rule now correctly handles native classes?
bmish: Should any existing rules be updated to handle native classes? If so, we should create a ticket to track that. For example, `no-new-mixins` presumably should apply to both class/native classes.
laurmurclar: @bmish probably, but I honestly don't yet have enough context to know which. A tracking issue sounds like a good idea, but I'm going to need help writing it up 😄 

Patch:
diff --git a/lib/rules/alias-model-in-controller.js b/lib/rules/alias-model-in-controller.js
index 5ad61cd9..d383e5c0 100644
--- a/lib/rules/alias-model-in-controller.js
+++ b/lib/rules/alias-model-in-controller.js
@@ -26,11 +26,9 @@ module.exports = {
       context.report(node, message);
     };
 
-    const filePath = context.getFilename();
-
     return {
       CallExpression(node) {
-        if (!ember.isEmberController(node, filePath)) {
+        if (!ember.isEmberController(context, node)) {
           return;
         }
 
diff --git a/lib/rules/avoid-using-needs-in-controllers.js b/lib/rules/avoid-using-needs-in-controllers.js
index 66bd8c4c..5e8fb628 100644
--- a/lib/rules/avoid-using-needs-in-controllers.js
+++ b/lib/rules/avoid-using-needs-in-controllers.js
@@ -22,13 +22,12 @@ module.exports = {
         '`needs` API has been deprecated, `Ember.inject.controller` should be used instead';
       context.report(node, message);
     };
-    const filePath = context.getFilename();
 
     return {
       CallExpression(node) {
         const isReopenNode = ember.isReopenObject(node) || ember.isReopenClassObject(node);
 
-        if (!ember.isEmberController(node, filePath) && !isReopenNode) {
+        if (!ember.isEmberController(context, node) && !isReopenNode) {
           return;
         }
 
diff --git a/lib/rules/no-new-mixins.js b/lib/rules/no-new-mixins.js
index 93d7f416..e4bbd4f0 100644
--- a/lib/rules/no-new-mixins.js
+++ b/lib/rules/no-new-mixins.js
@@ -21,11 +21,9 @@ module.exports = {
   ERROR_MESSAGE,
 
   create(context) {
-    const filePath = context.getFilename();
-
     return {
       CallExpression(node) {
-        if (ember.isEmberMixin(node, filePath)) {
+        if (ember.isEmberMixin(context, node)) {
           context.report(node, ERROR_MESSAGE);
         }
       },
diff --git a/lib/rules/no-on-calls-in-components.js b/lib/rules/no-on-calls-in-components.js
index 5067cdfb..e79f31be 100644
--- a/lib/rules/no-on-calls-in-components.js
+++ b/lib/rules/no-on-calls-in-components.js
@@ -36,8 +36,6 @@ module.exports = {
       'didDestroyElement',
     ];
 
-    const filePath = context.getFilename();
-
     const isOnCall = function(node) {
       if (!node.value) {
         return false;
@@ -64,7 +62,7 @@ module.exports = {
 
     return {
       CallExpression(node) {
-        if (!ember.isEmberComponent(node, filePath)) {
+        if (!ember.isEmberComponent(context, node)) {
           return;
         }
 
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index 227514c4..87ac04ad 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -70,11 +70,9 @@ module.exports = {
       ]);
     }
 
-    const filePath = context.getFilename();
-
     return {
       CallExpression(node) {
-        if (!ember.isEmberComponent(node, filePath)) {
+        if (!ember.isEmberComponent(context, node)) {
           return;
         }
 
diff --git a/lib/rules/order-in-controllers.js b/lib/rules/order-in-controllers.js
index faa08e8f..ce9d7c87 100644
--- a/lib/rules/order-in-controllers.js
+++ b/lib/rules/order-in-controllers.js
@@ -38,14 +38,13 @@ module.exports = {
 
   create(context) {
     const options = context.options[0] || {};
-    const filePath = context.getFilename();
     const order = options.order
       ? addBackwardsPosition(options.order, 'empty-method', 'method')
       : ORDER;
 
     return {
       CallExpression(node) {
-        if (!ember.isEmberController(node, filePath)) {
+        if (!ember.isEmberController(context, node)) {
           return;
         }
 
diff --git a/lib/rules/order-in-routes.js b/lib/rules/order-in-routes.js
index 41cca300..7667cf3d 100644
--- a/lib/rules/order-in-routes.js
+++ b/lib/rules/order-in-routes.js
@@ -65,11 +65,9 @@ module.exports = {
       ]);
     }
 
-    const filePath = context.getFilename();
-
     return {
       CallExpression(node) {
-        if (!ember.isEmberRoute(node, filePath)) {
+        if (!ember.isEmberRoute(context, node)) {
           return;
         }
 
diff --git a/lib/rules/require-super-in-init.js b/lib/rules/require-super-in-init.js
index 9dec2f99..e05a0c67 100644
--- a/lib/rules/require-super-in-init.js
+++ b/lib/rules/require-super-in-init.js
@@ -81,8 +81,6 @@ module.exports = {
   create(context) {
     const message = 'Call this._super(...arguments) in init hook';
 
-    const filePath = context.getFilename();
-
     const report = function(node) {
       context.report(node, message);
     };
@@ -90,11 +88,11 @@ module.exports = {
     return {
       CallExpression(node) {
         if (
-          !ember.isEmberComponent(node, filePath) &&
-          !ember.isEmberController(node, filePath) &&
-          !ember.isEmberRoute(node, filePath) &&
-          !ember.isEmberMixin(node, filePath) &&
-          !ember.isEmberService(node, filePath)
+          !ember.isEmberComponent(context, node) &&
+          !ember.isEmberController(context, node) &&
+          !ember.isEmberRoute(context, node) &&
+          !ember.isEmberMixin(context, node) &&
+          !ember.isEmberService(context, node)
         ) {
           return;
         }
diff --git a/lib/rules/require-tagless-components.js b/lib/rules/require-tagless-components.js
index a30d5269..d3f17e2a 100644
--- a/lib/rules/require-tagless-components.js
+++ b/lib/rules/require-tagless-components.js
@@ -99,14 +99,12 @@ module.exports = {
   },
 
   create(context) {
-    const filePath = context.getFilename();
-
     return {
       // Handle classic components
       'CallExpression > MemberExpression[property.name="extend"]'(node) {
         const callExpression = node.parent;
 
-        if (!isEmberComponent(callExpression, filePath)) {
+        if (!isEmberComponent(context, callExpression)) {
           return;
         }
 
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index 0a78549f..0433839f 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -2,6 +2,7 @@
 
 const utils = require('./utils');
 const types = require('./types');
+const assert = require('assert');
 
 module.exports = {
   isDSModel,
@@ -52,6 +53,24 @@ module.exports = {
 };
 
 // Private
+const CORE_MODULE_IMPORT_PATHS = {
+  Component: '@ember/component',
+  Controller: '@ember/controller',
+  Mixin: '@ember/object/mixin',
+  Route: '@ember/routing/route',
+  Service: '@ember/service',
+};
+
+function isClassicEmberCoreModule(node, module, filePath) {
+  const isExtended = isEmberObject(node);
+  let isModuleByPath;
+
+  if (filePath) {
+    isModuleByPath = isModuleByFilePath(filePath, module.toLowerCase()) && isExtended;
+  }
+
+  return isModule(node, module) || isModuleByPath;
+}
 
 function isLocalModule(callee, element) {
   return (
@@ -116,17 +135,6 @@ function isTestFile(fileName) {
   return fileName.endsWith('-test.js');
 }
 
-function isEmberCoreModule(node, module, filePath) {
-  const isExtended = isEmberObject(node);
-  let isModuleByPath;
-
-  if (filePath) {
-    isModuleByPath = isModuleByFilePath(filePath, module.toLowerCase()) && isExtended;
-  }
-
-  return isModule(node, module) || isModuleByPath;
-}
-
 function isEmberObject(node) {
   return (
     node.callee.property &&
@@ -142,24 +150,46 @@ function isReopenObject(node) {
   return node.callee.property && node.callee.property.name === 'reopen';
 }
 
-function isEmberComponent(node, filePath) {
-  return isEmberCoreModule(node, 'Component', filePath);
+function isEmberCoreModule(context, node, moduleName) {
+  if (types.isCallExpression(node)) {
+    // "classic" class pattern
+    return isClassicEmberCoreModule(node, moduleName, context.getFilename());
+  } else if (types.isClassDeclaration(node)) {
+    // native classes
+    if (!node.superClass) {
+      return false;
+    }
+    const superClassImportPath = utils.getSourceModuleNameForIdentifier(context, node.superClass);
+
+    if (superClassImportPath === CORE_MODULE_IMPORT_PATHS[moduleName]) {
+      return true;
+    }
+  } else {
+    assert(
+      'Function should only be called on a `CallExpression` (classic class) or `ClassDeclaration` (native class)'
+    );
+  }
+  return false;
+}
+
+function isEmberComponent(context, node) {
+  return isEmberCoreModule(context, node, 'Component');
 }
 
-function isEmberController(node, filePath) {
-  return isEmberCoreModule(node, 'Controller', filePath);
+function isEmberController(context, node) {
+  return isEmberCoreModule(context, node, 'Controller');
 }
 
-function isEmberMixin(node, filePath) {
-  return isEmberCoreModule(node, 'Mixin', filePath);
+function isEmberMixin(context, node) {
+  return isEmberCoreModule(context, node, 'Mixin');
 }
 
-function isEmberRoute(node, filePath) {
-  return isEmberCoreModule(node, 'Route', filePath);
+function isEmberRoute(context, node) {
+  return isEmberCoreModule(context, node, 'Route');
 }
 
-function isEmberService(node, filePath) {
-  return isEmberCoreModule(node, 'Service', filePath);
+function isEmberService(context, node) {
+  return isEmberCoreModule(context, node, 'Service');
 }
 
 function isInjectedServiceProp(node) {
diff --git a/lib/utils/types.js b/lib/utils/types.js
index da4c3083..61d6e753 100644
--- a/lib/utils/types.js
+++ b/lib/utils/types.js
@@ -7,6 +7,7 @@ module.exports = {
   isBinaryExpression,
   isCallExpression,
   isCallWithFunctionExpression,
+  isClassDeclaration,
   isConciseArrowFunctionWithCallExpression,
   isConditionalExpression,
   isExpressionStatement,
@@ -96,6 +97,16 @@ function isCallWithFunctionExpression(node) {
   );
 }
 
+/**
+ * Check whether or not a node is a ClassDeclaration.
+ *
+ * @param {Object} node The node to check.
+ * @returns {boolean} Whether or not the node is a ClassDeclaration.
+ */
+function isClassDeclaration(node) {
+  return node !== undefined && node.type === 'ClassDeclaration';
+}
+
 /**
  * Check whether or not a node is an ArrowFunctionExpression with concise body
  * that contains a call expression.
diff --git a/package.json b/package.json
index fedb2af8..b034dbae 100644
--- a/package.json
+++ b/package.json
@@ -80,6 +80,9 @@
     }
   },
   "jest": {
+    "testPathIgnorePatterns": [
+      "<rootDir>/tests/helpers/"
+    ],
     "testMatch": [
       "**/tests/**/*.js"
     ],


Test patch:
diff --git a/tests/helpers/faux-context.js b/tests/helpers/faux-context.js
new file mode 100644
index 00000000..dc1d825d
--- /dev/null
+++ b/tests/helpers/faux-context.js
@@ -0,0 +1,32 @@
+'use strict';
+
+const babelEslint = require('babel-eslint');
+
+/**
+ * Builds a fake ESLint context object that's enough to satisfy the contract
+ * expected by `getSourceModuleNameForIdentifier` and `isEmberCoreModule`
+ */
+class FauxContext {
+  constructor(code, filename) {
+    const { ast } = babelEslint.parseForESLint(code);
+
+    this.ast = ast;
+    this.filename = filename;
+  }
+
+  /**
+   * Does not build the full tree of "parents" for the identifier, but
+   * we only care about the first one; the Program node
+   */
+  getAncestors() {
+    return [this.ast];
+  }
+
+  getFilename() {
+    return this.filename;
+  }
+}
+
+module.exports = {
+  FauxContext,
+};
diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js
index caea4f1a..e341d6e8 100644
--- a/tests/lib/utils/ember-test.js
+++ b/tests/lib/utils/ember-test.js
@@ -2,6 +2,7 @@
 
 const babelEslint = require('babel-eslint');
 const emberUtils = require('../../../lib/utils/ember');
+const { FauxContext } = require('../../helpers/faux-context');
 
 function parse(code) {
   return babelEslint.parse(code).body[0].expression;
@@ -98,65 +99,116 @@ describe('isTestFile', () => {
 
 describe('isEmberCoreModule', () => {
   it('should check if current file is a component', () => {
-    const node = parse('CustomComponent.extend()');
-    const filePath = 'example-app/components/path/to/some-component.js';
-    expect(emberUtils.isEmberCoreModule(node, 'component', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'CustomComponent.extend()',
+      'example-app/components/path/to/some-component.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Component')).toBeTruthy();
   });
 
   it('should check if current file is a component', () => {
-    const node = parse('Component.extend()');
-    const filePath = 'example-app/some-twisted-path/some-component.js';
-    expect(emberUtils.isEmberCoreModule(node, 'component', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'Component.extend()',
+      'example-app/some-twisted-path/some-component.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Component')).toBeTruthy();
   });
 
   it('should check if current file is a controller', () => {
-    const node = parse('CustomController.extend()');
-    const filePath = 'example-app/controllers/path/to/some-controller.js';
-    expect(emberUtils.isEmberCoreModule(node, 'controller', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'CustomController.extend()',
+      'example-app/controllers/path/to/some-controller.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Controller')).toBeTruthy();
   });
 
   it('should check if current file is a controller', () => {
-    const node = parse('Controller.extend()');
-    const filePath = 'example-app/some-twisted-path/some-controller.js';
-    expect(emberUtils.isEmberCoreModule(node, 'controller', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'Controller.extend()',
+      'example-app/some-twisted-path/some-controller.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Controller')).toBeTruthy();
   });
 
   it('should check if current file is a route', () => {
-    const node = parse('CustomRoute.extend()');
-    const filePath = 'example-app/routes/path/to/some-route.js';
-    expect(emberUtils.isEmberCoreModule(node, 'route', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'CustomRoute.extend()',
+      'example-app/routes/path/to/some-route.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Route')).toBeTruthy();
   });
 
   it('should check if current file is a route', () => {
-    const node = parse('Route.extend()');
-    const filePath = 'example-app/some-twisted-path/some-route.js';
-    expect(emberUtils.isEmberCoreModule(node, 'route', filePath)).toBeTruthy();
+    const context = new FauxContext(
+      'Route.extend()',
+      'example-app/some-twisted-path/some-route.js'
+    );
+    const node = context.ast.body[0].expression;
+    expect(emberUtils.isEmberCoreModule(context, node, 'Route')).toBeTruthy();
   });
 });
 
 describe('isEmberComponent', () => {
   describe("should check if it's an Ember Component", () => {
     it('it should detect Component when using Ember.Component', () => {
-      const node = parse('Ember.Component.extend()');
-      expect(emberUtils.isEmberComponent(node)).toBeTruthy();
+      const context = new FauxContext('Ember.Component.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberComponent(context, node)).toBeTruthy();
     });
 
     it('it should detect Component when using local module Component', () => {
-      const node = parse('Component.extend()');
-      expect(emberUtils.isEmberComponent(node)).toBeTruthy();
+      const context = new FauxContext('Component.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberComponent(context, node)).toBeTruthy();
+    });
+
+    it('should detect Component when using native classes', () => {
+      const context = new FauxContext(`
+        import Component from '@ember/component';
+        class MyComponent extends Component {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberComponent(context, node)).toBeTruthy();
+    });
+
+    it('shouldnt detect Component when using native classes if the import path is incorrect', () => {
+      const context = new FauxContext(`
+        import Component from '@something-else/component';
+        class MyComponent extends Component {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberComponent(context, node)).toBeFalsy();
     });
   });
 
   describe("should check if it's an Ember Component even if it uses custom name", () => {
     it("it shouldn't detect Component when no file path is provided", () => {
-      const node = parse('CustomComponent.extend()');
-      expect(emberUtils.isEmberComponent(node)).toBeFalsy();
+      const context = new FauxContext('CustomComponent.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberComponent(context, node)).toBeFalsy();
     });
 
     it('it should detect Component when file path is provided', () => {
-      const node = parse('CustomComponent.extend()');
-      const filePath = 'example-app/components/path/to/some-component.js';
-      expect(emberUtils.isEmberComponent(node, filePath)).toBeTruthy();
+      const context = new FauxContext(
+        'CustomComponent.extend()',
+        'example-app/components/path/to/some-component.js'
+      );
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberComponent(context, node)).toBeTruthy();
+    });
+
+    it('should detect Component when using native classes if the import path is correct', () => {
+      const context = new FauxContext(`
+        import CustomComponent from '@ember/component';
+        class MyComponent extends CustomComponent {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberComponent(context, node)).toBeTruthy();
     });
   });
 });
@@ -164,26 +216,59 @@ describe('isEmberComponent', () => {
 describe('isEmberController', () => {
   describe("should check if it's an Ember Controller", () => {
     it('it should detect Controller when using Ember.Controller', () => {
-      const node = parse('Ember.Controller.extend()');
-      expect(emberUtils.isEmberController(node)).toBeTruthy();
+      const context = new FauxContext('Ember.Controller.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberController(context, node)).toBeTruthy();
     });
 
     it('it should detect Controller when using local module Controller', () => {
-      const node = parse('Controller.extend()');
-      expect(emberUtils.isEmberController(node)).toBeTruthy();
+      const context = new FauxContext('Controller.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberController(context, node)).toBeTruthy();
+    });
+
+    it('should detect Controller when using native classes', () => {
+      const context = new FauxContext(`
+        import Controller from '@ember/controller';
+        class MyController extends Controller {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberController(context, node)).toBeTruthy();
+    });
+
+    it('shouldnt detect Controller when using native classes if the import path is incorrect', () => {
+      const context = new FauxContext(`
+        import Controller from '@something-else/controller';
+        class MyController extends Controller {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberController(context, node)).toBeFalsy();
     });
   });
 
   describe("should check if it's an Ember Controller even if it uses custom name", () => {
     it("it shouldn't detect Controller when no file path is provided", () => {
-      const node = parse('CustomController.extend()');
-      expect(emberUtils.isEmberController(node)).toBeFalsy();
+      const context = new FauxContext('CustomController.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberController(context, node)).toBeFalsy();
     });
 
     it('it should detect Controller when file path is provided', () => {
-      const node = parse('CustomController.extend()');
-      const filePath = 'example-app/controllers/path/to/some-feature.js';
-      expect(emberUtils.isEmberController(node, filePath)).toBeTruthy();
+      const context = new FauxContext(
+        'CustomController.extend()',
+        'example-app/controllers/path/to/some-feature.js'
+      );
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberController(context, node)).toBeTruthy();
+    });
+
+    it('should detect Controller when using native classes if the import path is correct', () => {
+      const context = new FauxContext(`
+        import CustomController from '@ember/controller';
+        class MyController extends CustomController {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberController(context, node)).toBeTruthy();
     });
   });
 });
@@ -191,26 +276,93 @@ describe('isEmberController', () => {
 describe('isEmberRoute', () => {
   describe("should check if it's an Ember Route", () => {
     it('should detect Route when using Ember.Route', () => {
-      const node = parse('Ember.Route.extend()');
-      expect(emberUtils.isEmberRoute(node)).toBeTruthy();
+      const context = new FauxContext('Ember.Route.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberRoute(context, node)).toBeTruthy();
     });
 
     it('should detect Route when using local module Route', () => {
-      const node = parse('Route.extend()');
-      expect(emberUtils.isEmberRoute(node)).toBeTruthy();
+      const context = new FauxContext('Route.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberRoute(context, node)).toBeTruthy();
+    });
+
+    it('should detect Route when using native classes', () => {
+      const context = new FauxContext(`
+        import Route from '@ember/routing/route';
+        class MyRoute extends Route {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberRoute(context, node)).toBeTruthy();
+    });
+
+    it('shouldnt detect Route when using native classes if the import path is incorrect', () => {
+      const context = new FauxContext(`
+        import Route from '@something-else/routing/route';
+        class MyRoute extends Route {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberRoute(context, node)).toBeFalsy();
     });
   });
 
   describe("should check if it's an Ember Route even if it uses custom name", () => {
     it("it shouldn't detect Route when no file path is provided", () => {
-      const node = parse('CustomRoute.extend()');
-      expect(emberUtils.isEmberRoute(node)).toBeFalsy();
+      const context = new FauxContext('CustomRoute.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberRoute(context, node)).toBeFalsy();
     });
 
     it('it should detect Route when file path is provided', () => {
-      const node = parse('CustomRoute.extend()');
-      const filePath = 'example-app/routes/path/to/some-feature.js';
-      expect(emberUtils.isEmberRoute(node, filePath)).toBeTruthy();
+      const context = new FauxContext(
+        'CustomRoute.extend()',
+        'example-app/routes/path/to/some-feature.js'
+      );
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberRoute(context, node)).toBeTruthy();
+    });
+
+    it('should detect Route when using native classes if the import path is correct', () => {
+      const context = new FauxContext(`
+        import CustomRoute from '@ember/routing/route';
+        class MyRoute extends CustomRoute {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberRoute(context, node)).toBeTruthy();
+    });
+  });
+});
+
+describe('isEmberMixin', () => {
+  describe("should check if it's an Ember Mixin", () => {
+    it('should detect Mixin when using native classes', () => {
+      const context = new FauxContext(`
+        import Mixin from '@ember/object/mixin';
+        class MyMixin extends Mixin {}
+      `);
+
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberMixin(context, node)).toBeTruthy();
+    });
+
+    it('shouldnt detect Mixin when using native classes if the import path is incorrect', () => {
+      const context = new FauxContext(`
+        import Mixin from '@something-else/object/mixin';
+        class MyMixin extends Mixin {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberMixin(context, node)).toBeFalsy();
+    });
+  });
+
+  describe("should check if it's an Ember Mixin even if it uses custom name", () => {
+    it('should detect Mixin when using native classes if the import path is correct', () => {
+      const context = new FauxContext(`
+        import CustomMixin from '@ember/object/mixin';
+        class MyMixin extends CustomMixin {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberMixin(context, node)).toBeTruthy();
     });
   });
 });
@@ -218,26 +370,59 @@ describe('isEmberRoute', () => {
 describe('isEmberService', () => {
   describe("should check if it's an Ember Service", () => {
     it('should detect Service when using Ember.Service', () => {
-      const node = parse('Ember.Service.extend()');
-      expect(emberUtils.isEmberService(node)).toBeTruthy();
+      const context = new FauxContext('Ember.Service.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberService(context, node)).toBeTruthy();
     });
 
     it('should detect Service when using local module Service', () => {
-      const node = parse('Service.extend()');
-      expect(emberUtils.isEmberService(node)).toBeTruthy();
+      const context = new FauxContext('Service.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberService(context, node)).toBeTruthy();
+    });
+
+    it('should detect Service when using native classes', () => {
+      const context = new FauxContext(`
+        import Service from '@ember/service';
+        class MyService extends Service {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberService(context, node)).toBeTruthy();
+    });
+
+    it('shouldnt detect Service when using native classes if the import path is incorrect', () => {
+      const context = new FauxContext(`
+        import Service from '@something-else/service';
+        class MyService extends Service {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberService(context, node)).toBeFalsy();
     });
   });
 
   describe("should check if it's an Ember Service even if it uses custom name", () => {
     it("shouldn't detect Service when no file path is provided", () => {
-      const node = parse('CustomService.extend()');
-      expect(emberUtils.isEmberService(node)).toBeFalsy();
+      const context = new FauxContext('CustomService.extend()');
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberService(context, node)).toBeFalsy();
     });
 
     it('it should detect Service when file path is provided', () => {
-      const node = parse('CustomService.extend()');
-      const filePath = 'example-app/services/path/to/some-feature.js';
-      expect(emberUtils.isEmberService(node, filePath)).toBeTruthy();
+      const context = new FauxContext(
+        'CustomService.extend()',
+        'example-app/services/path/to/some-feature.js'
+      );
+      const node = context.ast.body[0].expression;
+      expect(emberUtils.isEmberService(context, node)).toBeTruthy();
+    });
+
+    it('should detect Service when using native classes if the import path is correct', () => {
+      const context = new FauxContext(`
+        import CustomService from '@ember/service';
+        class MyService extends CustomService {}
+      `);
+      const node = context.ast.body[1];
+      expect(emberUtils.isEmberService(context, node)).toBeTruthy();
     });
   });
 });
@@ -510,11 +695,6 @@ describe('isRelation', () => {
     }`);
     expect(emberUtils.isRelation(property)).toBeTruthy();
   });
-
-  it('should detect if given node is a route', () => {
-    const node = parse('this.route("lorem-ipsum")');
-    expect(emberUtils.isRoute(node)).toBeTruthy();
-  });
 });
 
 describe('parseDependentKeys', () => {
diff --git a/tests/lib/utils/utils/get-source-module-name-for-identifier-test.js b/tests/lib/utils/utils/get-source-module-name-for-identifier-test.js
index 3678556b..b8a3937c 100644
--- a/tests/lib/utils/utils/get-source-module-name-for-identifier-test.js
+++ b/tests/lib/utils/utils/get-source-module-name-for-identifier-test.js
@@ -1,27 +1,5 @@
-'use strict';
-
-const babelEslint = require('babel-eslint');
 const { getSourceModuleNameForIdentifier } = require('../../../../lib/utils/utils');
-
-/**
- * Builds a fake ESLint context object that's enough to satisfy the contract
- * expected by `getSourceModuleNameForIdentifier`
- */
-class FauxContext {
-  constructor(code) {
-    const { ast } = babelEslint.parseForESLint(code);
-
-    this.ast = ast;
-  }
-
-  /**
-   * Does not build the full tree of "parents" for the identifier, but
-   * we only care about the first one; the Program node
-   */
-  getAncestors() {
-    return [this.ast];
-  }
-}
+const { FauxContext } = require('../../../helpers/faux-context');
 
 test('when the identifier is not imported', () => {
   const context = new FauxContext(`