Repository: ember-cli/eslint-plugin-ember
Pull request #1194 (2021-05-10 19:38:54)

Add new rule `no-restricted-property-modifications`
Porting over and improving the rule from [here](https://github.com/square/eslint-plugin-square/blob/master/docs/rules/no-modifying-immutable-properties.md).


Patch:
diff --git a/README.md b/README.md
index 62c5f918..ad422b3e 100644
--- a/README.md
+++ b/README.md
@@ -155,6 +155,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | [no-html-safe](./docs/rules/no-html-safe.md) | disallow the use of `htmlSafe` |  |  |
 | [no-incorrect-calls-with-inline-anonymous-functions](./docs/rules/no-incorrect-calls-with-inline-anonymous-functions.md) | disallow inline anonymous functions as arguments to `debounce`, `once`, and `scheduleOnce` | :white_check_mark: |  |
 | [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | disallow usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. | :white_check_mark: |  |
+| [no-restricted-property-modifications](./docs/rules/no-restricted-property-modifications.md) | disallow modifying the specified properties |  | :wrench: |
 | [require-fetch-import](./docs/rules/require-fetch-import.md) | enforce explicit import for `fetch()` |  |  |
 
 ### Routes
diff --git a/docs/rules/no-restricted-property-modifications.md b/docs/rules/no-restricted-property-modifications.md
new file mode 100644
index 00000000..0dad67c6
--- /dev/null
+++ b/docs/rules/no-restricted-property-modifications.md
@@ -0,0 +1,60 @@
+# no-restricted-property-modifications
+
+:wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+There are some properties, especially globally-injected ones, that you may want to treat as read-only, and ensure that no one modifies them.
+
+## Rule Details
+
+This rule prevents modifying the specified properties.
+
+It also disallows using computed property macros like `alias` and `reads` that enable the specified properties to be indirectly modified.
+
+## Examples
+
+All examples assume a configuration of `properties: ['currentUser']`.
+
+Examples of **incorrect** code for this rule:
+
+```js
+import Component from '@ember/component';
+import { alias, reads } from '@ember/object/computed';
+
+export default class MyComponent extends Component {
+  @alias('currentUser') aliasForCurrentUser1; // Not allowed
+  @reads('currentUser') aliasForCurrentUser2; // Not allowed
+
+  @alias('currentUser.somePermission1') somePermission1; // Not allowed
+  @reads('currentUser.somePermission2') somePermission2; // Not allowed
+
+  myFunction() {
+    this.set('currentUser', {}); // Not allowed
+    this.set('currentUser.somePermission', true); // Not allowed
+
+    this.currentUser = {}; // Not allowed
+    this.currentUser.somePermission = true; // Not allowed
+  }
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+import Component from '@ember/component';
+import { readOnly } from '@ember/object/computed';
+
+export default class MyComponent extends Component {
+  @readOnly('currentUser.somePermission') somePermission; // Allowed
+
+  myFunction() {
+    console.log(this.currentUser.somePermission); // Allowed
+  }
+}
+```
+
+## Configuration
+
+* object -- containing the following properties:
+  * `String[]` -- `properties` -- array of names of properties that should not be modified (modifying child/nested/sub-properties of these is also not allowed)
+
+Not yet implemented: There is currently no way to configure whether sub-properties are restricted from modification. To make this configurable, the `properties` array option could be updated to also accept objects of the form `{ name: 'myPropertyName', includeSubProperties: false }` where `includeSubProperties` defaults to `true`.
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 bb26228d..a087ed43 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
@@ -59,9 +59,9 @@ function findTrackedProperties(nodeClassDeclaration, trackedImportName) {
         (node) =>
           types.isClassProperty(node) &&
           decoratorUtils.hasDecorator(node, trackedImportName) &&
-          (types.isIdentifier(node.key) || types.isStringLiteral(node.key))
+          types.isIdentifier(node.key)
       )
-      .map((node) => node.key.name || node.key.value)
+      .map((node) => node.key.name)
   );
 }
 
diff --git a/lib/rules/no-controllers.js b/lib/rules/no-controllers.js
index c9b36cbe..e82b65f8 100644
--- a/lib/rules/no-controllers.js
+++ b/lib/rules/no-controllers.js
@@ -53,8 +53,8 @@ function classDeclarationHasProperty(classDeclaration, propertyName) {
     classDeclaration.body.body.some(
       (item) =>
         types.isClassProperty(item) &&
-        ((types.isIdentifier(item.key) && item.key.name === propertyName) ||
-          (types.isStringLiteral(item.key) && item.key.value === propertyName))
+        types.isIdentifier(item.key) &&
+        item.key.name === propertyName
     )
   );
 }
