Repository: ember-cli/eslint-plugin-ember
Pull request #1164 (2021-04-21 20:32:56)

Account for `observes` decorator in `no-unused-services` rule
Fixes https://github.com/ember-cli/eslint-plugin-ember/issues/1157

A previous PR accounted for `observer` but did not account for the `observes` decorator.


Patch:
diff --git a/lib/rules/no-unused-services.js b/lib/rules/no-unused-services.js
index e778226f..5b1af4ef 100644
--- a/lib/rules/no-unused-services.js
+++ b/lib/rules/no-unused-services.js
@@ -53,6 +53,7 @@ module.exports = {
     let importedGetPropertiesName;
     let importedInjectName;
     let importedObserverName;
+    let importedObservesName;
     const macros = getMacros();
     const importedMacros = {};
 
@@ -116,7 +117,8 @@ module.exports = {
             getImportIdentifier(node, '@ember/object', 'getProperties');
           importedObserverName =
             importedObserverName || getImportIdentifier(node, '@ember/object', 'observer');
-        } else if (node.source.value === '@ember/object/computed') {
+        }
+        if (node.source.value === '@ember/object/computed') {
           for (const spec of node.specifiers) {
             const name = spec.imported.name;
             if (macros.includes(name)) {
@@ -124,10 +126,17 @@ module.exports = {
               importedMacros[localName] = name;
             }
           }
-        } else if (node.source.value === '@ember/service') {
+        }
+        if (node.source.value === '@ember/service') {
           importedInjectName =
             importedInjectName || getImportIdentifier(node, '@ember/service', 'inject');
-        } else if (node.source.value === 'ember') {
+        }
+        if (node.source.value === '@ember-decorators/object') {
+          importedObservesName =
+            importedObservesName ||
+            getImportIdentifier(node, '@ember-decorators/object', 'observes');
+        }
+        if (node.source.value === 'ember') {
           importedEmberName = importedEmberName || getImportIdentifier(node, 'ember');
         }
       },
@@ -281,6 +290,27 @@ module.exports = {
           reportInstances();
         }
       },
+      // @observes('foo', ...)
+      Decorator(node) {
+        // If Ember and Ember.inject weren't imported OR observes wasn't imported, skip out early
+        if ((!importedEmberName && !importedInjectName) || !importedObservesName) {
+          return;
+        }
+
+        const currentClass = classStack.peek();
+        if (
+          currentClass &&
+          emberUtils.isObserverDecorator(node, importedObservesName) &&
+          types.isCallExpression(node.expression)
+        ) {
+          for (const elem of node.expression.arguments) {
+            if (types.isStringLiteral(elem)) {
+              const name = splitValue(elem.value);
+              currentClass.uses.add(name);
+            }
+          }
+        }
+      },
       // foo: service(...)
       Property(node) {
         // If Ember and Ember.inject weren't imported, skip out early


Test patch:
diff --git a/tests/lib/rules/no-unused-services.js b/tests/lib/rules/no-unused-services.js
index e52925ae..8f7b0858 100644
--- a/tests/lib/rules/no-unused-services.js
+++ b/tests/lib/rules/no-unused-services.js
@@ -24,6 +24,8 @@ const RENAMED_EO_IMPORTS =
   "import {computed as cp, get as g, getProperties as gp, observer as ob} from '@ember/object';";
 const ALIAS_IMPORT = "import {alias} from '@ember/object/computed';";
 const RENAMED_ALIAS_IMPORT = "import {alias as al} from '@ember/object/computed';";
+const OBSERVES_IMPORT = "import {observes} from '@ember-decorators/object';";
+const RENAMED_OBSERVES_IMPORT = "import {observes as obs} from '@ember-decorators/object';";
 const EMBER_IMPORT = "import Ember from 'ember';";
 const RENAMED_EMBER_IMPORT = "import Em from 'ember';";
 
@@ -112,6 +114,8 @@ function generateComputedUseCasesFor(propertyName, renamed = false) {
 function generateObserverUseCasesFor(propertyName, renamed = false) {
   const observerName = renamed ? 'ob' : 'observer';
   const observerImport = renamed ? RENAMED_EO_IMPORTS : EO_IMPORTS;
+  const observesName = renamed ? 'obs' : 'observes';
+  const observesImport = renamed ? RENAMED_OBSERVES_IMPORT : OBSERVES_IMPORT;
   const emberName = renamed ? 'Em' : 'Ember';
   const emberImport = renamed ? RENAMED_EMBER_IMPORT : EMBER_IMPORT;
   return [
@@ -119,6 +123,7 @@ function generateObserverUseCasesFor(propertyName, renamed = false) {
     `${SERVICE_IMPORT}${observerImport} Component.extend({ ${SERVICE_NAME}: service(), someObserved: on('init', ${observerName}('${propertyName}.prop', ()=>{})) });`,
     `${SERVICE_IMPORT}${emberImport} Component.extend({ ${SERVICE_NAME}: service(), someObserved: ${emberName}.observer('${propertyName}.prop', ()=>{}) });`,
     `${SERVICE_IMPORT}${emberImport} Component.extend({ ${SERVICE_NAME}: service(), someObserved: on('init', ${emberName}.observer('${propertyName}.prop', ()=>{})) });`,
+    `${SERVICE_IMPORT}${observesImport} class MyClass { @service() ${propertyName}; @${observesName}('${propertyName}.prop') get someVal() {} }`,
   ];
 }