Repository: ember-cli/eslint-plugin-ember
Pull request #887 (2020-07-07 02:13:51)

Add option for custom computed property macros in `no-assignment-of-untracked-properties-used-in-tracking-contexts` rule
This change allows for configuring the dependency key checks to also check custom macros, such as the [t macro](https://ember-intl.github.io/ember-intl/versions/master/docs/guide/translating-text#t) from ember-intl. The configuration options a a bit strange, since I'm expecting that objects are generally used at a specific index, while strings could be used for a range of indices. The possibilities are endless, so I'm trying to support what I expect to be the common cases.

cc @bmish 


Patch:
diff --git a/docs/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.md b/docs/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.md
index 148a86ba..9710c1ae 100644
--- a/docs/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.md
+++ b/docs/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.md
@@ -69,6 +69,82 @@ class MyComponent extends Component {
 
 The autofixer for this rule will update assignments to use `set`. Alternatively, you can begin using tracked properties.
 
+## Configuration
+
+* object -- containing the following properties:
+  * array -- `extraMacros` -- Array of configurations for custom computed property macros which have dependent keys as arguments, each with hte following properties:
+    * string -- `name` -- The name the macro is exported with
+    * string -- `path` -- The file path used for importing the macro
+    * string -- `indexName` -- If this macro can also be imported through an index (like `computed` for `computed.and`), include it here
+    * string -- `indexPath` -- The path for importing the index. For example, with `import { computed } from '@ember/object'` and `computed.and(...)`, `@ember/object` is the `indexPath` and `computed` is the `indexName`.
+    * array -- `argumentFormat` -- array of configurations for how to parse the arguments of the macro to extract the computed dependencies, with at least one of the following properties:
+      * object -- `strings` -- Configuration for extracting raw strings from the argument list, with the following options:
+        * number -- `count` -- How many arguments to consider as dependencies. Use `Number.MAX_VALUE` for all of them.
+        * number -- `startIndex` -- Defaults to zero. If it's something else, that many arguments will be skipped before checking for `count` dependencies.
+      * object -- `objects` -- Configuration for extracting the values of an object as dependency keys, with the following properties:
+        * number -- `index` -- The index of the argument to be checked.
+        * array -- `keys` -- Array of strings for which keys values should be checked for. If not provided, all values will be checked.
+
+Example configuration:
+
+```js
+module.exports = {
+  rules: {
+    'ember/no-assignment-of-untracked-properties-used-in-tracking-contexts': {
+      extraMacros: [
+        {
+          name: 'rejectBy',
+          path: 'custom-macros/macros',
+          indexName: 'customComputed',
+          indexPath: 'custom-macros',
+          argumentFormat: [
+            {
+              strings: {
+                count: 1
+              }
+            }
+          ]
+        },
+        {
+          name: 't',
+          path: 'ember-intl',
+          argumentFormat: [
+            {
+              objects: {
+                index: 1
+              }
+            }
+          ]
+        }
+      ]
+    }
+  }
+};
+```
+
+This configuration works for the [t macro](https://ember-intl.github.io/ember-intl/versions/master/docs/guide/translating-text#t) from ember-intl, and a custom `rejectBy` macro that behaves similarly to `filterBy` (with the second string argument not being a dependency):
+
+```js
+import { A, isArray } from '@ember/array';
+import { get } from '@ember/object';
+
+export default function rejectBy(dependentKey, propertyKey, value) {
+  return computed(`${dependentKey}.@each.${propertyKey}`, function () {
+    const parent = get(this, dependentKey);
+    if (!isArray(parent)) {
+      return A();
+    }
+    let callback;
+    if (arguments.length === 2) {
+      callback = (item) => !get(item, propertyKey);
+    } else {
+      callback = (item) => get(item, propertyKey) !== value;
+    }
+    return A(parent.filter(callback));
+  });
+}
+```
+
 ## References
 
 * [Spec](https://api.emberjs.com/ember/release/functions/@ember%2Fobject/set) for `set()`
diff --git a/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js b/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
index d688cf6e..6efad4e6 100644
--- a/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
+++ b/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
@@ -5,12 +5,44 @@ const types = require('../utils/types');
 const decoratorUtils = require('../utils/decorators');
 const propertySetterUtils = require('../utils/property-setter');
 const { getImportIdentifier } = require('../utils/import');
-const { getMacros } = require('../utils/computed-property-macros');
+const { DEFAULT_MACRO_CONFIGURATIONS } = require('../utils/computed-property-macros');
 const {
   findComputedPropertyDependentKeys,
   keyExistsAsPrefixInList,
 } = require('../utils/computed-property-dependent-keys');
 
+function createMacrosByPathMap(macroConfigurations) {
+  const macrosByPath = new Map();
+
+  macroConfigurations.forEach((config) => {
+    if (!macrosByPath.has(config.path)) {
+      macrosByPath.set(config.path, new Map());
+    }
+
+    macrosByPath.get(config.path).set(config.name, config);
+  });
+
+  return macrosByPath;
+}
+
+function createMacrosByIndexPathMap(macroConfigurations) {
+  const macrosByIndexPath = new Map();
+
+  macroConfigurations.forEach((config) => {
+    if (!macrosByIndexPath.has(config.indexPath)) {
+      macrosByIndexPath.set(config.indexPath, new Map());
+    }
+
+    if (!macrosByIndexPath.get(config.indexPath).has(config.indexName)) {
+      macrosByIndexPath.get(config.indexPath).set(config.indexName, new Map());
+    }
+
+    macrosByIndexPath.get(config.indexPath).get(config.indexName).set(config.name, config);
+  });
+
+  return macrosByIndexPath;
+}
+
 const ERROR_MESSAGE =
   "Use `set(this, 'propertyName', 'value')` instead of assignment for untracked properties that are used as computed property dependencies (or convert to using tracked properties).";
 
@@ -63,7 +95,95 @@ module.exports = {
         'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.md',
     },
     fixable: 'code',
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          extraMacros: {
+            type: 'array',
+            items: {
+              type: 'object',
+              required: ['path', 'name'],
+              dependencies: {
+                indexPath: ['indexName'],
+                indexName: ['indexPath'],
+              },
+              properties: {
+                path: {
+                  type: 'string',
+                  minLength: 1,
+                  // Extra macros cannot be in @ember/
+                  pattern: '^(?!@ember/).+/',
+                },
+                name: {
+                  type: 'string',
+                  minLength: 1,
+                },
+                indexPath: {
+                  type: 'string',
+                  minLength: 1,
+                  // Extra macros cannot be in @ember/
+                  pattern: '^(?!@ember/).+/',
+                },
+                indexName: {
+                  type: 'string',
+                  minLength: 1,
+                },
+                argumentFormat: {
+                  type: 'array',
+                  minItems: 1,
+                  items: {
+                    type: 'object',
+                    properties: {
+                      strings: {
+                        type: 'object',
+                        properties: {
+                          startIndex: {
+                            type: 'number',
+                            multipleOf: 1,
+                            default: 0,
+                            minimum: 0,
+                          },
+                          count: {
+                            type: 'number',
+                            default: Number.MAX_VALUE,
+                            multipleOf: 1,
+                            minimum: 1,
+                          },
+                        },
+                      },
+                      objects: {
+                        type: 'object',
+                        required: ['index'],
+                        properties: {
+                          keys: {
+                            type: 'array',
+                            minItems: 1,
+                            items: {
+                              type: 'string',
+                              minLength: 1,
+                            },
+                          },
+                          index: {
+                            type: 'number',
+                            multipleOf: 1,
+                            minimum: 0,
+                          },
+                        },
+                        additionalProperties: false,
+                      },
+                    },
+                    additionalProperties: false,
+                  },
+                },
+              },
+              additionalProperties: false,
+            },
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
   },
 
   ERROR_MESSAGE,
@@ -74,29 +194,51 @@ module.exports = {
       return {};
     }
 
+    const options = context.options[0] || { extraMacros: [] };
+
+    const macroConfigurations = [...DEFAULT_MACRO_CONFIGURATIONS, ...options.extraMacros];
+
+    const macrosByPath = createMacrosByPathMap(macroConfigurations);
+    const macrosByIndexPath = createMacrosByIndexPathMap(macroConfigurations);
+
     // State being tracked for this file.
-    let computedImportName = undefined;
     let trackedImportName = undefined;
+    let computedImportName = undefined;
     let setImportName = undefined;
-    let macroImportNames = new Map();
+    let macrosByName = new Map();
+    let macrosByIndexName = new Map();
 
     // State being tracked for the current class we're inside.
     const classStack = new Stack();
 
     return {
       ImportDeclaration(node) {
+        const importPath = node.source.value;
+
+        // Checking for something like `import { readOnly } from '@ember/object/computed;`
+        if (macrosByPath.has(importPath)) {
+          const newMacroImportNameEntries = [...macrosByPath.get(importPath)]
+            .map(([name, config]) => [getImportIdentifier(node, importPath, name), config])
+            .filter(([importedName]) => importedName);
+          macrosByName = new Map([...macrosByName, ...newMacroImportNameEntries]);
+        }
+
+        // Checking for something like `import { computed } from '@ember/object;`
+        if (macrosByIndexPath.has(importPath)) {
+          const newMacroIndexImportEntries = [...macrosByIndexPath.get(importPath)]
+            .map(([indexName, propertyConfigs]) => [
+              getImportIdentifier(node, importPath, indexName),
+              propertyConfigs,
+            ])
+            .filter(([importedName]) => importedName);
+
+          macrosByIndexName = new Map([...macrosByIndexName, ...newMacroIndexImportEntries]);
+        }
+
         if (node.source.value === '@ember/object') {
+          setImportName = setImportName || getImportIdentifier(node, '@ember/object', 'set');
           computedImportName =
             computedImportName || getImportIdentifier(node, '@ember/object', 'computed');
-          setImportName = setImportName || getImportIdentifier(node, '@ember/object', 'set');
-        } else if (node.source.value === '@ember/object/computed') {
-          macroImportNames = new Map(
-            getMacros().map((macro) => [
-              macro,
-              macroImportNames.get(macro) ||
-                getImportIdentifier(node, '@ember/object/computed', macro),
-            ])
-          );
         } else if (node.source.value === '@glimmer/tracking') {
           trackedImportName =
             trackedImportName || getImportIdentifier(node, '@glimmer/tracking', 'tracked');
@@ -109,7 +251,8 @@ module.exports = {
         const computedPropertyDependentKeys = findComputedPropertyDependentKeys(
           node,
           computedImportName,
-          macroImportNames
+          macrosByName,
+          macrosByIndexName
         );
 
         // Gather tracked properties from this class.
@@ -133,7 +276,8 @@ module.exports = {
           const computedPropertyDependentKeys = findComputedPropertyDependentKeys(
             node,
             computedImportName,
-            macroImportNames
+            macrosByName,
+            macrosByIndexName
           );
 
           // No tracked properties in classic classes.
diff --git a/lib/utils/computed-property-dependent-keys.js b/lib/utils/computed-property-dependent-keys.js
index 8ff4f9d5..63836c95 100644
--- a/lib/utils/computed-property-dependent-keys.js
+++ b/lib/utils/computed-property-dependent-keys.js
@@ -5,11 +5,7 @@ const { getName } = require('../utils/utils');
 const types = require('../utils/types');
 const decoratorUtils = require('../utils/decorators');
 const assert = require('assert');
-const {
-  getMacrosFromImportNames,
-  getTrackedArgumentCount,
-  macroToCanonicalName,
-} = require('../utils/computed-property-macros');
+const { getMacrosFromImports } = require('../utils/computed-property-macros');
 
 module.exports = {
   collapseKeys,
@@ -173,13 +169,32 @@ function keyExistsAsPrefixInList(keys, key) {
   return keys.some((currentKey) => computedPropertyDependencyMatchesKeyPath(currentKey, key));
 }
 
+/**
+ * A configuration for determining what arguments of a computed macro should be used as dependency keys
+ * @typedef {Object} ComputedMacroConfiguration
+ * @property {Object} [strings] - the configuration for which string arguments should be used
+ * @property {number} strings.startIndex - the first index to look for string arguments
+ * @property {number} strings.count - how many arguments to look at, including the start index.
+ * @property {Object} [objects] - the configuration for which object arguments' values should  be used
+ * @property {Object} objects.index - the argument index to look for object values in
+ * @property {Object} [objects.allowedKeys] - if present, indicates which keys should be looked at for getting values.
+ */
+
 /**
  * Gets the set of computed property dependency keys used inside a class.
  *
  * @param {Node} nodeClass - Node for the class
+ * @param {string} [computedImportName] - The name by which computed from '@ember/object' as imported with.
+ * @param {Map<String, ComputedMacroConfiguration>} - A mapping from the names by which computed macros were imported to the configuration for what keys they depend on
+ * @param {Map<String, Map<String, ComputedMacroConfiguration>} - A mapping from the names by which a computed macro index was imported to a map from what properties it has to what their macro configurations are.
  * @returns {Set<string>} - set of dependent keys used inside the class
  */
-function findComputedPropertyDependentKeys(nodeClass, computedImportName, macroImportNames) {
+function findComputedPropertyDependentKeys(
+  nodeClass,
+  computedImportName,
+  macrosByName,
+  macrosByIndexName
+) {
   if (types.isClassDeclaration(nodeClass)) {
     // Native JS class.
     return new Set(
@@ -191,19 +206,14 @@ function findComputedPropertyDependentKeys(nodeClass, computedImportName, macroI
         }
 
         // Check for a computed macro.
-        const macroNames = getMacrosFromImportNames(computedImportName, macroImportNames);
+        const macroConfigsByName = getMacrosFromImports(macrosByName, macrosByIndexName);
         const macroDecorator = decoratorUtils.findDecoratorByNameCallback(node, (decoratorName) =>
-          macroNames.has(decoratorName)
+          macroConfigsByName.has(decoratorName)
         );
         if (macroDecorator) {
           return getComputedPropertyDependentKeys(
             macroDecorator.expression,
-            getTrackedArgumentCount(
-              macroToCanonicalName(
-                decoratorUtils.getDecoratorName(macroDecorator),
-                macroImportNames
-              )
-            )
+            macroConfigsByName.get(decoratorUtils.getDecoratorName(macroDecorator))
           );
         }
 
@@ -229,12 +239,9 @@ function findComputedPropertyDependentKeys(nodeClass, computedImportName, macroI
               }
 
               // Check for a computed macro.
-              const macroNames = getMacrosFromImportNames(computedImportName, macroImportNames);
-              if (macroNames.has(name)) {
-                return getComputedPropertyDependentKeys(
-                  node.value,
-                  getTrackedArgumentCount(macroToCanonicalName(name, macroImportNames))
-                );
+              const macroConfigsByName = getMacrosFromImports(macrosByName, macrosByIndexName);
+              if (macroConfigsByName.has(name)) {
+                return getComputedPropertyDependentKeys(node.value, macroConfigsByName.get(name));
               }
             }
             return [];
@@ -249,22 +256,85 @@ function findComputedPropertyDependentKeys(nodeClass, computedImportName, macroI
   return new Set();
 }
 
+/**
+ * Returns an array of the dependency keys for a particular argument and index for a particular configuration.
+ *
+ * @param {Object} argument - the argument to a computed macro
+ * @param {number} index - the index of this particular argument for the computed macro
+ * @param {ComputedMacroConfiguration} config - the configuration indicating how dependent keys should be gotten from the argument
+ * @returns {String[]} - the list of string dependent keys indicated by the configuration.
+ */
+function getComputedPropertyDependentKeysForConfig(argument, index, { strings, objects }) {
+  if (strings) {
+    const { startIndex, count } = strings;
+    if (
+      index >= startIndex &&
+      index < startIndex + count &&
+      argument.type === 'Literal' &&
+      typeof argument.value === 'string'
+    ) {
+      return [argument.value];
+    }
+  }
+
+  if (objects) {
+    const { index: configIndex, keys: allowedKeys } = objects;
+
+    if (index === configIndex && argument.type === 'ObjectExpression') {
+      return argument.properties
+        .filter(({ key }) => {
+          if (!allowedKeys) {
+            return true;
+          }
+
+          if (key.type === 'Identifier') {
+            return allowedKeys.includes(key.name);
+          } else if (key.type === 'Literal' && typeof key.value === 'string') {
+            return allowedKeys.includes(key.value);
+          } else {
+            return false;
+          }
+        })
+        .filter(
+          (property) =>
+            property.value.type === 'Literal' && typeof property.value.value === 'string'
+        )
+        .map((property) => property.value.value);
+    }
+  }
+
+  return [];
+}
+
 /**
  * Gets the list of string dependent keys from a computed property.
  *
  * @param {Node} node - the computed property node
- * @param {number} dependentKeyArgumentCount - number of arguments to check for dependent keys
+ * @param {ComputedMacroConfiguration} [computedDependenciesConfig=null] - the configuration indicating how dependent keys should be gotten from the node.
+ *   If not present, all string arguments will be used.
  * @returns {String[]} - the list of string dependent keys from this computed property
  */
-function getComputedPropertyDependentKeys(node, dependentKeyArgumentCount = Number.MAX_VALUE) {
+function getComputedPropertyDependentKeys(node, computedDependenciesConfig = null) {
   if (!node.arguments) {
     return [];
   }
 
-  return expandKeys(
-    node.arguments
-      .slice(0, dependentKeyArgumentCount)
-      .filter((arg) => arg.type === 'Literal' && typeof arg.value === 'string')
-      .map((node) => node.value)
-  );
+  const baseKeys = javascriptUtils.flatMap(node.arguments, (argument, index) => {
+    if (!computedDependenciesConfig) {
+      if (argument.type === 'Literal' && typeof argument.value === 'string') {
+        return [argument.value];
+      } else {
+        return [];
+      }
+    }
+
+    return javascriptUtils.flatMap(
+      computedDependenciesConfig.argumentFormat,
+      ({ strings, objects }) => {
+        return getComputedPropertyDependentKeysForConfig(argument, index, { strings, objects });
+      }
+    );
+  });
+
+  return expandKeys(baseKeys);
 }
diff --git a/lib/utils/computed-property-macros.js b/lib/utils/computed-property-macros.js
index 1443aa16..1b962ca3 100644
--- a/lib/utils/computed-property-macros.js
+++ b/lib/utils/computed-property-macros.js
@@ -1,11 +1,5 @@
 const assert = require('assert');
-
-module.exports = {
-  getMacros,
-  getMacrosFromImportNames,
-  getTrackedArgumentCount,
-  macroToCanonicalName,
-};
+const { flatMap } = require('./javascript');
 
 /**
  * Example macros:
@@ -47,6 +41,25 @@ const MACROS_TO_TRACKED_ARGUMENT_COUNT = {
   uniqBy: 1,
 };
 
+// Configurations for the default macros that match the configurations for the
+// no-assignment-of-untracked-properties-used-in-tracking-contexts rule
+const DEFAULT_MACRO_CONFIGURATIONS = Object.entries(MACROS_TO_TRACKED_ARGUMENT_COUNT).map(
+  ([macroName, argumentCount]) => ({
+    name: macroName,
+    path: '@ember/object/computed',
+    indexName: 'computed',
+    indexPath: '@ember/object',
+    argumentFormat: [
+      {
+        strings: {
+          startIndex: 0,
+          count: argumentCount,
+        },
+      },
+    ],
+  })
+);
+
 /**
  * @returns {string[]} - a list of all macros.
  */
@@ -61,10 +74,12 @@ function getMacros() {
  * @param {set<string>} macroImportNames
  * @returns {set<string>} - a set containing all possible macro names to check for.
  */
-function getMacrosFromImportNames(computedImportName, macroImportNames) {
-  return new Set([
-    ...macroImportNames.values(),
-    ...(computedImportName ? getMacros().map((macro) => `${computedImportName}.${macro}`) : []),
+function getMacrosFromImports(macrosByImport, macrosByIndexImport) {
+  return new Map([
+    ...macrosByImport,
+    ...flatMap([...macrosByIndexImport], ([indexName, propertyConfigs]) => {
+      return [...propertyConfigs].map(([name, config]) => [`${indexName}.${name}`, config]);
+    }),
   ]);
 }
 
@@ -92,3 +107,11 @@ function macroToCanonicalName(macro, macroImportNames) {
         (canonicalName) => macroImportNames.get(canonicalName) === macro
       );
 }
+
+module.exports = {
+  getMacros,
+  getMacrosFromImports,
+  getTrackedArgumentCount,
+  macroToCanonicalName,
+  DEFAULT_MACRO_CONFIGURATIONS,
+};
diff --git a/lib/utils/javascript.js b/lib/utils/javascript.js
index 1e250e9f..868ed704 100644
--- a/lib/utils/javascript.js
+++ b/lib/utils/javascript.js
@@ -49,7 +49,7 @@ function flat(arr) {
  * @returns {Array<U>}
  */
 function flatMap(array, callback) {
-  return array.reduce((result, item) => result.concat(callback(item)), []);
+  return array.reduce((result, item, index) => result.concat(callback(item, index)), []);
 }
 
 function removeWhitespace(str) {


Test patch:
diff --git a/tests/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js b/tests/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
index d10144fd..2846ece9 100644
--- a/tests/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
+++ b/tests/lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js
@@ -24,6 +24,7 @@ ruleTester.run('no-assignment-of-untracked-properties-used-in-tracking-contexts'
     // **********************
     // Native class
     // **********************
+
     {
       // Assignment of property which is not used as a dependent key.
       code: `
@@ -516,5 +517,87 @@ import { mapBy } from '@ember/object/computed';
       filename: '/components/foo.js',
       errors: [{ message: ERROR_MESSAGE, type: 'AssignmentExpression' }],
     },
+    {
+      // Custom macro for direct strings
+      options: [
+        {
+          extraMacros: [
+            {
+              path: 'custom-macros/custom',
+              name: 'rejectBy',
+              argumentFormat: [
+                {
+                  strings: {
+                    count: 1,
+                  },
+                },
+              ],
+            },
+          ],
+        },
+      ],
+      code: `
+      import { rejectBy } from 'custom-macros/custom';
+      import Component from '@ember/component';
+      Component.extends({
+        myProp: rejectBy('chores', 'done', true),
+        myFunction1() { this.chores = 123; },
+        myFunction2() { this.done = 123; }, // Allowed since this isn't a dependent key.
+      })`,
+      output: `
+      import { set } from '@ember/object';
+import { rejectBy } from 'custom-macros/custom';
+      import Component from '@ember/component';
+      Component.extends({
+        myProp: rejectBy('chores', 'done', true),
+        myFunction1() { set(this, 'chores', 123); },
+        myFunction2() { this.done = 123; }, // Allowed since this isn't a dependent key.
+      })`,
+      filename: '/components/foo.js',
+      errors: [{ message: ERROR_MESSAGE, type: 'AssignmentExpression' }],
+    },
+    {
+      // Custom macro for object values
+      options: [
+        {
+          extraMacros: [
+            {
+              path: 'custom-macros/custom',
+              name: 't',
+              indexPath: 'custom-macros/ind',
+              indexName: 'customComputed',
+              argumentFormat: [
+                {
+                  objects: {
+                    index: 1,
+                  },
+                },
+              ],
+            },
+          ],
+        },
+      ],
+      code: `
+      import { customComputed } from 'custom-macros/ind';
+      import Component from '@ember/component';
+      Component.extends({
+        myProp: customComputed.t('unused', { tKey: 'dependency.key' }),
+        myFunction1() { this.dependency = 123; },
+        myFunction2() { this.unused = 123; }, // Allowed since this isn't a dependent key.
+        myFunction3() { this.tKey = 123; }, // Allowed since this is a key, not a value.
+      })`,
+      output: `
+      import { set } from '@ember/object';
+import { customComputed } from 'custom-macros/ind';
+      import Component from '@ember/component';
+      Component.extends({
+        myProp: customComputed.t('unused', { tKey: 'dependency.key' }),
+        myFunction1() { set(this, 'dependency', 123); },
+        myFunction2() { this.unused = 123; }, // Allowed since this isn't a dependent key.
+        myFunction3() { this.tKey = 123; }, // Allowed since this is a key, not a value.
+      })`,
+      filename: '/components/foo.js',
+      errors: [{ message: ERROR_MESSAGE, type: 'AssignmentExpression' }],
+    },
   ],
 });
diff --git a/tests/lib/utils/computed-property-dependent-keys-test.js b/tests/lib/utils/computed-property-dependent-keys-test.js
index c6809b7e..886a3418 100644
--- a/tests/lib/utils/computed-property-dependent-keys-test.js
+++ b/tests/lib/utils/computed-property-dependent-keys-test.js
@@ -73,65 +73,209 @@ describe('keyExistsAsPrefixInList', () => {
 describe('findComputedPropertyDependentKeys', () => {
   it('returns the right result with native class', () => {
     const computedImportName = 'c';
-    const macroImportNames = new Map([
-      ['readOnly', 'readOnlyRenamed'],
-      ['mapBy', 'mapBy'],
+    const macrosByImport = new Map([
+      [
+        'rejectBy',
+        {
+          argumentFormat: [
+            {
+              strings: {
+                startIndex: 0,
+                count: 1,
+              },
+            },
+          ],
+        },
+      ],
+      [
+        't',
+        {
+          argumentFormat: [
+            {
+              objects: { index: 1, keys: ['foo', 'bar', 'baz'] },
+            },
+          ],
+        },
+      ],
     ]);
+
+    const macrosByIndexImport = new Map([
+      [
+        'c',
+        new Map([
+          [
+            'readOnly',
+            {
+              argumentFormat: [
+                {
+                  strings: {
+                    startIndex: 0,
+                    count: 1,
+                  },
+                },
+              ],
+            },
+          ],
+        ]),
+      ],
+      [
+        'somethingElse',
+        new Map([
+          [
+            'strange',
+            {
+              argumentFormat: [
+                {
+                  strings: {
+                    startIndex: 1,
+                    count: 2,
+                  },
+                },
+                {
+                  objects: {
+                    index: 3,
+                  },
+                },
+              ],
+            },
+          ],
+        ]),
+      ],
+    ]);
+
     const classNode = babelEslint.parse(`
-    import { computed as c } from '@ember/object';
-    import { readOnly as readOnlyRenamed, mapBy } from '@ember/object/computed';
-    import Component from '@ember/component';
-    class MyClass extends Component {
-      @c('x') get myProp() {}
-      @c('x', 'x') get myProp() {} // Intentional duplicate dependent key.
-      @c() get myProp() {}
-      @readOnlyRenamed('y') get myProp() {}
-      @c.readOnly('z') get myProp() {}
-      @mapBy('chores', 'done', true) get myProp() {}
-      @random() get myProp() {}
-      @random('') get myProp() {}
-      @random('z') get myProp() {}
-      @computed('bad') get myProp() {} // Wrong name.
-      @readOnly('bad') get myProp() {} // Wrong name.
-    }`).body[3];
+      import { computed as c } from '@ember/object';
+      import { rejectBy, t } from 'custom-macros/macros';
+      import { somethingElse } from 'custom-macros';
+      import Component from '@ember/component';
+
+      class MyClass extends Component {
+        @c('x') get myProp() {}
+        @c('x', 'x') get myProp() {} // Intentional duplicate dependent key.
+        @c() get myProp() {}
+        @c.readOnly('y') get myProp() {}
+        @c.notSpecified('z') get myProp() {}
+        @rejectBy('chores', 'done', true) get myProp() {}
+        @random() get myProp() {}
+        @random('') get myProp() {}
+        @random('z') get myProp() {}
+        @computed('bad') get myProp() {} // Wrong name.
+        @readOnly('bad') get myProp() {} // Wrong name.
+        @t('ignored', { foo: 'kept', "bar": 'also-kept', unknown: 'not-kept' }) stringProp;
+        @somethingElse.strange('ignored', 'yes', 'also-yes', { arbitrary: 'indeed' }) otherProp;
+      }
+    `).body[4];
     expect([
       ...cpdkUtils.findComputedPropertyDependentKeys(
         classNode,
         computedImportName,
-        macroImportNames
+        macrosByImport,
+        macrosByIndexImport
       ),
-    ]).toStrictEqual(['x', 'y', 'z', 'chores']);
+    ]).toStrictEqual(['x', 'y', 'chores', 'kept', 'also-kept', 'yes', 'also-yes', 'indeed']);
   });
 
   it('returns the right result with classic class', () => {
     const computedImportName = 'c';
-    const macroImportNames = new Map([
-      ['readOnly', 'readOnlyRenamed'],
-      ['mapBy', 'mapBy'],
+    const macrosByImport = new Map([
+      [
+        'rejectBy',
+        {
+          argumentFormat: [
+            {
+              strings: {
+                startIndex: 0,
+                count: 1,
+              },
+            },
+          ],
+        },
+      ],
+      [
+        't',
+        {
+          argumentFormat: [
+            {
+              objects: { index: 1, keys: ['foo', 'bar', 'baz'] },
+            },
+          ],
+        },
+      ],
     ]);
+
+    const macrosByIndexImport = new Map([
+      [
+        'c',
+        new Map([
+          [
+            'readOnly',
+            {
+              argumentFormat: [
+                {
+                  strings: {
+                    startIndex: 0,
+                    count: 1,
+                  },
+                },
+              ],
+            },
+          ],
+        ]),
+      ],
+      [
+        'somethingElse',
+        new Map([
+          [
+            'strange',
+            {
+              argumentFormat: [
+                {
+                  strings: {
+                    startIndex: 1,
+                    count: 2,
+                  },
+                },
+                {
+                  objects: {
+                    index: 3,
+                  },
+                },
+              ],
+            },
+          ],
+        ]),
+      ],
+    ]);
+
     const classNode = babelEslint.parse(`
-    import { computed as c } from '@ember/object';
-    import { readOnly as readOnlyRenamed, mapBy } from '@ember/object/computed';
-    import Component from '@ember/component';
-    Component.extend({
-      prop1: c('x'),
-      prop2: c('x', 'x'), // Intentional duplicate dependent key.
-      prop3: c(),
-      prop4: readOnlyRenamed('y'),
-      prop5: c?.readOnly('z'), // Macro plus optional expression.
-      prop6: mapBy('chores', 'done', true),
-      prop7: random(),
-      prop8: random(''),
-      prop9: random('z'),
-      prop10: computed('bad'), // Wrong name.
-      prop11: readOnly('bad'), // Wrong name.
-    });`).body[3].expression;
+      import { computed as c } from '@ember/object';
+      import { rejectBy, t } from 'custom-macros/macros';
+      import { somethingElse } from 'custom-macros';
+      import Component from '@ember/component';
+
+      Component.extend({
+        prop1: c('x'),
+        prop2: c('x', 'x'), // Intentional duplicate dependent key.
+        prop3: c(),
+        prop5: c?.readOnly('y'), // Macro plus optional expression.
+        prop4: c.notSpecified('z'),
+        prop6: rejectBy('chores', 'done', true),
+        prop7: random(),
+        prop8: random(''),
+        prop9: random('z'),
+        prop10: computed('bad'), // Wrong name.
+        prop11: readOnly('bad'), // Wrong name.
+        prop12: t('ignored', { foo: 'kept', "bar": 'also-kept', unknown: 'not-kept' }),
+        prop13: somethingElse.strange('ignored', 'yes', 'also-yes', { arbitrary: 'indeed' }),
+      });
+    `).body[4].expression;
     expect([
       ...cpdkUtils.findComputedPropertyDependentKeys(
         classNode,
         computedImportName,
-        macroImportNames
+        macrosByImport,
+        macrosByIndexImport
       ),
-    ]).toStrictEqual(['x', 'y', 'z', 'chores']);
+    ]).toStrictEqual(['x', 'y', 'chores', 'kept', 'also-kept', 'yes', 'also-yes', 'indeed']);
   });
 });
diff --git a/tests/lib/utils/computed-property-macros-test.js b/tests/lib/utils/computed-property-macros-test.js
index 3e51144c..cc8a19ea 100644
--- a/tests/lib/utils/computed-property-macros-test.js
+++ b/tests/lib/utils/computed-property-macros-test.js
@@ -1,6 +1,6 @@
 const {
   getMacros,
-  getMacrosFromImportNames,
+  getMacrosFromImports,
   getTrackedArgumentCount,
   macroToCanonicalName,
 } = require('../../../lib/utils/computed-property-macros');
@@ -11,13 +11,31 @@ describe('getMacros', () => {
   });
 });
 
-describe('getMacrosFromImportNames', () => {
+describe('getMacrosFromImports', () => {
   it('returns some of the correct macros', () => {
-    const result = [
-      ...getMacrosFromImportNames('computedRenamed', new Map([['readOnly', 'readOnlyRenamed']])),
-    ];
+    const macrosByImport = new Map([
+      ['readOnlyRenamed', 'read-only-config'],
+      ['aliasRenamed', 'alias-config'],
+    ]);
+    const macrosByIndexImport = new Map([
+      [
+        'computed',
+        new Map([
+          ['readOnly', 'read-only-config'],
+          ['alias', 'alias-config'],
+        ]),
+      ],
+      ['customComputed', new Map([['rejectBy', 'reject-by-config']])],
+    ]);
+    const result = [...getMacrosFromImports(macrosByImport, macrosByIndexImport)];
     expect(result).toStrictEqual(
-      expect.arrayContaining(['computedRenamed.readOnly', 'readOnlyRenamed'])
+      expect.arrayContaining([
+        ['readOnlyRenamed', 'read-only-config'],
+        ['aliasRenamed', 'alias-config'],
+        ['computed.readOnly', 'read-only-config'],
+        ['computed.alias', 'alias-config'],
+        ['customComputed.rejectBy', 'reject-by-config'],
+      ])
     );
   });
 });