@@ -69,9 +69,7 @@ function callExpressionClassHasProperty(callExpression, propertyName, scopeManag
         resultingNode &&
         resultingNode.type === 'ObjectExpression' &&
         resultingNode.properties.some(
-          (prop) =>
-            (types.isIdentifier(prop.key) && prop.key.name === propertyName) ||
-            (types.isStringLiteral(prop.key) && prop.key.value === propertyName)
+          (prop) => types.isIdentifier(prop.key) && prop.key.name === propertyName
         )
       );
     })
diff --git a/lib/rules/no-restricted-property-modifications.js b/lib/rules/no-restricted-property-modifications.js
new file mode 100644
index 00000000..2b5aada9
--- /dev/null
+++ b/lib/rules/no-restricted-property-modifications.js
@@ -0,0 +1,180 @@
+'use strict';
+
+const { isStringLiteral } = require('../utils/types');
+const { getImportIdentifier } = require('../utils/import');
+const { isThisSet: isThisSetNative } = require('../utils/property-setter');
+const { nodeToDependentKey } = require('../utils/property-getter');
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow modifying the specified properties',
+      category: 'Miscellaneous',
+      url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-restricted-property-modifications.md',
+    },
+    fixable: 'code',
+    schema: {
+      type: 'array',
+      minItems: 1,
+      maxItems: 1,
+      items: [
+        {
+          type: 'object',
+          properties: {
+            properties: {
+              type: 'array',
+              items: {
+                type: 'string',
+              },
+              minItems: 1,
+              uniqueItems: true,
+            },
+          },
+          required: ['properties'],
+          additionalProperties: false,
+        },
+      ],
+    },
+    messages: {
+      doNotUseAssignment: 'Do not use assignment on properties that should not be modified.',
+      doNotUseSet: 'Do not call `set` on properties that should not be modified.',
+      useReadOnlyMacro:
+        'Use the `readOnly` computed property macro for properties that should not be modified.',
+    },
+  },
+  create(context) {
+    let importedComputedName;
+    let importedReadsName;
+    let importedAliasName;
+
+    const readOnlyProperties = context.options[0].properties;
+
+    return {
+      ImportDeclaration(node) {
+        if (node.source.value === '@ember/object') {
+          importedComputedName =
+            importedComputedName || getImportIdentifier(node, '@ember/object', 'computed');
+        } else if (node.source.value === '@ember/object/computed') {
+          importedReadsName =
+            importedReadsName || getImportIdentifier(node, '@ember/object/computed', 'reads');
+          importedAliasName =
+            importedAliasName || getImportIdentifier(node, '@ember/object/computed', 'alias');
+        }
+      },
+
+      AssignmentExpression(node) {
+        if (!isThisSetNative(node)) {
+          return;
+        }
+
+        const dependentKey = nodeToDependentKey(node.left, context);
+        if (
+          readOnlyProperties.includes(dependentKey) ||
+          readOnlyProperties.some((property) => dependentKey.startsWith(`${property}.`))
+        ) {
+          context.report({
+            node,
+            messageId: 'doNotUseAssignment',
+          });
+        }
+      },
+
+      CallExpression(node) {
+        if (
+          isReadOnlyPropertyUsingAliasOrReads(
+            node,
+            readOnlyProperties,
+            importedComputedName,
+            importedAliasName,
+            importedReadsName
+          )
+        ) {
+          context.report({
+            node,
+            messageId: 'useReadOnlyMacro',
+            fix(fixer) {
+              const argumentText0 = context.getSourceCode().getText(node.arguments[0]);
+              return node.callee.type === 'MemberExpression'
+                ? fixer.replaceText(node, `${importedComputedName}.readOnly(${argumentText0})`)
+                : fixer.replaceText(node, `readOnly(${argumentText0})`);
+            },
+          });
+        } else if (isThisSetReadOnlyProperty(node, readOnlyProperties)) {
+          context.report({ node, messageId: 'doNotUseSet' });
+        }
+      },
+    };
+  },
+};
+
+function isReadOnlyPropertyUsingAliasOrReads(
+  node,
+  readOnlyProperties,
+  importedComputedName,
+  importedAliasName,
+  importedReadsName
+) {
+  // Looks for: reads('readOnlyProperty') or alias('readOnlyProperty')
+  return (
+    (isAliasComputedProperty(node, importedComputedName, importedAliasName) ||
+      isReadsComputedProperty(node, importedComputedName, importedReadsName)) &&
+    node.arguments.length === 1 &&
+    isStringLiteral(node.arguments[0]) &&
+    isReadOnlyProperty(node.arguments[0].value, readOnlyProperties)
+  );
+}
+
+function isThisSet(node) {
+  // Looks for: this.set('readOnlyProperty...', ...);
+  return (
+    node.callee.type === 'MemberExpression' &&
+    node.callee.object.type === 'ThisExpression' &&
+    node.callee.property.type === 'Identifier' &&
+    node.callee.property.name === 'set' &&
+    node.arguments.length === 2 &&
+    isStringLiteral(node.arguments[0])
+  );
+}
+
+function isThisSetReadOnlyProperty(node, readOnlyProperties) {
+  return isThisSet(node) && isReadOnlyProperty(node.arguments[0].value, readOnlyProperties);
+}
+
+function isAliasComputedProperty(node, importedComputedName, importedAliasName) {
+  return (
+    isIdentifierCall(node, importedAliasName) ||
+    isMemberExpressionCall(node, importedComputedName, 'alias')
+  );
+}
+
+function isReadsComputedProperty(node, importedComputedName, importedReadsName) {
+  return (
+    isIdentifierCall(node, importedReadsName) ||
+    isMemberExpressionCall(node, importedComputedName, 'reads')
+  );
+}
+
+function isIdentifierCall(node, name) {
+  return (
+    node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === name
+  );
+}
+
+function isMemberExpressionCall(node, object, name) {
+  return (
+    node.type === 'CallExpression' &&
+    node.callee.type === 'MemberExpression' &&
+    node.callee.object.type === 'Identifier' &&
+    node.callee.object.name === object &&
+    node.callee.property.type === 'Identifier' &&
+    node.callee.property.name === name
+  );
+}
+
+function isReadOnlyProperty(property, readOnlyProperties) {
+  return (
+    readOnlyProperties.includes(property) ||
+    readOnlyProperties.some((propertyCurrent) => property.startsWith(`${propertyCurrent}.`))
+  );
+}
diff --git a/lib/rules/no-restricted-service-injections.js b/lib/rules/no-restricted-service-injections.js
index f3689e2a..811dd029 100644
--- a/lib/rules/no-restricted-service-injections.js
+++ b/lib/rules/no-restricted-service-injections.js
@@ -123,7 +123,7 @@ module.exports = {
           }
         } else {
           // The service name is the property name.
-          checkForViolationAndReport(node, node.key.name || node.key.value);
+          checkForViolationAndReport(node, node.key.name);
         }
       },
 
