Repository: ember-cli/eslint-plugin-ember
Pull request #574 (2019-11-07 17:11:39)

Update `reportUnorderedProperties` util function to also work with native classes 
This is needed to unblock native class compatibility for the `order-in-*` rules, which we want to fix (see https://github.com/ember-cli/eslint-plugin-ember/issues/417)

The `reportUnorderedProperties` function relied on a bunch of utils which currently only work for "classic" classes. This PR adds functionality which also make them compatible with native classes.


Patch:
diff --git a/lib/rules/no-unnecessary-service-injection-argument.js b/lib/rules/no-unnecessary-service-injection-argument.js
index 920dce33..bef1b027 100644
--- a/lib/rules/no-unnecessary-service-injection-argument.js
+++ b/lib/rules/no-unnecessary-service-injection-argument.js
@@ -26,7 +26,7 @@ module.exports = {
     return {
       Property(node) {
         if (
-          !emberUtils.isInjectedServiceProp(node.value) ||
+          !emberUtils.isInjectedServiceProp(node) ||
           node.value.arguments.length !== 1 ||
           !types.isLiteral(node.value.arguments[0])
         ) {
diff --git a/lib/rules/require-computed-property-dependencies.js b/lib/rules/require-computed-property-dependencies.js
index bd88dba4..028ea4e3 100644
--- a/lib/rules/require-computed-property-dependencies.js
+++ b/lib/rules/require-computed-property-dependencies.js
@@ -192,7 +192,7 @@ function findInjectedServiceNames(node) {
     enter(child) {
       if (
         types.isProperty(child) &&
-        emberUtils.isInjectedServiceProp(child.value) &&
+        emberUtils.isInjectedServiceProp(child) &&
         types.isIdentifier(child.key)
       ) {
         results.push(child.key.name);
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index 0433839f..4c3b8db5 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -90,15 +90,24 @@ function isEmberModule(callee, element, module) {
 }
 
 function isPropOfType(node, type) {
-  const calleeNode = node.callee;
-
-  return (
-    types.isCallExpression(node) &&
-    ((types.isIdentifier(calleeNode) && calleeNode.name === type) ||
+  if (types.isProperty(node) && types.isCallExpression(node.value)) {
+    const calleeNode = node.value.callee;
+    return (
+      (types.isIdentifier(calleeNode) && calleeNode.name === type) ||
       (types.isMemberExpression(calleeNode) &&
         types.isIdentifier(calleeNode.property) &&
-        calleeNode.property.name === type))
-  );
+        calleeNode.property.name === type)
+    );
+  } else if ((types.isClassProperty(node) || types.isMethodDefinition(node)) && node.decorators) {
+    return node.decorators.some(decorator => {
+      const expression = decorator.expression;
+      return (
+        (types.isIdentifier(expression) && expression.name === type) ||
+        (types.isCallExpression(expression) && expression.callee.name === type)
+      );
+    });
+  }
+  return false;
 }
 
 // Public
@@ -195,6 +204,7 @@ function isEmberService(context, node) {
 function isInjectedServiceProp(node) {
   return isPropOfType(node, 'service') || isPropOfType(node, 'inject');
 }
+
 function isInjectedControllerProp(node) {
   return isPropOfType(node, 'controller');
 }
@@ -235,6 +245,9 @@ function isObjectProp(node) {
 
 function isCustomProp(property) {
   const value = property.value;
+  if (!value) {
+    return false;
+  }
   const isCustomObjectProp = types.isObjectExpression(value) && property.key.name !== 'actions';
 
   return (
@@ -367,19 +380,23 @@ function getEmberImportAliasName(importDeclaration) {
 
 function isSingleLineFn(property) {
   return (
-    types.isCallExpression(property.value) &&
-    utils.getSize(property.value) === 1 &&
-    !isObserverProp(property.value) &&
-    (isComputedProp(property.value) || !types.isCallWithFunctionExpression(property.value))
+    (types.isMethodDefinition(property) && utils.getSize(property) === 1) ||
+    (property.value &&
+      types.isCallExpression(property.value) &&
+      utils.getSize(property.value) === 1 &&
+      !isObserverProp(property) &&
+      (isComputedProp(property.value) || !types.isCallWithFunctionExpression(property.value)))
   );
 }
 
 function isMultiLineFn(property) {
   return (
-    types.isCallExpression(property.value) &&
-    utils.getSize(property.value) > 1 &&
-    !isObserverProp(property.value) &&
-    (isComputedProp(property.value) || !types.isCallWithFunctionExpression(property.value))
+    (types.isMethodDefinition(property) && utils.getSize(property) > 1) ||
+    (property.value &&
+      types.isCallExpression(property.value) &&
+      utils.getSize(property.value) > 1 &&
+      !isObserverProp(property) &&
+      (isComputedProp(property.value) || !types.isCallWithFunctionExpression(property.value)))
   );
 }
 
@@ -393,15 +410,13 @@ function isFunctionExpression(property) {
 
 function isRelation(property) {
   const relationAttrs = ['hasMany', 'belongsTo'];
-  let result = false;
 
-  relationAttrs.forEach(relation => {
-    if (isModule(property.value, relation, 'DS')) {
-      result = true;
-    }
+  return relationAttrs.some(relation => {
+    return (
+      (property.value && isModule(property.value, relation, 'DS')) ||
+      types.isClassPropertyWithDecorator(property, relation)
+    );
   });
-
-  return result;
 }
 
 /**
diff --git a/lib/utils/property-order.js b/lib/utils/property-order.js
index fb090cca..99f30801 100644
--- a/lib/utils/property-order.js
+++ b/lib/utils/property-order.js
@@ -2,6 +2,7 @@
 
 const ember = require('./ember');
 const utils = require('./utils');
+const types = require('./types');
 
 module.exports = {
   determinePropertyType,
@@ -50,11 +51,11 @@ const NAMES = {
 
 // eslint-disable-next-line complexity
 function determinePropertyType(node, parentType) {
-  if (ember.isInjectedServiceProp(node.value)) {
+  if (ember.isInjectedServiceProp(node)) {
     return 'service';
   }
 
-  if (ember.isInjectedControllerProp(node.value)) {
+  if (ember.isInjectedControllerProp(node)) {
     return 'controller';
   }
 
@@ -77,7 +78,10 @@ function determinePropertyType(node, parentType) {
   }
 
   if (parentType === 'model') {
-    if (ember.isModule(node.value, 'attr', 'DS')) {
+    if (
+      (node.value && ember.isModule(node.value, 'attr', 'DS')) ||
+      types.isClassPropertyWithDecorator(node, 'attr')
+    ) {
       return 'attribute';
     } else if (ember.isRelation(node)) {
       return 'relationship';
@@ -92,7 +96,7 @@ function determinePropertyType(node, parentType) {
     }
   }
 
-  if (ember.isObserverProp(node.value)) {
+  if (ember.isObserverProp(node)) {
     return 'observer';
   }
 
@@ -112,7 +116,7 @@ function determinePropertyType(node, parentType) {
     return 'property';
   }
 
-  if (ember.isFunctionExpression(node.value)) {
+  if (node.value && ember.isFunctionExpression(node.value)) {
     if (utils.isEmptyMethod(node)) {
       return 'empty-method';
     }
@@ -165,7 +169,11 @@ function reportUnorderedProperties(node, context, parentType, ORDER) {
   const firstPropertyOfType = {};
   let firstPropertyOfNextType;
 
-  ember.getModuleProperties(node).forEach(property => {
+  const properties = types.isClassDeclaration(node)
+    ? node.body.body
+    : ember.getModuleProperties(node);
+
+  properties.forEach(property => {
     const type = determinePropertyType(property, parentType);
     const order = getOrder(ORDER, type);
 
diff --git a/lib/utils/types.js b/lib/utils/types.js
index 5ef41a24..f3ac845c 100644
--- a/lib/utils/types.js
+++ b/lib/utils/types.js
@@ -8,6 +8,8 @@ module.exports = {
   isCallExpression,
   isCallWithFunctionExpression,
   isClassDeclaration,
+  isClassProperty,
+  isClassPropertyWithDecorator,
   isConciseArrowFunctionWithCallExpression,
   isConditionalExpression,
   isExpressionStatement,
@@ -18,6 +20,7 @@ module.exports = {
   isLiteral,
   isLogicalExpression,
   isMemberExpression,
+  isMethodDefinition,
   isNewExpression,
   isObjectExpression,
   isObjectPattern,
@@ -108,6 +111,37 @@ function isClassDeclaration(node) {
   return node !== undefined && node.type === 'ClassDeclaration';
 }
 
+/**
+ * Check whether or not a node is a ClassProperty.
+ *
+ * @param {Object} node The node to check.
+ * @returns {boolean} Whether or not the node is a ClassProperty.
+ */
+function isClassProperty(node) {
+  return node !== undefined && node.type === 'ClassProperty';
+}
+
+/**
+ * Check whether or not a node is a ClassProperty, with at least the given decorator.
+ *
+ * @param {Object} node The node to check.
+ * @param {string} decoratorName The decorator to look for
+ * @returns {boolean} Whether or not the node is a ClassProperty with the given decorator.
+ */
+function isClassPropertyWithDecorator(node, decoratorName) {
+  return (
+    isClassProperty(node) &&
+    node.decorators &&
+    node.decorators.some(decorator => {
+      const expression = decorator.expression;
+      return (
+        (isIdentifier(expression) && expression.name === decoratorName) ||
+        (isCallExpression(expression) && expression.callee.name === decoratorName)
+      );
+    })
+  );
+}
+
 /**
  * Check whether or not a node is an ArrowFunctionExpression with concise body
  * that contains a call expression.
@@ -210,6 +244,16 @@ function isMemberExpression(node) {
   return node !== undefined && node.type === 'MemberExpression';
 }
 
+/**
+ * Check whether or not a node is a MethodDefinition.
+ *
+ * @param {Object} node The node to check.
+ * @return {boolean} Whether or not the node is a MethodDefinition.
+ */
+function isMethodDefinition(node) {
+  return node !== undefined && node.type === 'MethodDefinition';
+}
+
 /**
  * Check whether or not a node is an NewExpression.
  *


Test patch:
diff --git a/tests/helpers/faux-context.js b/tests/helpers/faux-context.js
index dc1d825d..5fb6f5af 100644
--- a/tests/helpers/faux-context.js
+++ b/tests/helpers/faux-context.js
@@ -7,11 +7,12 @@ const babelEslint = require('babel-eslint');
  * expected by `getSourceModuleNameForIdentifier` and `isEmberCoreModule`
  */
 class FauxContext {
-  constructor(code, filename) {
+  constructor(code, filename = '', report = () => {}) {
     const { ast } = babelEslint.parseForESLint(code);
 
     this.ast = ast;
     this.filename = filename;
+    this.report = report;
   }
 
   /**
diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js
index e341d6e8..287fe266 100644
--- a/tests/lib/utils/ember-test.js
+++ b/tests/lib/utils/ember-test.js
@@ -428,35 +428,134 @@ describe('isEmberService', () => {
 });
 
 describe('isInjectedServiceProp', () => {
-  let node;
+  describe('classic classes', () => {
+    it("should check if it's an injected service prop with renamed import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          currentUser: service()
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    });
 
-  it("should check if it's an injected service prop", () => {
-    node = parse('service()');
-    expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    it("should check if it's an injected service prop with full import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          currentUser: Ember.inject.service()
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    });
 
-    node = parse('Ember.inject.service()');
-    expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    it("should check if it's an injected service prop with destructured import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          currentUser: inject()
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    });
 
-    node = parse('inject()');
-    expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    it("should check that it's not an injected service prop", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          currentUser: otherFunction()
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeFalsy();
+    });
 
-    node = parse('otherFunction()');
-    expect(emberUtils.isInjectedServiceProp(node)).toBeFalsy();
+    it("should check that it's not an injected service prop when 'service' is not a function", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          currentUser: service.otherFunction()
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeFalsy();
+    });
+  });
 
-    node = parse('service.otherFunction()');
-    expect(emberUtils.isInjectedServiceProp(node)).toBeFalsy();
+  describe('native classes', () => {
+    it("should check if it's an injected service prop when using renamed import", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @service currentUser;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    });
+
+    it("should check if it's an injected service prop when using decorator", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @inject currentUser;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeTruthy();
+    });
+
+    it("should check that it's not an injected service prop when using another decorator", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @otherDecorator currentUser;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isInjectedServiceProp(node)).toBeFalsy();
+    });
   });
 });
 
 describe('isInjectedControllerProp', () => {
-  let node;
+  describe('classic classes', () => {
+    it("should check if it's an injected controller prop with destructed import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          application: controller(),
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedControllerProp(node)).toBeTruthy();
+    });
 
-  it("should check if it's an injected controller prop", () => {
-    node = parse('controller()');
-    expect(emberUtils.isInjectedControllerProp(node)).toBeTruthy();
+    it("should check if it's an injected controller prop with full import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          application: Ember.inject.controller(),
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isInjectedControllerProp(node)).toBeTruthy();
+    });
+  });
 
-    node = parse('Ember.inject.controller()');
-    expect(emberUtils.isInjectedControllerProp(node)).toBeTruthy();
+  describe('native classes', () => {
+    it("should check if it's an injected controller prop with decorator", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @controller application;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isInjectedControllerProp(node)).toBeTruthy();
+    });
+
+    it("should check that it's not an injected controller prop", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @otherDecorator application;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isInjectedControllerProp(node)).toBeFalsy();
+    });
   });
 });
 
@@ -491,14 +590,70 @@ describe('isComputedProp', () => {
 });
 
 describe('isObserverProp', () => {
-  let node;
+  describe('classic classes', () => {
+    it("should check if it's an observer prop using destructured import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          someObserver: observer(),
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    });
+
+    it("should check if it's an observer prop with full import", () => {
+      const context = new FauxContext(`
+        export default Controller.extend({
+          someObserver: Ember.observer(),
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    });
+
+    it("should check if it's an observer prop with multi-line observer", () => {
+      const context = new FauxContext(`
+        export default Component.extend({
+          levelOfHappiness: observer("attitude", "health", () => {
+          }),
+          vehicle: alias("car")
+        });
+      `);
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    });
+  });
 
-  it("should check if it's an observer prop", () => {
-    node = parse('observer()');
-    expect(emberUtils.isObserverProp(node)).toBeTruthy();
+  describe('native classes', () => {
+    it("should check if it's an observer prop using decorator", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @observer someObserver;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    });
 
-    node = parse('Ember.observer()');
-    expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    it("should check if it's an observer prop using decorator with arg", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @observer("someArg") someObserver() {};
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isObserverProp(node)).toBeTruthy();
+    });
+
+    it("should check that it's not an observer prop", () => {
+      const context = new FauxContext(`
+        class MyController extends Controller {
+          @otherDecorator someObserver;
+        }
+      `);
+      const node = context.ast.body[0].body.body[0];
+      expect(emberUtils.isObserverProp(node)).toBeFalsy();
+    });
   });
 });
 
@@ -628,6 +783,24 @@ describe('isSingleLineFn', () => {
 
   it('should check if given function has one line', () => {
     expect(emberUtils.isSingleLineFn(property)).toBeTruthy();
+
+    let context = new FauxContext(`
+      class MyController extends Controller {
+        @computed("someProp") someFunction() {}
+      }
+    `);
+    let node = context.ast.body[0].body.body[0];
+    expect(emberUtils.isSingleLineFn(node)).toBeTruthy();
+
+    context = new FauxContext(`
+      class MyController extends Controller {
+        @computed("someProp") someFunction() {
+          console.log("hello");
+        }
+      }
+    `);
+    node = context.ast.body[0].body.body[0];
+    expect(emberUtils.isSingleLineFn(node)).toBeFalsy();
   });
 });
 
@@ -640,6 +813,24 @@ describe('isMultiLineFn', () => {
 
   it('should check if given function has more than one line', () => {
     expect(emberUtils.isMultiLineFn(property)).toBeTruthy();
+
+    let context = new FauxContext(`
+      class MyController extends Controller {
+        @computed("someProp") someFunction() {
+          console.log("hello");
+        }
+      }
+    `);
+    let node = context.ast.body[0].body.body[0];
+    expect(emberUtils.isMultiLineFn(node)).toBeTruthy();
+
+    context = new FauxContext(`
+      class MyController extends Controller {
+        @computed("someProp") someFunction() {}
+      }
+    `);
+    node = context.ast.body[0].body.body[0];
+    expect(emberUtils.isMultiLineFn(node)).toBeFalsy();
   });
 });
 
diff --git a/tests/lib/utils/property-order-test.js b/tests/lib/utils/property-order-test.js
index 369cac09..aab43f2b 100644
--- a/tests/lib/utils/property-order-test.js
+++ b/tests/lib/utils/property-order-test.js
@@ -1,6 +1,7 @@
 'use-strict';
 
 const propertyOrder = require('../../../lib/utils/property-order');
+const { FauxContext } = require('../../helpers/faux-context');
 
 describe('addBackwardsPosition', () => {
   it('should not change the order if the desired position is already included in the order', () => {
@@ -44,3 +45,341 @@ describe('addBackwardsPosition', () => {
     expect(newOrder).toEqual([['method', 'bar', 'empty-method'], 'foo']);
   });
 });
+
+describe('determinePropertyType', () => {
+  describe('classic classes', () => {
+    it('should determine service-type props', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          currentUser: service(),
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('service');
+    });
+
+    it('should determine controller-type props', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          application: controller(),
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('controller');
+    });
+
+    it('should determine init-type props', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          init() {}
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('init');
+    });
+
+    it('should determine component lifecycle hooks', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          didInsertElement() {}
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('didInsertElement');
+    });
+
+    it('should determine query-params', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          queryParams: []
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('query-params');
+    });
+
+    it('should determine inherited properties', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          isDestroyed: false
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('inherited-property');
+    });
+
+    it('should determine attributes', () => {
+      const context = new FauxContext(
+        `export default Model.extend({
+          someAttr: DS.attr()
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'model')).toEqual('attribute');
+    });
+
+    it('should determine relationships', () => {
+      const context = new FauxContext(
+        `export default Model.extend({
+          someRelationship: hasMany('otherModel')
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'model')).toEqual('relationship');
+    });
+
+    it('should determine observer-type props', () => {
+      const context = new FauxContext(
+        `export default Controller.extend({
+          someObvs: observer(),
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('observer');
+    });
+
+    it('should determine actions', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          actions: {}
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('actions');
+    });
+
+    it('should determine single-line functions', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          foo: boo()
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual(
+        'single-line-function'
+      );
+    });
+
+    it('should determine multi-line functions', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          foo: boo(bar, baz, () => {
+            console.log('boop')
+          })
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('multi-line-function');
+    });
+
+    it('should determine properties', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          foo: "boo"
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('property');
+    });
+
+    it('should determine empty methods', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          foo() {}
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('empty-method');
+    });
+
+    it('should determine methods', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          foo() { console.log("hello") }
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('method');
+    });
+  });
+
+  describe('native classes', () => {
+    it('should determine service-type props', () => {
+      const context = new FauxContext(
+        `class MyController extends Controller {
+          @service currentUser;
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('service');
+    });
+
+    it('should determine controller-type props', () => {
+      const context = new FauxContext(
+        `class MyController extends Controller {
+          @controller application;
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('controller');
+    });
+
+    it('should determine init-type props', () => {
+      const context = new FauxContext(
+        `class MyController extends Controller {
+          init() {}
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('init');
+    });
+
+    it('should determine query-params', () => {
+      const context = new FauxContext(
+        `class MyController extends Controller {
+          queryParams = [];
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('query-params');
+    });
+
+    it('should determine attributes', () => {
+      const context = new FauxContext(
+        `class MyModel extends Model {
+          @attr someAttr;
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'model')).toEqual('attribute');
+    });
+
+    it('should determine relationships', () => {
+      const context = new FauxContext(
+        `class MyModel extends Model {
+          @hasMany('otherModel') someRelationship;
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'model')).toEqual('relationship');
+    });
+
+    it('should determine observer-type props', () => {
+      const context = new FauxContext(
+        `class MyController extends Controller {
+          @observer someObvs;
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'controller')).toEqual('observer');
+    });
+
+    it('should determine single-line functions', () => {
+      const context = new FauxContext(
+        `class MyComponent extends Component {
+          foo() {}
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual(
+        'single-line-function'
+      );
+    });
+
+    it('should determine multi-line functions', () => {
+      const context = new FauxContext(
+        `class MyComponent extends Component {
+          foo(bar) {
+            console.log(bar)
+          }
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('multi-line-function');
+    });
+
+    it('should determine properties', () => {
+      const context = new FauxContext(
+        `class MyComponent extends Component {
+          foo = "boo";
+        }`
+      );
+      const node = context.ast.body[0].body.body[0];
+      expect(propertyOrder.determinePropertyType(node, 'component')).toEqual('property');
+    });
+  });
+});
+
+describe('reportUnorderedProperties', () => {
+  describe('classic classes', () => {
+    it('should not report nodes if the order is correct', () => {
+      const order = ['controller', 'service', 'query-params'];
+      const context = new FauxContext(
+        `export default Controller.extend({
+        application: controller(),
+        currentUser: service(),
+        queryParams: [],
+      });`,
+        '',
+        jest.fn()
+      );
+      const node = context.ast.body[0].declaration;
+
+      propertyOrder.reportUnorderedProperties(node, context, 'controller', order);
+      expect(context.report).not.toHaveBeenCalled();
+    });
+
+    it('should report nodes if the order is incorrect', () => {
+      const order = ['controller', 'service', 'query-params'];
+      const context = new FauxContext(
+        `export default Controller.extend({
+          currentUser: service(),
+          application: controller(),
+          queryParams: [],
+        });`,
+        '',
+        jest.fn()
+      );
+      const node = context.ast.body[0].declaration;
+
+      propertyOrder.reportUnorderedProperties(node, context, 'controller', order);
+      expect(context.report).toHaveBeenCalled();
+    });
+  });
+
+  describe('native classes', () => {
+    it('should not report nodes if the order is correct', () => {
+      const order = ['controller', 'service', 'query-params'];
+      const context = new FauxContext(
+        `export default class MyController extends Controller {
+          @controller application;
+          @service currentUser;
+          queryParams = [];
+        }`,
+        '',
+        jest.fn()
+      );
+      const node = context.ast.body[0].declaration;
+
+      propertyOrder.reportUnorderedProperties(node, context, 'controller', order);
+      expect(context.report).not.toHaveBeenCalled();
+    });
+
+    it('should report nodes if the order is incorrect', () => {
+      const order = ['controller', 'service', 'query-params'];
+      const context = new FauxContext(
+        `export default class MyController extends Controller {
+          @service currentUser;
+          @controller application;
+          queryParams = [];
+        }`,
+        '',
+        jest.fn()
+      );
+      const node = context.ast.body[0].declaration;
+
+      propertyOrder.reportUnorderedProperties(node, context, 'controller', order);
+      expect(context.report).toHaveBeenCalled();
+    });
+  });
+});