Repository: ember-cli/eslint-plugin-ember
Pull request #1143 (2021-04-14 20:21:55)

Add new rule `no-unused-services`
New rule proposal: https://github.com/ember-cli/eslint-plugin-ember/issues/1142

A new rule warning about injected services that are unused. It’s the same concept as no-unused-vars -- by removing unused services, we can reduce the amount of code we have and improve code readability.

What counts as using the service:

```js
@service(‘foo’) foo;
OR
foo: service(‘foo’)

// this.get
this.get(‘foo’)
this.get(‘foo.bar’)

// this.getProperties
this.getProperties(‘foo’)
this.getProperties(‘foo.bar’)
this.getProperties([‘foo’])
this.getProperties([‘foo.bar’])

// Ember.get
get(this, ‘foo’)
get(this, ‘foo.bar’)

// Ember.getProperties
getProperties(this, ‘foo’)
getProperties(this, ‘foo.bar’)
getProperties(this, [‘foo’])
getProperties(this, [‘foo.bar’])

// direct this invocation
this.foo
this.foo.bar

// destructured this
const { foo } = this
let { foo } = this
```


Patch:
diff --git a/README.md b/README.md
index af95b48c..3eb830e7 100644
--- a/README.md
+++ b/README.md
@@ -171,11 +171,17 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 |  | [route-path-style](./docs/rules/route-path-style.md) | enforce usage of kebab-case (instead of snake_case or camelCase) in route paths |
 | :white_check_mark: | [routes-segments-snake-case](./docs/rules/routes-segments-snake-case.md) | enforce usage of snake_cased dynamic segments in routes |
 
-### Stylistic Issues
+### Services
 
 |    | Rule ID | Description |
 |:---|:--------|:------------|
 | :wrench: | [no-unnecessary-service-injection-argument](./docs/rules/no-unnecessary-service-injection-argument.md) | disallow unnecessary argument when injecting services |