@@ -147,7 +147,7 @@ module.exports = {
           serviceDecorator.expression.arguments[0].type === 'Literal' &&
           typeof serviceDecorator.expression.arguments[0].value === 'string'
             ? serviceDecorator.expression.arguments[0].value
-            : node.key.name || node.key.value;
+            : node.key.name;
 
         checkForViolationAndReport(node, serviceName);
       },
diff --git a/lib/rules/no-unnecessary-service-injection-argument.js b/lib/rules/no-unnecessary-service-injection-argument.js
index 73e381dd..2748a659 100644
--- a/lib/rules/no-unnecessary-service-injection-argument.js
+++ b/lib/rules/no-unnecessary-service-injection-argument.js
@@ -49,7 +49,7 @@ module.exports = {
           return;
         }
 
-        const keyName = node.key.name || node.key.value;
+        const keyName = node.key.name;
         const firstArg = node.value.arguments[0];
         const firstArgValue = firstArg.value;
         if (keyName === firstArgValue) {
@@ -74,7 +74,7 @@ module.exports = {
           return;
         }
 
-        const keyName = node.key.name || node.key.value;
+        const keyName = node.key.name;
         const firstArg = node.decorators[0].expression.arguments[0];
         const firstArgValue = firstArg.value;
         if (keyName === firstArgValue) {
diff --git a/lib/rules/no-unused-services.js b/lib/rules/no-unused-services.js
index 3b18e0d4..73135c70 100644
--- a/lib/rules/no-unused-services.js
+++ b/lib/rules/no-unused-services.js
@@ -321,13 +321,8 @@ module.exports = {
           currentClass &&
           emberUtils.isInjectedServiceProp(node, importedEmberName, importedInjectName)
         ) {
-          if (node.key.type === 'Identifier') {
-            const name = node.key.name;
-            currentClass.services[name] = node;
-          } else if (types.isStringLiteral(node.key)) {
-            const name = node.key.value;
-            currentClass.services[name] = node;
-          }
+          const name = node.key.name;
+          currentClass.services[name] = node;
         }
       },
       // @service(...) foo;
@@ -342,13 +337,8 @@ module.exports = {
           currentClass &&
           emberUtils.isInjectedServiceProp(node, importedEmberName, importedInjectName)
         ) {
-          if (node.key.type === 'Identifier') {
-            const name = node.key.name;
-            currentClass.services[name] = node;
-          } else if (types.isStringLiteral(node.key)) {
-            const name = node.key.value;
-            currentClass.services[name] = node;
-          }
+          const name = node.key.name;
+          currentClass.services[name] = node;
         }
       },
       // this.foo...
