Repository: ember-cli/eslint-plugin-ember
Pull request #665 (2020-02-11 17:53:42)

Update `order-in-*` rules to support custom ordering of properties
Taking over PR #191.

We have a use-case for this where we want properties named "layout" ordered before all else.


Patch:
diff --git a/README.md b/README.md
index 0417286f..519e0429 100644
--- a/README.md
+++ b/README.md
@@ -67,7 +67,6 @@ Each rule has emojis denoting what configuration it belongs to and/or a :wrench:
 | :white_check_mark: | [closure-actions](./docs/rules/closure-actions.md) | enforce usage of closure actions |
 |  | [named-functions-in-promises](./docs/rules/named-functions-in-promises.md) | enforce usage of named functions in promises |
 | :white_check_mark: | [new-module-imports](./docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 |
-|  | [no-controllers](./docs/rules/no-controllers.md) | disallow non-essential controllers |
 | :white_check_mark: | [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions |
 | :car: | [no-get-with-default](./docs/rules/no-get-with-default.md) | disallow usage of the Ember's `getWithDefault` function |
 | :car::wrench: | [no-get](./docs/rules/no-get.md) | require using ES5 getters instead of Ember's `get` / `getProperties` functions |
diff --git a/docs/rules/no-controllers.md b/docs/rules/no-controllers.md
deleted file mode 100644
index 8960b4bf..00000000
--- a/docs/rules/no-controllers.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# no-controllers
-
-Some people may prefer to avoid the use of controllers in their applications, typically in favor of components which can be more portable and easier to test.
-
-This rule disallows controller usage, except when the controller is used to define `queryParams`.
-
-## Examples
-
-Examples of **incorrect** code for this rule:
-
-```js
-import Controller from '@ember/controller';
-export default class ArticlesController extends Controller {
-  // Controller disallowed since it doesn't use `queryParams`.
-  ...
-}
-```
-
-Examples of **correct** code for this rule:
-
-```js
-import Controller from '@ember/controller';
-export default class ArticlesController extends Controller {
-  queryParams = ['category']; // Controller allowed for defining `queryParams`.
-  @tracked category = null;
-  ...
-}
-```
-
-Note that this rule supports both classic and native class syntax.
-
-## Further Reading
-
-- [Guide on controllers](https://guides.emberjs.com/release/routing/controllers/)
-- [Guide on `queryParameters`](https://guides.emberjs.com/release/routing/query-params/)
diff --git a/docs/rules/order-in-components.md b/docs/rules/order-in-components.md
index 0ed7020f..8f448d60 100644
--- a/docs/rules/order-in-components.md
+++ b/docs/rules/order-in-components.md
@@ -54,7 +54,20 @@ order: [
 ]
 ```
 
-You can find full list of properties that you can use to configure this rule [here](/lib/utils/property-order.js#L10).
+### Custom Properties
+
+If you would like to specify ordering for a property type that is not listed, you can use the custom property syntax
+in the order list to specify where the property should go:
+
+```
+custom:myPropertyName
+```
+
+These must be prefixed with `custom:`
+
+### Additional Properties
+
+You can find the full list of properties [here](/lib/utils/property-order.js#L10).
 
 ## Description
 
diff --git a/docs/rules/order-in-controllers.md b/docs/rules/order-in-controllers.md
index 537212d6..3d0d878a 100644
--- a/docs/rules/order-in-controllers.md
+++ b/docs/rules/order-in-controllers.md
@@ -30,7 +30,20 @@ order: [
 ]
 ```
 
-You can find full list of properties that you can use to configure this rule [here](/lib/utils/property-order.js#L10).
+### Custom Properties
+
+If you would like to specify ordering for a property type that is not listed, you can use the custom property syntax
+in the order list to specify where the property should go:
+
+```
+custom:myPropertyName
+```
+
+These must be prefixed with `custom:`
+
+### Additional Properties
+
+You can find the full list of properties [here](/lib/utils/property-order.js#L10).
 
 ## Description
 
diff --git a/docs/rules/order-in-models.md b/docs/rules/order-in-models.md
index 8d2818b6..c8b103c6 100644
--- a/docs/rules/order-in-models.md
+++ b/docs/rules/order-in-models.md
@@ -23,7 +23,20 @@ order: [
 ]
 ```
 
-You can find full list of properties that you can use to configure this rule [here](/lib/utils/property-order.js#L10).
+### Custom Properties
+
+If you would like to specify ordering for a property type that is not listed, you can use the custom property syntax
+in the order list to specify where the property should go:
+
+```
+custom:myPropertyName
+```
+
+These must be prefixed with `custom:`
+
+### Additional Properties
+
+You can find the full list of properties [here](/lib/utils/property-order.js#L10).
 
 ## Description
 
diff --git a/docs/rules/order-in-routes.md b/docs/rules/order-in-routes.md
index bb2e14c0..f0bd3766 100644
--- a/docs/rules/order-in-routes.md
+++ b/docs/rules/order-in-routes.md
@@ -50,7 +50,20 @@ order: [
 ]
 ```
 
-You can find full list of properties that you can use to configure this rule [here](/lib/utils/property-order.js#L10).
+### Custom Properties
+
+If you would like to specify ordering for a property type that is not listed, you can use the custom property syntax
+in the order list to specify where the property should go:
+
+```
+custom:myPropertyName
+```
+
+These must be prefixed with `custom:`
+
+### Additional Properties
+
+You can find the full list of properties [here](/lib/utils/property-order.js#L10).
 
 ## Description
 
diff --git a/lib/index.js b/lib/index.js
index dd348d6d..1f2163b2 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -20,7 +20,6 @@ module.exports = {
     'no-classic-classes': require('./rules/no-classic-classes'),
     'no-classic-components': require('./rules/no-classic-components'),
     'no-computed-properties-in-native-classes': require('./rules/no-computed-properties-in-native-classes'),
-    'no-controllers': require('./rules/no-controllers'),
     'no-deeply-nested-dependent-keys-with-each': require('./rules/no-deeply-nested-dependent-keys-with-each'),
     'no-duplicate-dependent-keys': require('./rules/no-duplicate-dependent-keys'),
     'no-ember-super-in-es-classes': require('./rules/no-ember-super-in-es-classes'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 1b717845..5640f977 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -23,7 +23,6 @@ module.exports = {
   "ember/no-classic-classes": "off",
   "ember/no-classic-components": "off",
   "ember/no-computed-properties-in-native-classes": "off",
-  "ember/no-controllers": "off",
   "ember/no-deeply-nested-dependent-keys-with-each": "error",
   "ember/no-duplicate-dependent-keys": "error",
   "ember/no-ember-super-in-es-classes": "error",
diff --git a/lib/rules/no-classic-components.js b/lib/rules/no-classic-components.js
index 3224b808..b08d21be 100644
--- a/lib/rules/no-classic-components.js
+++ b/lib/rules/no-classic-components.js
@@ -1,8 +1,5 @@
 'use strict';
 
-const types = require('../utils/types');
-const assert = require('assert');
-
 const ERROR_MESSAGE =
   'Use Glimmer components(@glimmer/component) instead of classic components(@ember/component)';
 
@@ -30,18 +27,12 @@ module.exports = {
 
     return {
       ImportDeclaration(node) {
-        if (node.source.value === '@ember/component') {
-          const importDefaultSpecifier = getImportDefaultSpecifier(node);
-          if (importDefaultSpecifier) {
-            report(importDefaultSpecifier);
-          }
+        const importSource = node.source.value;
+
+        if (importSource === '@ember/component') {
+          report(node);
         }
       },
     };
   },
 };
-
-function getImportDefaultSpecifier(node) {
-  assert(types.isImportDeclaration(node));
-  return node.specifiers.find(specifier => types.isImportDefaultSpecifier(specifier));
-}
diff --git a/lib/rules/no-controllers.js b/lib/rules/no-controllers.js
deleted file mode 100644
index 638ea8ab..00000000
--- a/lib/rules/no-controllers.js
+++ /dev/null
@@ -1,67 +0,0 @@
-const ember = require('../utils/ember');
-const types = require('../utils/types');
-const assert = require('assert');
-
-const ERROR_MESSAGE = 'Avoid using controllers except for specifying `queryParams`';
-
-module.exports = {
-  ERROR_MESSAGE,
-  meta: {
-    type: 'suggestion',
-    docs: {
-      description: 'disallow non-essential controllers',
-      category: 'Best Practices',
-      recommended: false,
-      url:
-        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-controllers.md',
-    },
-    fixable: null,
-    schema: [],
-  },
-
-  create: context => {
-    return {
-      ClassDeclaration(node) {
-        if (
-          ember.isEmberController(context, node) &&
-          (node.body.body.length === 0 || !classDeclarationHasProperty(node, 'queryParams'))
-        ) {
-          context.report(node, ERROR_MESSAGE);
-        }
-      },
-
-      CallExpression(node) {
-        if (
-          ember.isEmberController(context, node) &&
-          (node.arguments.length === 0 || !callExpressionClassHasProperty(node, 'queryParams'))
-        ) {
-          context.report(node, ERROR_MESSAGE);
-        }
-      },
-    };
-  },
-};
-
-function classDeclarationHasProperty(classDeclaration, propertyName) {
-  assert(types.isClassDeclaration(classDeclaration));
-  return (
-    classDeclaration.body.body.length >= 1 &&
-    classDeclaration.body.body.some(
-      item =>
-        types.isClassProperty(item) &&
-        types.isIdentifier(item.key) &&
-        item.key.name === propertyName
-    )
-  );
-}
-
-function callExpressionClassHasProperty(callExpression, propertyName) {
-  assert(types.isCallExpression(callExpression));
-  return (
-    callExpression.arguments.length >= 1 &&
-    types.isObjectExpression(callExpression.arguments[callExpression.arguments.length - 1]) &&
-    callExpression.arguments[callExpression.arguments.length - 1].properties.some(
-      prop => types.isIdentifier(prop.key) && prop.key.name === propertyName
-    )
-  );
-}
diff --git a/lib/rules/order-in-routes.js b/lib/rules/order-in-routes.js
index 2939fb56..a4d88d8b 100644
--- a/lib/rules/order-in-routes.js
+++ b/lib/rules/order-in-routes.js
@@ -83,7 +83,6 @@ module.exports = {
         if (!ember.isEmberRoute(context, node)) {
           return;
         }
-
         reportUnorderedProperties(node, context, 'route', order);
       },
     };
diff --git a/lib/utils/property-order.js b/lib/utils/property-order.js
index 4d384de7..e1e94f9f 100644
--- a/lib/utils/property-order.js
+++ b/lib/utils/property-order.js
@@ -50,7 +50,7 @@ const NAMES = {
 };
 
 // eslint-disable-next-line complexity
-function determinePropertyType(node, parentType) {
+function determinePropertyType(node, parentType, ORDER) {
   if (ember.isInjectedServiceProp(node)) {
     return 'service';
   }
@@ -112,6 +112,12 @@ function determinePropertyType(node, parentType) {
     return 'multi-line-function';
   }
 
+  const propName = getNodeKeyName(node);
+  const possibleOrderName = `custom:${propName}`;
+  if (ORDER.includes(possibleOrderName)) {
+    return possibleOrderName;
+  }
+
   if (ember.isCustomProp(node)) {
     return 'property';
   }
@@ -145,22 +151,34 @@ function getOrder(ORDER, type) {
   return ORDER.length;
 }
 
-function getName(type, node) {
-  let prefix;
-  if (!node.computed && type !== 'actions' && type !== 'model') {
+function getNodeKeyName(node) {
+  if (node.key) {
     if (node.key.type === 'Identifier') {
-      prefix = node.key.name;
+      return node.key.name;
     } else if (node.key.type === 'Literal' && typeof node.key.value === 'string') {
-      prefix = node.key.value;
+      return node.key.value;
     }
   }
 
+  return 'unknown';
+}
+
+function getName(type, node) {
+  let prefix;
+  if (!node.computed && type !== 'actions' && type !== 'model') {
+    prefix = getNodeKeyName(node);
+  }
+
   const typeDescription = NAMES[type];
 
   if (type === 'inherited-property') {
     return prefix ? `inherited "${prefix}" property` : 'inherited property';
   }
 
+  if (type.startsWith('custom:')) {
+    return prefix ? `"${prefix}" custom property` : 'custom property';
+  }
+
   return prefix ? `"${prefix}" ${typeDescription}` : typeDescription;
 }
 
@@ -174,7 +192,7 @@ function reportUnorderedProperties(node, context, parentType, ORDER) {
     : ember.getModuleProperties(node);
 
   properties.forEach(property => {
-    const type = determinePropertyType(property, parentType);
+    const type = determinePropertyType(property, parentType, ORDER);
     const order = getOrder(ORDER, type);
 
     const info = {
diff --git a/lib/utils/types.js b/lib/utils/types.js
index 07c31e14..30886530 100644
--- a/lib/utils/types.js
+++ b/lib/utils/types.js
@@ -18,7 +18,6 @@ module.exports = {
   isFunctionExpression,
   isIdentifier,
   isImportDeclaration,
-  isImportDefaultSpecifier,
   isLiteral,
   isLogicalExpression,
   isMemberExpression,
@@ -226,16 +225,6 @@ function isImportDeclaration(node) {
   return node !== undefined && node.type === 'ImportDeclaration';
 }
 
-/**
- * Check whether or not a node is an ImportDefaultSpecifier.
- *
- * @param {Object} node The node to check.
- * @returns {boolean} Whether or not the node is an ImportDefaultSpecifier.
- */
-function isImportDefaultSpecifier(node) {
-  return node !== undefined && node.type === 'ImportDefaultSpecifier';
-}
-
 /**
  * Check whether or not a node is an Literal.
  *


Test patch:
diff --git a/tests/lib/rules/no-classic-components.js b/tests/lib/rules/no-classic-components.js
index 0eaa2533..9625cb98 100644
--- a/tests/lib/rules/no-classic-components.js
+++ b/tests/lib/rules/no-classic-components.js
@@ -19,11 +19,7 @@ const parserOptions = {
 const ruleTester = new RuleTester({ parserOptions });
 
 ruleTester.run('no-classic-components', rule, {
-  valid: [
-    "import Component from '@glimmer/component';",
-    "import { setComponentTemplate } from '@ember/component';",
-    "import { helper } from '@ember/component/helper';",
-  ],
+  valid: ["import Component from '@glimmer/component';"],
 
   invalid: [
     {
@@ -32,7 +28,7 @@ ruleTester.run('no-classic-components', rule, {
       errors: [
         {
           message: ERROR_MESSAGE,
-          type: 'ImportDefaultSpecifier',
+          type: 'ImportDeclaration',
         },
       ],
     },
diff --git a/tests/lib/rules/no-controllers.js b/tests/lib/rules/no-controllers.js
deleted file mode 100644
index 3eeeb49c..00000000
--- a/tests/lib/rules/no-controllers.js
+++ /dev/null
@@ -1,117 +0,0 @@
-//------------------------------------------------------------------------------
-// Requirements
-//------------------------------------------------------------------------------
-
-const rule = require('../../../lib/rules/no-controllers');
-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-controllers', rule, {
-  valid: [
-    `
-      import Controller from '@ember/controller';
-      export default Controller.extend({
-        someFunction() {},
-        query: null,
-        sortType: null,
-        sortOrder: null,
-        queryParams: ['query', 'sortType', 'sortOrder']
-      });
-    `,
-    `
-      import Controller from '@ember/controller';
-      export default class ArticlesController extends Controller {
-        get filteredArticles() {}
-        @tracked category = null;
-        queryParams = ['category'];
-      }
-    `,
-  ],
-
-  invalid: [
-    // ***************
-    // Legacy classes:
-    // ***************
-
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default Controller.extend();
-      `,
-      output: null,
-      errors: [{ type: 'CallExpression', message: ERROR_MESSAGE }],
-    },
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default Controller.extend(SomeMixin);
-      `,
-      output: null,
-      errors: [{ type: 'CallExpression', message: ERROR_MESSAGE }],
-    },
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default Controller.extend({});
-      `,
-      output: null,
-      errors: [{ type: 'CallExpression', message: ERROR_MESSAGE }],
-    },
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default Controller.extend({
-          randomProperty: true,
-          randomFunction() {}
-        });
-      `,
-      output: null,
-      errors: [{ type: 'CallExpression', message: ERROR_MESSAGE }],
-    },
-    {
-      // With mixin.
-      code: `
-        import Controller from '@ember/controller';
-        export default Controller.extend(SomeMixin, {
-          randomProperty: true,
-          randomFunction() {}
-        });
-      `,
-      output: null,
-      errors: [{ type: 'CallExpression', message: ERROR_MESSAGE }],
-    },
-
-    // ***************
-    // Native classes:
-    // ***************
-
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default class ArticlesController extends Controller {}
-      `,
-      output: null,
-      errors: [{ type: 'ClassDeclaration', message: ERROR_MESSAGE }],
-    },
-    {
-      code: `
-        import Controller from '@ember/controller';
-        export default class ArticlesController extends Controller {
-          randomProperty = true;
-          get filteredArticles() {}
-        }
-      `,
-      output: null,
-      errors: [{ type: 'ClassDeclaration', message: ERROR_MESSAGE }],
-    },
-  ],
-});
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index a7997bf7..612406e4 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -304,6 +304,7 @@ eslintTester.run('order-in-components', rule, {
         }).volatile(),
         bar() { const foo = 'bar'}
       });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
       options: [
         {
           order: [
@@ -316,6 +317,21 @@ eslintTester.run('order-in-components', rule, {
         },
       ],
     },
+    {
+      code: `export default Component.extend({
+        prop: null,
+        actions: {
+          action: () => {}
+        },
+        customProp: { a: 1 }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['property', 'actions', 'custom:customProp', 'method'],
+        },
+      ],
+    },
   ],
   invalid: [
     {
@@ -718,6 +734,7 @@ eslintTester.run('order-in-components', rule, {
         bar() { const foo = 'bar'},
         onBar: () => {}
       });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           message:
@@ -756,5 +773,45 @@ eslintTester.run('order-in-components', rule, {
         },
       ],
     },
+    {
+      code: `export default Component.extend({
+        prop: null,
+        actions: {},
+        customProp: { a: 1 }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['property', 'custom:customProp', 'actions', 'method'],
+        },
+      ],
+      errors: [
+        {
+          message: 'The "customProp" custom property should be above the actions hash on line 3',
+          line: 4,
+        },
+      ],
+    },
+    {
+      code: `export default Component.extend({
+        customProp: { a: 1 },
+        aMethod() {
+          console.log('not empty');
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['method', 'custom:customProp'],
+        },
+      ],
+      errors: [
+        {
+          message:
+            'The "aMethod" method should be above the "customProp" custom property on line 2',
+          line: 3,
+        },
+      ],
+    },
   ],
 });
diff --git a/tests/lib/rules/order-in-controllers.js b/tests/lib/rules/order-in-controllers.js
index 5ea1a4aa..e8203d01 100644
--- a/tests/lib/rules/order-in-controllers.js
+++ b/tests/lib/rules/order-in-controllers.js
@@ -117,7 +117,22 @@ eslintTester.run('order-in-controllers', rule, {
             this._super(...arguments);
           }
         });
-      `,
+    `,
+    {
+      code: `export default Controller.extend({
+        prop: null,
+        actions: {
+          action: () => {}
+        },
+        customProp: { a: 1 }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['property', 'actions', 'custom:customProp'],
+        },
+      ],
+    },
   ],
   invalid: [
     {
@@ -313,6 +328,7 @@ eslintTester.run('order-in-controllers', rule, {
           foo: service()
         });
       `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           message:
@@ -374,5 +390,26 @@ eslintTester.run('order-in-controllers', rule, {
         },
       ],
     },
+    {
+      code: `export default Controller.extend({
+        customProp: { a: 1 },
+        aMethod() {
+          console.log('not empty');
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['method', 'custom:customProp'],
+        },
+      ],
+      errors: [
+        {
+          message:
+            'The "aMethod" method should be above the "customProp" custom property on line 2',
+          line: 3,
+        },
+      ],
+    },
   ],
 });
diff --git a/tests/lib/rules/order-in-models.js b/tests/lib/rules/order-in-models.js
index c3f0d7d8..20f9f5bc 100644
--- a/tests/lib/rules/order-in-models.js
+++ b/tests/lib/rules/order-in-models.js
@@ -76,6 +76,20 @@ eslintTester.run('order-in-models', rule, {
         },
       ],
     },
+    {
+      code: `export default DS.Model.extend({
+        a: attr('string'),
+        convertA(paramA) {
+        },
+        customProp: { a: 1 }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['attribute', 'method', 'custom:customProp'],
+        },
+      ],
+    },
   ],
   invalid: [
     {
@@ -224,5 +238,26 @@ eslintTester.run('order-in-models', rule, {
         },
       ],
     },
+    {
+      code: `export default DS.Model.extend({
+        customProp: { a: 1 },
+        aMethod() {
+          console.log('not empty');
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['method', 'custom:customProp'],
+        },
+      ],
+      errors: [
+        {
+          message:
+            'The "aMethod" method should be above the "customProp" custom property on line 2',
+          line: 3,
+        },
+      ],
+    },
   ],
 });
diff --git a/tests/lib/rules/order-in-routes.js b/tests/lib/rules/order-in-routes.js
index bcd7cede..28bfdbda 100644
--- a/tests/lib/rules/order-in-routes.js
+++ b/tests/lib/rules/order-in-routes.js
@@ -139,7 +139,22 @@ eslintTester.run('order-in-routes', rule, {
           },
           customFoo() {}
         });
-      `,
+    `,
+    {
+      code: `export default Route.extend({
+        prop: null,
+        actions: {
+          action: () => {}
+        },
+        customProp: { a: 1 }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['property', 'actions', 'custom:customProp'],
+        },
+      ],
+    },
   ],
   invalid: [
     {
@@ -629,5 +644,26 @@ eslintTester.run('order-in-routes', rule, {
         },
       ],
     },
+    {
+      code: `export default Route.extend({
+        customProp: { a: 1 },
+        aMethod() {
+          console.log('not empty');
+        }
+      });`,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      options: [
+        {
+          order: ['method', 'custom:customProp'],
+        },
+      ],
+      errors: [
+        {
+          message:
+            'The "aMethod" method should be above the "customProp" custom property on line 2',
+          line: 3,
+        },
+      ],
+    },
   ],
 });
diff --git a/tests/lib/utils/property-order-test.js b/tests/lib/utils/property-order-test.js
index c1d5a898..a5b952cd 100644
--- a/tests/lib/utils/property-order-test.js
+++ b/tests/lib/utils/property-order-test.js
@@ -185,7 +185,7 @@ describe('determinePropertyType', () => {
         });`
       );
       const node = context.ast.body[0].declaration.arguments[0].properties[0];
-      expect(propertyOrder.determinePropertyType(node, 'component')).toStrictEqual('property');
+      expect(propertyOrder.determinePropertyType(node, 'component', [])).toStrictEqual('property');
     });
 
     it('should determine empty methods', () => {
@@ -195,7 +195,9 @@ describe('determinePropertyType', () => {
         });`
       );
       const node = context.ast.body[0].declaration.arguments[0].properties[0];
-      expect(propertyOrder.determinePropertyType(node, 'component')).toStrictEqual('empty-method');
+      expect(propertyOrder.determinePropertyType(node, 'component', [])).toStrictEqual(
+        'empty-method'
+      );
     });
 
     it('should determine methods', () => {
@@ -205,7 +207,34 @@ describe('determinePropertyType', () => {
         });`
       );
       const node = context.ast.body[0].declaration.arguments[0].properties[0];
-      expect(propertyOrder.determinePropertyType(node, 'component')).toStrictEqual('method');
+      expect(propertyOrder.determinePropertyType(node, 'component', [])).toStrictEqual('method');
+    });
+
+    it('should determine custom properties when given order with custom property', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          myFooProperty: null
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(
+        propertyOrder.determinePropertyType(node, 'component', [
+          'custom:myBarProperty',
+          'custom:myFooProperty',
+        ])
+      ).toStrictEqual('custom:myFooProperty');
+    });
+
+    it('should determine custom properties as normal properties when given order without custom property', () => {
+      const context = new FauxContext(
+        `export default Component.extend({
+          myFooProperty: null
+        });`
+      );
+      const node = context.ast.body[0].declaration.arguments[0].properties[0];
+      expect(
+        propertyOrder.determinePropertyType(node, 'component', ['custom:myBarProperty'])
+      ).toStrictEqual('property');
     });
   });
 
@@ -301,7 +330,7 @@ describe('determinePropertyType', () => {
         }`
       );
       const node = context.ast.body[0].body.body[0];
-      expect(propertyOrder.determinePropertyType(node, 'component')).toStrictEqual(
+      expect(propertyOrder.determinePropertyType(node, 'component', [])).toStrictEqual(
         'multi-line-function'
       );
     });
@@ -313,7 +342,7 @@ describe('determinePropertyType', () => {
         }`
       );
       const node = context.ast.body[0].body.body[0];
-      expect(propertyOrder.determinePropertyType(node, 'component')).toStrictEqual('property');
+      expect(propertyOrder.determinePropertyType(node, 'component', [])).toStrictEqual('property');
     });
   });
 });