+|  | [no-unused-services](./docs/rules/no-unused-services.md) | disallow unused service injections |
+
+### Stylistic Issues
+
+|    | Rule ID | Description |
+|:---|:--------|:------------|
 | :wrench: | [order-in-components](./docs/rules/order-in-components.md) | enforce proper order of properties in components |
 | :wrench: | [order-in-controllers](./docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers |
 | :wrench: | [order-in-models](./docs/rules/order-in-models.md) | enforce proper order of properties in models |
diff --git a/docs/rules/no-unused-services.md b/docs/rules/no-unused-services.md
new file mode 100644
index 00000000..fb0beb1d
--- /dev/null
+++ b/docs/rules/no-unused-services.md
@@ -0,0 +1,43 @@
+# no-unused-services
+
+Disallow unused service injections.
+
+By removing unused service injections, we can reduce the amount of code we have and improve code readability.
+
+**Note**: This rule can exhibit false positives when an injected service is only used in:
+
+- The corresponding handlebars template file for a controller or component
+- A mixin or other class that the current class extends from
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+import Component from '@glimmer/component';
+
+export default class MyComponent extends Component {
+  @service() myService;
+
+  // myService is not referenced below at all
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import Component from '@glimmer/component';
+
+export default class MyComponent extends Component {
+  @service() myService;
+
+  get someProperty() {
+    return this.myService.getSomething(); // using the injected service
+  }
+}
+```
+
+## References
+
+- Ember [Services](https://guides.emberjs.com/release/applications/services/) guide
+- Ember [inject](https://emberjs.com/api/ember/release/functions/@ember%2Fservice/inject) function spec
diff --git a/lib/index.js b/lib/index.js
index 1d97bc31..bbfde56b 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -68,6 +68,7 @@ module.exports = {
     'no-unnecessary-index-route': require('./rules/no-unnecessary-index-route'),
     'no-unnecessary-route-path-option': require('./rules/no-unnecessary-route-path-option'),
     'no-unnecessary-service-injection-argument': require('./rules/no-unnecessary-service-injection-argument'),
+    'no-unused-services': require('./rules/no-unused-services'),
     'no-volatile-computed-properties': require('./rules/no-volatile-computed-properties'),
     'order-in-components': require('./rules/order-in-components'),
     'order-in-controllers': require('./rules/order-in-controllers'),
diff --git a/lib/rules/no-unnecessary-service-injection-argument.js b/lib/rules/no-unnecessary-service-injection-argument.js
index 7d6e5f3b..d8965198 100644
--- a/lib/rules/no-unnecessary-service-injection-argument.js
+++ b/lib/rules/no-unnecessary-service-injection-argument.js
@@ -15,7 +15,7 @@ module.exports = {
     type: 'suggestion',
     docs: {
       description: 'disallow unnecessary argument when injecting services',
-      category: 'Stylistic Issues',
+      category: 'Services',
       recommended: false,
       url:
         'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-unnecessary-service-injection-argument.md',
diff --git a/lib/rules/no-unused-services.js b/lib/rules/no-unused-services.js
new file mode 100644
index 00000000..8f7343d8
--- /dev/null
+++ b/lib/rules/no-unused-services.js
@@ -0,0 +1,293 @@
+'use strict';
+
+const types = require('../utils/types');
+const emberUtils = require('../utils/ember');
+const propertyGetterUtils = require('../utils/property-getter');
+const { getImportIdentifier } = require('../utils/import');
+const {
+  getMacros,
+  MACROS_TO_TRACKED_ARGUMENT_COUNT,
+} = require('../utils/computed-property-macros');
+
+class Stack {
+  constructor() {
+    this.stack = new Array();
+  }
+  pop() {
+    return this.stack.pop();
+  }
+  push(item) {
+    this.stack.push(item);
+  }
+  peek() {
+    return this.stack.length > 0 ? this.stack[this.stack.length - 1] : undefined;
+  }
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow unused service injections',
+      category: 'Services',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-unused-services.md',
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      main:
+        'The service `{{name}}` is not referenced in this JS file, but it may still be referenced in the corresponding HBS file. Please remove this service injection if it is not being used within the HBS file.',
+      removeServiceInjection:
+        'Remove the service injection if not used within the corresponding HBS file.',
+    },
+  },
+
+  create(context) {
+    const classStack = new Stack();
+    const sourceCode = context.getSourceCode();
+
+    let importedComputedName;
+    let importedEmberName;
+    let importedGetName;
+    let importedGetPropertiesName;
+    const macros = getMacros();
+    const importedMacros = {};
+
+    /**
+     * Gets the trailing comma token of the given node.
+     * If the trailing comma does not exist, this returns undefined.
+     * @param {ASTNode} node The given node
+     * @returns {Token|undefined} The trailing comma token or undefined
+     */
+    function getTrailingToken(node) {
+      const nextToken = sourceCode.getTokenAfter(node);
+      return types.isCommaToken(nextToken) ? nextToken : undefined;
+    }
+
+    /**
+     * Go through the current class and report any unused services
+     * @returns {void}
+     */
+    function reportInstances() {
+      const currentClass = classStack.pop();
+      const { services, uses } = currentClass;
+      if (!services || !uses) {
+        return;
+      }
+
+      for (const name of Object.keys(services)) {
+        if (!uses.has(name)) {
+          const node = services[name];
+          context.report({
+            node,
+            data: { name },
+            messageId: 'main',
+            suggest: [
+              {
+                messageId: 'removeServiceInjection',
+                fix(fixer) {
+                  const fixers = [fixer.remove(node)];
+                  if (types.isProperty(node)) {
+                    const trailingTokenNode = getTrailingToken(node);
+                    if (trailingTokenNode) {
+                      fixers.push(fixer.remove(trailingTokenNode));
+                    }
+                  }
+                  return fixers;
+                },
+              },
+            ],
+          });
+        }
+      }
+    }
+
+    return {
+      ImportDeclaration(node) {
+        if (node.source.value === '@ember/object') {
+          importedComputedName =
+            importedComputedName || getImportIdentifier(node, '@ember/object', 'computed');
+          importedGetName = importedGetName || getImportIdentifier(node, '@ember/object', 'get');
+          importedGetPropertiesName =
+            importedGetPropertiesName ||
+            getImportIdentifier(node, '@ember/object', 'getProperties');
+        } else if (node.source.value === '@ember/object/computed') {
+          for (const spec of node.specifiers) {
+            const name = spec.imported.name;
+            if (macros.includes(name)) {
+              const localName = spec.local.name;
+              importedMacros[localName] = name;
+            }
+          }
+        } else if (node.source.value === 'ember') {
+          importedEmberName = importedEmberName || getImportIdentifier(node, 'ember');
+        }
+      },
+      // Native JS class
+      ClassDeclaration(node) {
+        classStack.push({ node, services: {}, uses: new Set() });
+      },
+      // eslint-disable-next-line complexity
+      CallExpression(node) {
+        if (emberUtils.isAnyEmberCoreModule(context, node)) {
+          // Classic class
+          classStack.push({ node, services: {}, uses: new Set() });
+        } else {
+          const currentClass = classStack.peek();
+          if (!currentClass) {
+            return;
+          }
+
+          if (
+            emberUtils.isComputedProp(node, importedEmberName, importedComputedName, {
+              includeMacro: true,
+            })
+          ) {
+            if (node.callee.property) {
+              // Ember.computed.or(), computed.or()
+              const macroName = node.callee.property.name;
+              if (macros.includes(macroName)) {
+                for (
+                  let idx = 0;
+                  idx < MACROS_TO_TRACKED_ARGUMENT_COUNT[macroName] && idx < node.arguments.length;
+                  idx++
+                ) {
+                  const elem = node.arguments[idx];
+                  if (types.isStringLiteral(elem)) {
+                    const name = splitValue(elem.value);
+                    currentClass.uses.add(name);
+                  }
+                }
+              }
+            } else {
+              // Ember.computed(), computed()
+              for (const elem of node.arguments) {
+                if (types.isStringLiteral(elem)) {
+                  const name = splitValue(elem.value);
+                  currentClass.uses.add(name);
+                }
+              }
+            }
+          } else if (propertyGetterUtils.isThisGetCall(node)) {
+            // this.get('foo...');
+            const name = splitValue(node.arguments[0].value);
+            currentClass.uses.add(name);
+          } else if (
+            types.isThisExpression(node.callee.object) &&
+            node.callee.property.name === 'getProperties'
+          ) {
+            // this.getProperties([..., 'foo..', ...]); or this.getProperties(..., 'foo..', ...);
+            const argArray = types.isArrayExpression(node.arguments[0])
+              ? node.arguments[0].elements
+              : node.arguments;
+            for (const elem of argArray) {
+              const name = splitValue(elem.value);
+              currentClass.uses.add(name);
+            }
+          } else if (types.isIdentifier(node.callee)) {
+            const calleeName = node.callee.name;
+            if (types.isThisExpression(node.arguments[0])) {
+              // If Ember.get and Ember.getProperties weren't imported, skip out early
+              if (!importedGetName && !importedGetPropertiesName) {
+                return;
+              }
+
+              if (calleeName === importedGetName) {
+                // get(this, 'foo...');
+                const name = splitValue(node.arguments[1].value);
+                currentClass.uses.add(name);
+              } else if (calleeName === importedGetPropertiesName) {
+                // getProperties(this, [..., 'foo..', ...]); or getProperties(this, ..., 'foo..', ...);
+                const argArray = types.isArrayExpression(node.arguments[1])
+                  ? node.arguments[1].elements
+                  : node.arguments.slice(1);
+                for (const elem of argArray) {
+                  const name = splitValue(elem.value);
+                  currentClass.uses.add(name);
+                }
+              }
+            } else if (importedMacros[calleeName]) {
+              // Computed macros like @alias(), @or()
+              const macroName = importedMacros[calleeName];
+              for (
+                let idx = 0;
+                idx < MACROS_TO_TRACKED_ARGUMENT_COUNT[macroName] && idx < node.arguments.length;
+                idx++
+              ) {
+                const elem = node.arguments[idx];
+                if (types.isStringLiteral(elem)) {
+                  const name = splitValue(elem.value);
+                  currentClass.uses.add(name);
+                }
+              }
+            }
+          }
+        }
+      },
+      'ClassDeclaration:exit'(node) {
+        if (classStack.peek() && classStack.peek().node === node) {
+          // Leaving current (native) class.
+          reportInstances();
+        }
+      },
+      'CallExpression:exit'(node) {
+        if (classStack.peek() && classStack.peek().node === node) {
+          // Leaving current (classic) class.
+          reportInstances();
+        }
+      },
+      // foo: service(...)
+      Property(node) {
+        const currentClass = classStack.peek();
+        if (currentClass && emberUtils.isInjectedServiceProp(node)) {
+          const name = node.key.name;
+          currentClass.services[name] = node;
+        }
+      },
+      // @service(...) foo;
+      ClassProperty(node) {
+        const currentClass = classStack.peek();
+        if (currentClass && emberUtils.isInjectedServiceProp(node)) {
+          const name = node.key.name;
+          currentClass.services[name] = node;
+        }
+      },
+      // this.foo...
+      MemberExpression(node) {
+        const currentClass = classStack.peek();
+        if (
+          currentClass &&
+          types.isThisExpression(node.object) &&
+          types.isIdentifier(node.property)
+        ) {
+          const name = node.property.name;
+          currentClass.uses.add(name);
+        }
+      },
+      VariableDeclarator(node) {
+        const currentClass = classStack.peek();
+        if (
+          currentClass &&
+          node.init &&
+          types.isThisExpression(node.init) &&
+          types.isObjectPattern(node.id)
+        ) {
+          for (const property of node.id.properties) {
+            currentClass.uses.add(property.key.name);
+          }
+        }
+      },
+    };
+  },
+};
+
+/**
+ * Splits the value by "." and returns the first element
+ * @param {String} value The given value
+ * @returns {String|undefined} The first split element or undefined if value does not exist
+ */
+function splitValue(value) {
+  return value ? value.split('.')[0] : undefined;
+}
diff --git a/lib/utils/computed-property-macros.js b/lib/utils/computed-property-macros.js
index 1b962ca3..48acbee2 100644
--- a/lib/utils/computed-property-macros.js
+++ b/lib/utils/computed-property-macros.js
@@ -114,4 +114,5 @@ module.exports = {
   getTrackedArgumentCount,
   macroToCanonicalName,
   DEFAULT_MACRO_CONFIGURATIONS,
+  MACROS_TO_TRACKED_ARGUMENT_COUNT,
 };


Test patch:
diff --git a/tests/lib/rules/no-unused-services.js b/tests/lib/rules/no-unused-services.js
new file mode 100644
index 00000000..ba709d0a
--- /dev/null
+++ b/tests/lib/rules/no-unused-services.js
@@ -0,0 +1,446 @@
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-unused-services');
+const RuleTester = require('eslint').RuleTester;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 6,
+    sourceType: 'module',
+  },
+  parser: require.resolve('babel-eslint'),
+});
+
+const SERVICE_NAME = 'fooName';
+const EO_IMPORTS = "import {computed, get, getProperties} from '@ember/object';";
+const RENAMED_EO_IMPORTS =
+  "import {computed as cp, get as g, getProperties as gp} from '@ember/object';";
+const ALIAS_IMPORT = "import {alias} from '@ember/object/computed';";
+const RENAMED_ALIAS_IMPORT = "import {alias as al} from '@ember/object/computed';";
+
+/**
+ * Generate an array of usecases using the given property name
+ * @param {String} propertyName The given property name to access
+ * @returns {Array}
+ */
+function generateUseCasesFor(propertyName) {
+  return [
+    `this.${propertyName};`,
+    `this.${propertyName}[0];`,
+    `this.${propertyName}.prop;`,
+    `this.${propertyName}.func();`,
+    `this.get('${propertyName}');`,
+    `this.get('${propertyName}.prop');`,
+    `this.getProperties('a', '${propertyName}');`,
+    `this.getProperties('a', '${propertyName}.prop');`,
+    `this.getProperties(['a', '${propertyName}']);`,
+    `this.getProperties(['a', '${propertyName}.prop']);`,
+    `const { a, b, ${propertyName} } = this;`,
+    `let { c, ${propertyName} : prop, d } = this;`,
+  ];
+}
+
+/**
+ * Generate an array of usecases with Ember.get and Ember.getProperties using the given property name
+ * @param {String} propertyName The given property name to access
+ * @param {Boolean} renamed Whether or not the imports are renamed
+ * @returns {Array}
+ */
+function generateEmberObjectUseCasesFor(propertyName, renamed = false) {
+  const getName = renamed ? 'g' : 'get';
+  const getPropertiesName = renamed ? 'gp' : 'getProperties';
+  return [
+    `${getName}(this, '${propertyName}');`,
+    `${getName}(this, '${propertyName}.prop');`,
+    `${getPropertiesName}(this, 'a', '${propertyName}');`,
+    `${getPropertiesName}(this, 'a', '${propertyName}.prop');`,
+    `${getPropertiesName}(this, ['a', '${propertyName}']);`,
+    `${getPropertiesName}(this, ['a', '${propertyName}.prop']);`,
+  ];
+}
+
+/**
+ * Generate an array of usecases with a computed macro `alias` using the given property name
+ * @param {String} propertyName The given property name to access
+ * @param {Boolean} renamed Whether or not the imports are renamed
+ * @returns {Array}
+ */
+function generateMacroUseCasesFor(propertyName, renamed = false) {
+  const aliasName = renamed ? 'al' : 'alias';
+  const aliasImport = renamed ? RENAMED_ALIAS_IMPORT : ALIAS_IMPORT;
+  return [
+    `${aliasImport} class MyClass { @service('foo') ${propertyName}; @${aliasName}('${propertyName}.prop') someAlias; }`,
+    `${aliasImport} class MyClass { @service() ${propertyName}; @${aliasName}('${propertyName}.prop') someAlias; }`,
+    `${aliasImport} Component.extend({ ${SERVICE_NAME}: service('foo'), someAlias: ${aliasName}('${propertyName}.prop') });`,
+    `${aliasImport} Component.extend({ ${SERVICE_NAME}: service(), someAlias: ${aliasName}('${propertyName}.prop') });`,
+  ];
+}
+
+/**
+ * Generate an array of usecases with a computed property using the given property name
+ * @param {String} propertyName The given property name to access
+ * @param {Boolean} renamed Whether or not the imports are renamed
+ * @returns {Array}
+ */
+function generateComputedUseCasesFor(propertyName, renamed = false) {
+  const computedName = renamed ? 'cp' : 'computed';
+  const computedImport = renamed ? RENAMED_EO_IMPORTS : EO_IMPORTS;
+  return [
+    `${computedImport} class MyClass { @service('foo') ${propertyName}; @${computedName}('${propertyName}.prop') get someComputed() {} }`,
+    `${computedImport} class MyClass { @service() ${propertyName}; @${computedName}('${propertyName}.prop') get someComputed() {} }`,
+    `${computedImport} Component.extend({ ${SERVICE_NAME}: service('foo'), someComputed: ${computedName}('${propertyName}.prop', ()=>{}) });`,
+    `${computedImport} Component.extend({ ${SERVICE_NAME}: service(), someComputed: ${computedName}('${propertyName}.prop', ()=>{}) });`,
+    `${computedImport} Component.extend({ ${SERVICE_NAME}: service('foo'), someComputed: ${computedName}.alias('${propertyName}.prop', ()=>{}) });`,
+    `${computedImport} Component.extend({ ${SERVICE_NAME}: service(), someComputed: ${computedName}.alias('${propertyName}.prop', ()=>{}) });`,
+  ];
+}
+
+/**
+ * Generate an array of valid test cases
+ * @returns {Array}
+ */
+function generateValid() {
+  const valid = [];
+
+  const useCases = generateUseCasesFor(SERVICE_NAME);
+  for (const use of useCases) {
+    valid.push(
+      `class MyClass { @service('foo') ${SERVICE_NAME}; fooFunc() {${use}} }`,
+      `class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${use}} }`,
+      `Component.extend({ ${SERVICE_NAME}: service('foo'), fooFunc() {${use}} });`,
+      `Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${use}} });`
+    );
+  }
+
+  const emberObjectUseCases = [
+    generateEmberObjectUseCasesFor(SERVICE_NAME),
+    generateEmberObjectUseCasesFor(SERVICE_NAME, true),
+  ];
+  for (const [idx, useCases] of emberObjectUseCases.entries()) {
+    const imports = idx === 0 ? EO_IMPORTS : RENAMED_EO_IMPORTS;
+    for (const use of useCases) {
+      valid.push(
+        `${imports} class MyClass { @service('foo') ${SERVICE_NAME}; fooFunc() {${use}} }`,
+        `${imports} class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${use}} }`,
+        `${imports} Component.extend({ ${SERVICE_NAME}: service('foo'), fooFunc() {${use}} });`,
+        `${imports} Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${use}} });`
+      );
+    }
+  }
+
+  const macroUseCases = [
+    ...generateMacroUseCasesFor(SERVICE_NAME),
+    ...generateMacroUseCasesFor(SERVICE_NAME, true),
+  ];
+  const computedUseCases = [
+    ...generateComputedUseCasesFor(SERVICE_NAME),
+    ...generateComputedUseCasesFor(SERVICE_NAME, true),
+  ];
+  valid.push(...macroUseCases);
+  valid.push(...computedUseCases);
+
+  return valid;
+}
+
+// Testing for unrelated props + some edge cases
+const unrelatedPropUses = generateUseCasesFor('unrelatedProp');
+const edgeCases = ['let foo;', `this.prop.${SERVICE_NAME};`];
+const nonUses = unrelatedPropUses.concat(edgeCases).join('');
+const emberObjectUses1 = generateEmberObjectUseCasesFor(SERVICE_NAME).join('');
+const emberObjectUses2 = generateEmberObjectUseCasesFor('unrelatedProp').join('');
+
+ruleTester.run('no-unused-services', rule, {
+  valid: generateValid(),
+  invalid: [
+    {
+      code: `class MyClass { @service('foo') ${SERVICE_NAME}; fooFunc() {${nonUses}} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass {  fooFunc() {${nonUses}} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${nonUses}} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass {  fooFunc() {${nonUses}} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service('foo'), fooFunc() {${nonUses}} });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `Component.extend({  fooFunc() {${nonUses}} });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service('foo') });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [{ messageId: 'removeServiceInjection', output: 'Component.extend({  });' }],
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${nonUses}} });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `Component.extend({  fooFunc() {${nonUses}} });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service() });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [{ messageId: 'removeServiceInjection', output: 'Component.extend({  });' }],
+          type: 'Property',
+        },
+      ],
+    },
+    /* Using get/getProperties without @ember/object import */
+    {
+      code: `class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${emberObjectUses1}} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass {  fooFunc() {${emberObjectUses1}} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${emberObjectUses1}} });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `Component.extend({  fooFunc() {${emberObjectUses1}} });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    /* Using get/getProperties with @ember/object import for an unrelatedProp */
+    {
+      code: `${EO_IMPORTS} class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${emberObjectUses2}} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `${EO_IMPORTS} class MyClass {  fooFunc() {${emberObjectUses2}} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `${EO_IMPORTS} Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${emberObjectUses2}} });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `${EO_IMPORTS} Component.extend({  fooFunc() {${emberObjectUses2}} });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    /* Using computed props and macros without the imports */
+    {
+      code: `class MyClass { @service() ${SERVICE_NAME}; @alias('${SERVICE_NAME}') someAlias; @computed('${SERVICE_NAME}.prop') get someComputed() {} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass {  @alias('${SERVICE_NAME}') someAlias; @computed('${SERVICE_NAME}.prop') get someComputed() {} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `Component.extend({ ${SERVICE_NAME}: service(), someAlias1: alias('${SERVICE_NAME}'), someAlias2: computed.alias('${SERVICE_NAME}.prop'), someComputed: computed('${SERVICE_NAME}.prop', ()=>{}) });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `Component.extend({  someAlias1: alias('${SERVICE_NAME}'), someAlias2: computed.alias('${SERVICE_NAME}.prop'), someComputed: computed('${SERVICE_NAME}.prop', ()=>{}) });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    /* Using computed props and macros with the imports for an unrelatedProp */
+    {
+      code: `${EO_IMPORTS}${ALIAS_IMPORT} class MyClass { @service() ${SERVICE_NAME}; @alias('unrelatedProp', '${SERVICE_NAME}') someAlias; @computed('unrelatedProp.prop') get someComputed() {} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `${EO_IMPORTS}${ALIAS_IMPORT} class MyClass {  @alias('unrelatedProp', '${SERVICE_NAME}') someAlias; @computed('unrelatedProp.prop') get someComputed() {} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `${EO_IMPORTS}${ALIAS_IMPORT} Component.extend({ ${SERVICE_NAME}: service(), someAlias1: alias('unrelatedProp', '${SERVICE_NAME}'), someAlias2: computed.alias('unrelatedProp.prop'), someComputed: computed('unrelatedProp.prop', ()=>{}) });`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `${EO_IMPORTS}${ALIAS_IMPORT} Component.extend({  someAlias1: alias('unrelatedProp', '${SERVICE_NAME}'), someAlias2: computed.alias('unrelatedProp.prop'), someComputed: computed('unrelatedProp.prop', ()=>{}) });`,
+            },
+          ],
+          type: 'Property',
+        },
+      ],
+    },
+    /* Multiple classes */
+    {
+      code: `class MyClass1 { @service() ${SERVICE_NAME}; } class MyClass2 { fooFunc() {this.${SERVICE_NAME};} }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass1 {  } class MyClass2 { fooFunc() {this.${SERVICE_NAME};} }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `class MyClass1 { fooFunc() {this.${SERVICE_NAME};} } class MyClass2 { @service() ${SERVICE_NAME}; }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass1 { fooFunc() {this.${SERVICE_NAME};} } class MyClass2 {  }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    /* Nested classes */
+    {
+      code: `class MyClass1 { @service() ${SERVICE_NAME}; fooFunc1() { class MyClass2 { fooFunc2() {this.${SERVICE_NAME};} } } }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass1 {  fooFunc1() { class MyClass2 { fooFunc2() {this.${SERVICE_NAME};} } } }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+    {
+      code: `class MyClass1 { fooFunc1() {this.${SERVICE_NAME};} fooFunc2() { class MyClass2 { @service() ${SERVICE_NAME}; } } }`,
+      output: null,
+      errors: [
+        {
+          messageId: 'main',
+          suggestions: [
+            {
+              messageId: 'removeServiceInjection',
+              output: `class MyClass1 { fooFunc1() {this.${SERVICE_NAME};} fooFunc2() { class MyClass2 {  } } }`,
+            },
+          ],
+          type: 'ClassProperty',
+        },
+      ],
+    },
+  ],
+});