diff --git a/lib/rules/require-computed-property-dependencies.js b/lib/rules/require-computed-property-dependencies.js
index 99b48954..c7b20bc7 100644
--- a/lib/rules/require-computed-property-dependencies.js
+++ b/lib/rules/require-computed-property-dependencies.js
@@ -153,13 +153,10 @@ function findInjectedServiceNames(node) {
       }
       if (
         (types.isProperty(child) || types.isClassProperty(child)) &&
-        emberUtils.isInjectedServiceProp(child, importedEmberName, importedInjectName)
+        emberUtils.isInjectedServiceProp(child, importedEmberName, importedInjectName) &&
+        types.isIdentifier(child.key)
       ) {
-        if (types.isIdentifier(child.key)) {
-          results.push(child.key.name);
-        } else if (types.isStringLiteral(child.key)) {
-          results.push(child.key.value);
-        }
+        results.push(child.key.name);
       }
     },
   });
diff --git a/lib/rules/require-tagless-components.js b/lib/rules/require-tagless-components.js
index 24783f75..7645ee27 100644
--- a/lib/rules/require-tagless-components.js
+++ b/lib/rules/require-tagless-components.js
@@ -1,5 +1,7 @@
 'use strict';
 
+const { isEmberComponent } = require('../utils/ember');
+const { getSourceModuleNameForIdentifier } = require('../utils/import');
 const {
   isCallExpression,
   isIdentifier,
@@ -7,8 +9,6 @@ const {
   isStringLiteral,
 } = require('../utils/types');
 const { getNodeOrNodeFromVariable } = require('../utils/utils');
-const { getImportIdentifier } = require('../utils/import');
-const { isTestFile } = require('../utils/ember');
 
 const ERROR_MESSAGE_REQUIRE_TAGLESS_COMPONENTS =
   "Please switch to a tagless component by setting `tagName: ''` or converting to a Glimmer component";
@@ -101,30 +101,15 @@ module.exports = {
   },
 
   create(context) {
-    if (isTestFile(context.getFilename())) {
-      // This rule does not apply to test files.
-      return {};
-    }
-
     const sourceCode = context.getSourceCode();
     const { scopeManager } = sourceCode;
 
-    let importedComponentName;
-
     return {
-      ImportDeclaration(node) {
-        if (node.source.value === '@ember/component') {
-          importedComponentName =
-            importedComponentName || getImportIdentifier(node, '@ember/component');
-        }
-      },
-
       // Handle classic components
       'CallExpression > MemberExpression[property.name="extend"]'(node) {
         const callExpression = node.parent;
 
-        if (!(node.object.type === 'Identifier' && node.object.name === importedComponentName)) {
-          // Not an Ember component.
+        if (!isEmberComponent(context, callExpression)) {
           return;
         }
 
@@ -153,10 +138,7 @@ module.exports = {
 
       // Handle ES class components
       'ClassDeclaration[superClass]'(node) {
-        if (
-          node.superClass.type === 'Identifier' &&
-          node.superClass.name === importedComponentName
-        ) {
+        if (getSourceModuleNameForIdentifier(context, node.superClass) === '@ember/component') {
           let tagNameNode;
           let decorator;
 
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index ea469d7c..503e9a5f 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -183,11 +183,9 @@ function isMirageConfig(fileName) {
   return path.normalize(fileName).endsWith(path.join('mirage', 'config.js'));
 }
 
-// Some third-party libraries have an `extend` function and we want to avoid mistaking this for an extended Ember object.
-// TODO: ideally, this would check the actual name that these libraries are imported under, but there's a lot of plumbing needed for that.
-const COMMON_JQUERY_NAMES = ['$', 'jQuery']; // https://api.jquery.com/jquery.extend/
-const COMMON_LODASH_NAMES = ['_', 'lodash']; // https://lodash.com/docs/4.17.15#assignIn
-const THIRD_PARTY_LIBRARIES_WITH_EXTEND = new Set([...COMMON_JQUERY_NAMES, ...COMMON_LODASH_NAMES]);
+// jQuery has an `extend` function and we want to avoid mistaking it for an extended object.
+// TODO: ideally, this would check the actual name that jQuery is imported under, but there's a lot of plumbing needed for that.
+const COMMON_JQUERY_NAMES = new Set(['$', 'jQuery']);
 
 function isExtendObject(node) {
   // Check for:
@@ -198,10 +196,7 @@ function isExtendObject(node) {
     node.callee.type === 'MemberExpression' &&
     ((node.callee.property.type === 'Identifier' && node.callee.property.name === 'extend') ||
       (node.callee.property.type === 'Literal' && node.callee.property.value === 'extend')) &&
-    !(
-      node.callee.object.type === 'Identifier' &&
-      THIRD_PARTY_LIBRARIES_WITH_EXTEND.has(node.callee.object.name)
-    )
+    !(node.callee.object.type === 'Identifier' && COMMON_JQUERY_NAMES.has(node.callee.object.name))
   );
 }
 


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 e4b11529..826d0b25 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
@@ -54,19 +54,6 @@ ruleTester.run('no-assignment-of-untracked-properties-used-in-tracking-contexts'
       }`,
       filename: '/components/foo.js',
     },
-    {
-      // Assignment of tracked property with string literal property name.
-      code: `
-      import { computed } from '@ember/object';
-      import Component from '@ember/component';
-      import { tracked } from '@glimmer/tracking';
-      class MyClass extends Component {
-        @tracked 'x'
-        @computed('x') get prop() {}
-        myFunction() { this.x = 123; }
-      }`,
-      filename: '/components/foo.js',
-    },
     {
       // Assignment of tracked property (with aliased `tracked` import).
       code: `
diff --git a/tests/lib/rules/no-controllers.js b/tests/lib/rules/no-controllers.js
index a440bea7..b5461b35 100644
--- a/tests/lib/rules/no-controllers.js
+++ b/tests/lib/rules/no-controllers.js
@@ -28,13 +28,6 @@ ruleTester.run('no-controllers', rule, {
         queryParams: ['query', 'sortType', 'sortOrder']
       });
     `,
-    // Classic class with queryParams with string literal property name.
-    `
-      import Controller from '@ember/controller';
-      export default Controller.extend({
-        'queryParams': ['query', 'sortType', 'sortOrder']
-      });
-    `,
     // Classic class with queryParams: checks object argument from variable.
     `
       import Controller from '@ember/controller';
@@ -55,13 +48,6 @@ ruleTester.run('no-controllers', rule, {
         queryParams = ['category'];
       }
     `,
-    // Native class with queryParams with string literal property name.
-    `
-      import Controller from '@ember/controller';
-      export default class ArticlesController extends Controller {
-        'queryParams' = ['category'];
-      }
-    `,
   ],
 
   invalid: [
diff --git a/tests/lib/rules/no-restricted-property-modifications.js b/tests/lib/rules/no-restricted-property-modifications.js
new file mode 100644
index 00000000..0dd26a5f
--- /dev/null
+++ b/tests/lib/rules/no-restricted-property-modifications.js
@@ -0,0 +1,208 @@
+'use strict';
+
+const rule = require('../../../lib/rules/no-restricted-property-modifications');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2015,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run('no-restricted-property-modifications', rule, {
+  valid: [
+    // ****************************************
+    // Test cases for computed property macros.
+    // ****************************************
+
+    // readOnly
+    {
+      code: "import {readOnly} from '@ember/object/computed'; readOnly('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {readOnly} from '@ember/object/computed'; readOnly('currentUser.isUS')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {computed} from '@ember/object'; computed.readOnly('currentUser.isUS')",
+      options: [{ properties: ['currentUser'] }],
+    },
+
+    // alias
+    {
+      code: "import {alias} from '@ember/object/computed'; alias('myProperty')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {alias} from '@ember/object/computed'; alias('myProperty.otherThing')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {computed} from '@ember/object'; computed.alias('myProperty')",
+      options: [{ properties: ['currentUser'] }],
+    },
+
+    // reads
+    {
+      code: "import {reads} from '@ember/object/computed'; reads('myProperty')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {reads} from '@ember/object/computed'; reads('myProperty.otherThing')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {computed} from '@ember/object'; computed.reads('myProperty')",
+      options: [{ properties: ['currentUser'] }],
+    },
+
+    // Without import
+    {
+      code: "alias('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "computed.alias('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "computed.reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {computed} from 'wrong-import'; computed.reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "import {reads} from '@ember/object/computed'; computed.reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+    },
+
+    // ****************************************
+    // Test cases for this.set(...);
+    // ****************************************
+    {
+      code: "this.get('currentUser.somePermission')",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "something.set('currentUser.somePermission', true)",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "this.set('someProperty.somePermission', true)",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "this.set('someProperty.currentUser', true)",
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: "this.set('currentUserWithLongerName.somePermission', true)",
+      options: [{ properties: ['currentUser'] }],
+    },
+
+    // ****************************************
+    // Test cases for assignment
+    // ****************************************
+    {
+      code: 'this.foo = 123;',
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: 'this.foo.bar = 123;',
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: 'this.foo.currentUser = 123;',
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: 'currentUser = 123;',
+      options: [{ properties: ['currentUser'] }],
+    },
+    {
+      code: 'const currentUser = 123;',
+      options: [{ properties: ['currentUser'] }],
+    },
+  ],
+  invalid: [
+    // ****************************************
+    // Test cases for computed property macros.
+    // ****************************************
+    {
+      code: "import {alias} from '@ember/object/computed'; alias('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {alias} from '@ember/object/computed'; readOnly('currentUser')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+    {
+      code: "import {alias} from '@ember/object/computed'; alias('currentUser.isUS')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {alias} from '@ember/object/computed'; readOnly('currentUser.isUS')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+    {
+      code: "import {computed} from '@ember/object'; computed.alias('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {computed} from '@ember/object'; computed.readOnly('currentUser')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+    {
+      code: "import {reads} from '@ember/object/computed'; reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {reads} from '@ember/object/computed'; readOnly('currentUser')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+    {
+      code: "import {reads} from '@ember/object/computed'; reads('currentUser.isUS')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {reads} from '@ember/object/computed'; readOnly('currentUser.isUS')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+    {
+      code: "import {computed} from '@ember/object'; computed.reads('currentUser')",
+      options: [{ properties: ['currentUser'] }],
+      output: "import {computed} from '@ember/object'; computed.readOnly('currentUser')",
+      errors: [{ messageId: 'useReadOnlyMacro', type: 'CallExpression' }],
+    },
+
+    // ****************************************
+    // Test cases for this.set(...);
+    // ****************************************
+    {
+      code: "this.set('currentUser', {})",
+      options: [{ properties: ['currentUser'] }],
+      output: null,
+      errors: [{ messageId: 'doNotUseSet', type: 'CallExpression' }],
+    },
+    {
+      code: "this.set('currentUser.somePermission', true)",
+      options: [{ properties: ['currentUser'] }],
+      output: null,
+      errors: [{ messageId: 'doNotUseSet', type: 'CallExpression' }],
+    },
+
+    // ****************************************
+    // Test cases for assignment
+    // ****************************************
+    {
+      code: 'this.currentUser = {};',
+      options: [{ properties: ['currentUser'] }],
+      output: null,
+      errors: [{ messageId: 'doNotUseAssignment', type: 'AssignmentExpression' }],
+    },
+    {
+      code: 'this.currentUser.foo = true;',
+      options: [{ properties: ['currentUser'] }],
+      output: null,
+      errors: [{ messageId: 'doNotUseAssignment', type: 'AssignmentExpression' }],
+    },
+  ],
+});
diff --git a/tests/lib/rules/no-restricted-service-injections.js b/tests/lib/rules/no-restricted-service-injections.js
index 90fbef6c..4bf29bce 100644
--- a/tests/lib/rules/no-restricted-service-injections.js
+++ b/tests/lib/rules/no-restricted-service-injections.js
@@ -80,14 +80,6 @@ ruleTester.run('no-restricted-service-injections', rule, {
       filename: 'app/components/path.js',
       errors: [{ message: DEFAULT_ERROR_MESSAGE, type: 'Property' }],
     },
-    {
-      // Without service name argument (property name as string literal):
-      code: `${SERVICE_IMPORT} Component.extend({ 'myService': service() })`,
-      options: [{ paths: ['app/components'], services: ['my-service'] }],
-      output: null,
-      filename: 'app/components/path.js',
-      errors: [{ message: DEFAULT_ERROR_MESSAGE, type: 'Property' }],
-    },
     {
       // With camelized service name argument:
       code: `${SERVICE_IMPORT} Component.extend({ randomName: service('myService') })`,
@@ -152,14 +144,6 @@ ruleTester.run('no-restricted-service-injections', rule, {
       filename: 'app/components/path.js',
       errors: [{ message: DEFAULT_ERROR_MESSAGE, type: 'ClassProperty' }],
     },
-    {
-      // With decorator without service name argument (with parentheses) (with property name as string literal):
-      code: `${SERVICE_IMPORT} class MyComponent extends Component { @service() 'myService' }`,
-      options: [{ paths: ['app/components'], services: ['my-service'] }],
-      output: null,
-      filename: 'app/components/path.js',
-      errors: [{ message: DEFAULT_ERROR_MESSAGE, type: 'ClassProperty' }],
-    },
     {
       // With custom error message:
       code: `${SERVICE_IMPORT} Component.extend({ myService: service() })`,
diff --git a/tests/lib/rules/no-unnecessary-service-injection-argument.js b/tests/lib/rules/no-unnecessary-service-injection-argument.js
index da24f0c3..86adf7ae 100644
--- a/tests/lib/rules/no-unnecessary-service-injection-argument.js
+++ b/tests/lib/rules/no-unnecessary-service-injection-argument.js
@@ -39,22 +39,44 @@ ruleTester.run('no-unnecessary-service-injection-argument', rule, {
         ecmaFeatures: { legacyDecorators: true },
       },
     },
-    `${RENAMED_SERVICE_IMPORT} class Test { @service() serviceName }`,
+    {
+      code: `${RENAMED_SERVICE_IMPORT} class Test { @service() serviceName }`,
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
+    },
 
     // Property name matches service name but service name uses dashes
     // (allowed because it avoids needless runtime camelization <-> dasherization in the resolver):
     `${EMBER_IMPORT} export default Component.extend({ specialName: Ember.inject.service('service-name') });`,
     `${RENAMED_SERVICE_IMPORT} export default Component.extend({ specialName: service('service-name') });`,
     `${SERVICE_IMPORT} export default Component.extend({ specialName: inject('service-name') });`,
-    `${SERVICE_IMPORT} export default Component.extend({ 'specialName': inject('service-name') });`,
     `${RENAMED_SERVICE_IMPORT} const controller = Controller.extend({ serviceName: service('service-name') });`,
-    `${RENAMED_SERVICE_IMPORT} class Test { @service("service-name") serviceName }`,
-    `${RENAMED_SERVICE_IMPORT} class Test { @service("service-name") 'serviceName' }`,
+    {
+      code: `${RENAMED_SERVICE_IMPORT} class Test { @service("service-name") serviceName }`,
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
+    },
 
     // Property name does not match service name:
     `${EMBER_IMPORT} const controller = Controller.extend({ specialName: Ember.inject.service('service-name') });`,
     `${RENAMED_SERVICE_IMPORT} const controller = Controller.extend({ specialName: service('service-name') });`,
-    `${RENAMED_SERVICE_IMPORT} class Test { @service("specialName") serviceName }`,
+    {
+      code: `${RENAMED_SERVICE_IMPORT} class Test { @service("specialName") serviceName }`,
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
+    },
 
     // When usage is ignored because of additional arguments:
     `${EMBER_IMPORT} export default Component.extend({ serviceName: Ember.inject.service('serviceName', EXTRA_PROPERTY) });`,
@@ -64,13 +86,29 @@ ruleTester.run('no-unnecessary-service-injection-argument', rule, {
     // When usage is ignored because of template literal:
     `${EMBER_IMPORT} export default Component.extend({ serviceName: Ember.inject.service(\`serviceName\`) });`,
     `${SERVICE_IMPORT} export default Component.extend({ serviceName: service(\`serviceName\`) });`,
-    `${RENAMED_SERVICE_IMPORT} class Test { @service(\`specialName\`) serviceName }`,
+    {
+      code: `${RENAMED_SERVICE_IMPORT} class Test { @service(\`specialName\`) serviceName }`,
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
+    },
 
     // Not Ember's `service()` function:
     "export default Component.extend({ serviceName: otherFunction('serviceName') });",
     `${RENAMED_SERVICE_IMPORT} export default Component.extend({ serviceName: service.otherFunction('serviceName') });`,
     "export default Component.extend({ serviceName: inject.otherFunction('serviceName') });",
-    'class Test { @otherDecorator("name") name }',
+    {
+      code: 'class Test { @otherDecorator("name") name }',
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
+    },
 
     'export default Component.extend({ ...foo });',
   ],
@@ -104,6 +142,12 @@ ruleTester.run('no-unnecessary-service-injection-argument', rule, {
       code: `${RENAMED_SERVICE_IMPORT} class Test { @service("serviceName") serviceName }`,
       output: `${RENAMED_SERVICE_IMPORT} class Test { @service() serviceName }`,
       errors: [{ message: ERROR_MESSAGE, type: 'Literal' }],
+      parser: require.resolve('@babel/eslint-parser'),
+      parserOptions: {
+        ecmaVersion: 6,
+        sourceType: 'module',
+        ecmaFeatures: { legacyDecorators: true },
+      },
     },
   ],
 });
diff --git a/tests/lib/rules/no-unused-services.js b/tests/lib/rules/no-unused-services.js
index 729624cf..f1a4e1f2 100644
--- a/tests/lib/rules/no-unused-services.js
+++ b/tests/lib/rules/no-unused-services.js
@@ -139,10 +139,8 @@ function generateValid() {
     valid.push(
       `${SERVICE_IMPORT} class MyClass { @service('foo') ${SERVICE_NAME}; fooFunc() {${use}} }`,
       `${SERVICE_IMPORT} class MyClass { @service() ${SERVICE_NAME}; fooFunc() {${use}} }`,
-      `${SERVICE_IMPORT} class MyClass { @service() '${SERVICE_NAME}'; fooFunc() {${use}} }`,
       `${SERVICE_IMPORT} Component.extend({ ${SERVICE_NAME}: service('foo'), fooFunc() {${use}} });`,
-      `${SERVICE_IMPORT} Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${use}} });`,
-      `${SERVICE_IMPORT} Component.extend({ '${SERVICE_NAME}': service(), fooFunc() {${use}} });`
+      `${SERVICE_IMPORT} Component.extend({ ${SERVICE_NAME}: service(), fooFunc() {${use}} });`
     );
   }
 
diff --git a/tests/lib/rules/require-computed-property-dependencies.js b/tests/lib/rules/require-computed-property-dependencies.js
index 5fd8ff98..43c27743 100644
--- a/tests/lib/rules/require-computed-property-dependencies.js
+++ b/tests/lib/rules/require-computed-property-dependencies.js
@@ -85,10 +85,8 @@ ruleTester.run('require-computed-property-dependencies', rule, {
           console.log(this.intl);
           return this.name + this.intl.t('some.translation.key');
           console.log(this.otherService);
-          console.log(this.serviceNameInStringLiteral);
         }),
-        otherService: service(), // Service injection coming after computed property.
-        'serviceNameInStringLiteral': service() // Property name as string literal.
+        otherService: service() // Service injection coming after computed property.
       });
     `,
     // Should ignore the left side of an assignment.
@@ -119,13 +117,8 @@ ruleTester.run('require-computed-property-dependencies', rule, {
       import { inject as service } from '@ember/service';
       class Test {
         @service i18n; // Service names not required as dependent keys by default.
-        @service 'serviceNameInStringLiteral';
-
         @computed('first', 'last')
         get fullName() { return this.i18n.t(this.first + ' ' + this.last); }
-
-        @computed()
-        get foo() { return this.serviceNameInStringLiteral; }
       }
     `,
   ],
diff --git a/tests/lib/rules/require-tagless-components.js b/tests/lib/rules/require-tagless-components.js
index 0876d27f..af603b1c 100644
--- a/tests/lib/rules/require-tagless-components.js
+++ b/tests/lib/rules/require-tagless-components.js
@@ -28,22 +28,12 @@ ruleTester.run('require-tagless-components', rule, {
       import Component from '@ember/component';
       export default Component.extend({ tagName: '' });
     `,
-    `
-      import Component from '@ember/component';
-      export default Component.extend(Mixin, { tagName: '' });
-    `,
     `
       import Component from '@ember/component';
       export default class MyComponent extends Component {
         tagName = ''
       }
     `,
-    `
-      import Component from '@ember/component';
-      export default class MyComponent extends Component.extend(Mixin) {
-        tagName = ''
-      }
-    `,
     `
       import Component from '@ember/component';
       import { tagName } from '@ember-decorators/component';
@@ -66,31 +56,6 @@ ruleTester.run('require-tagless-components', rule, {
         tagName = 'some-non-empty-value';
       }
     `,
-    {
-      // Classic service in component file.
-      code: `
-        import Service from '@ember/service';
-        export default Service.extend({});
-      `,
-      filename: 'app/components/foo.js',
-    },
-    {
-      // Native service in component file.
-      code: `
-        import Service from '@ember/service';
-        export default class MyService extends Service {};
-      `,
-      filename: 'app/components/foo.js',
-    },
-    {
-      // Should ignore test files.
-      code: `
-        import Component from '@ember/component';
-        export default class MyComponent extends Component {};
-        Component.extend({});
-      `,
-      filename: 'tests/integration/components/foo-test.js',
-    },
   ],
   invalid: [
     {
diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js
index d633c5e4..edab2dc5 100644
--- a/tests/lib/utils/ember-test.js
+++ b/tests/lib/utils/ember-test.js
@@ -582,11 +582,6 @@ describe('isExtendObject', () => {
     expect(emberUtils.isExtendObject(node)).toBeFalsy();
   });
 
-  it('should not detect a potential lodash usage', () => {
-    expect(emberUtils.isExtendObject(parse('_.extend()'))).toBeFalsy();
-    expect(emberUtils.isExtendObject(parse('lodash.extend()'))).toBeFalsy();
-  });
-
   it('should not detect with non-extend name', () => {
     const node = parse('foo.notExtend()');
     expect(emberUtils.isExtendObject(node)).toBeFalsy();