Repository: ember-cli/eslint-plugin-ember (js)

Issue:
no-get autofix is running on ObjectProxies
We are getting troubles in my project with `eslint autofix on save` and the `no-get` rule.

An ObjectProxy file that uses the classic Mixin system:

```javascript
export default MyProxyObject extends ObjectProxy.extend(SomeMixin) { 
  method() {
    const property = this.get('property')
  }
 }
```

In this condition the `no-get` rule triggers an error that will be auto-fixed on save.

This should not be logged as an error because we got an error if trying access properties without `.get` in ObjectProxy.

Because I'm under a NDA I cannot share my code. But if some additional detail is needed I can answer.



Pull request description:
`no-get` rule should ignore proxy classes that look like `ObjectProxy.extend(SomeMixin)`
Fixes #1141.

@rogeraraujo90

Patch:
diff --git a/lib/utils/ember.js b/lib/utils/ember.js
index a96e3013..30b0ed3e 100644
--- a/lib/utils/ember.js
+++ b/lib/utils/ember.js
@@ -207,13 +207,26 @@ function isEmberCoreModule(context, node, moduleName) {
     return isClassicEmberCoreModule(node, moduleName, context.getFilename());
   } else if (types.isClassDeclaration(node)) {
     // native classes
-    if (!node.superClass || !types.isIdentifier(node.superClass)) {
+    if (
+      // class Foo extends Component
+      !node.superClass ||
+      // class Foo extends Component.extend(SomeMixin)
+      !(
+        types.isIdentifier(node.superClass) ||
+        (types.isCallExpression(node.superClass) &&
+          types.isMemberExpression(node.superClass.callee) &&
+          types.isIdentifier(node.superClass.callee.property) &&
+          node.superClass.callee.property.name === 'extend')
+      )
+    ) {
       return false;
     }
-    const superClassImportPath = importUtils.getSourceModuleNameForIdentifier(
-      context,
-      node.superClass
-    );
+
+    const superClass = types.isIdentifier(node.superClass)
+      ? node.superClass
+      : node.superClass.callee.object;
+
+    const superClassImportPath = importUtils.getSourceModuleNameForIdentifier(context, superClass);
 
     if (superClassImportPath === CORE_MODULE_IMPORT_PATHS[moduleName]) {
       return true;


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 7cb151c1..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
@@ -113,7 +113,7 @@ ruleTester.run('no-assignment-of-untracked-properties-used-in-tracking-contexts'
       code: `
       import { computed } from '@ember/object';
       import SomeThing from 'random';
-      SomeThing.extend({
+      SomeThing.extends({
         prop: computed('x', function() {}),
         myFunction() { this.x = 123; }
       })`,
@@ -151,10 +151,10 @@ ruleTester.run('no-assignment-of-untracked-properties-used-in-tracking-contexts'
       code: `
       import { computed } from '@ember/object';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         prop: computed('x') // This is here to make sure it is not considered in the next class.
       });
-      Component.extend({
+      Component.extends({
         ...someProp.getProperties('propA', 'propB'),
         prop1: computed(),
         prop2: computed(function() {}),
@@ -169,7 +169,7 @@ ruleTester.run('no-assignment-of-untracked-properties-used-in-tracking-contexts'
       import { readOnly, equal, gt } from '@ember/object/computed';
       import { computed } from '@ember/object';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         actions: {},
       });`,
       filename: '/components/foo.js',
@@ -430,7 +430,7 @@ import { computed } from '@ember/object';
       class MyClass extends Component {
         @tracked x // This is here to ensure that it is ignored in the subsequent class.
       }
-      Component.extend({
+      Component.extends({
         myProp: computed('x', function() {}),
         myFunction() { this.x = 123; }
       })`,
@@ -442,7 +442,7 @@ import { computed } from '@ember/object';
       class MyClass extends Component {
         @tracked x // This is here to ensure that it is ignored in the subsequent class.
       }
-      Component.extend({
+      Component.extends({
         myProp: computed('x', function() {}),
         myFunction() { set(this, 'x', 123); }
       })`,
@@ -459,7 +459,7 @@ import { computed } from '@ember/object';
       code: `
       import { readOnly as ro } from '@ember/object/computed';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: ro('x'),
         myFunction() { this.x = 123; }
       })`,
@@ -467,7 +467,7 @@ import { computed } from '@ember/object';
       import { set } from '@ember/object';
 import { readOnly as ro } from '@ember/object/computed';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: ro('x'),
         myFunction() { set(this, 'x', 123); }
       })`,
@@ -479,7 +479,7 @@ import { readOnly as ro } from '@ember/object/computed';
       code: `
       import { computed as c } from '@ember/object';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: c.readOnly('x'),
         myFunction() { this.x = 123; }
       })`,
@@ -487,7 +487,7 @@ import { readOnly as ro } from '@ember/object/computed';
       import { set } from '@ember/object';
 import { computed as c } from '@ember/object';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: c.readOnly('x'),
         myFunction() { set(this, 'x', 123); }
       })`,
@@ -500,7 +500,7 @@ import { computed as c } from '@ember/object';
       code: `
       import { mapBy } from '@ember/object/computed';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: mapBy('chores', 'done', true),
         myFunction1() { this.chores = 123; },
         myFunction2() { this.done = 123; } // Allowed since this isn't a dependent key.
@@ -509,7 +509,7 @@ import { computed as c } from '@ember/object';
       import { set } from '@ember/object';
 import { mapBy } from '@ember/object/computed';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: mapBy('chores', 'done', true),
         myFunction1() { set(this, 'chores', 123); },
         myFunction2() { this.done = 123; } // Allowed since this isn't a dependent key.
@@ -539,7 +539,7 @@ import { mapBy } from '@ember/object/computed';
       code: `
       import { rejectBy } from 'custom-macros/custom';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: rejectBy('chores', 'done', true),
         myFunction1() { this.chores = 123; },
         myFunction2() { this.done = 123; }, // Allowed since this isn't a dependent key.
@@ -548,7 +548,7 @@ import { mapBy } from '@ember/object/computed';
       import { set } from '@ember/object';
 import { rejectBy } from 'custom-macros/custom';
       import Component from '@ember/component';
-      Component.extend({
+      Component.extends({
         myProp: rejectBy('chores', 'done', true),
         myFunction1() { set(this, 'chores', 123); },
         myFunction2() { this.done = 123; }, // Allowed since this isn't a dependent key.
@@ -580,7 +580,7 @@ import { rejectBy } from 'custom-macros/custom';
       code: `
       import { customComputed } from 'custom-macros/ind';
       import Component from '@ember/component';
-      Component.extend({
+      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.
@@ -590,7 +590,7 @@ import { rejectBy } from 'custom-macros/custom';
       import { set } from '@ember/object';
 import { customComputed } from 'custom-macros/ind';
       import Component from '@ember/component';
-      Component.extend({
+      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.
diff --git a/tests/lib/rules/no-get.js b/tests/lib/rules/no-get.js
index 27034164..a2cc9a2b 100644
--- a/tests/lib/rules/no-get.js
+++ b/tests/lib/rules/no-get.js
@@ -162,6 +162,15 @@ ruleTester.run('no-get', rule, {
       }
     }
     `,
+    `
+    import ArrayProxy from '@ember/array/proxy';
+    class MyProxy extends ArrayProxy.extend(SomeMixin) {
+      someFunction() {
+        test();
+        console.log(this.get('propertyInsideProxyObject'));
+      }
+    }
+    `,
 
     // Ignores `get()` inside classes with `unknownProperty`:
     `
diff --git a/tests/lib/utils/ember-test.js b/tests/lib/utils/ember-test.js
index 9564d51c..afbfd676 100644
--- a/tests/lib/utils/ember-test.js
+++ b/tests/lib/utils/ember-test.js
@@ -204,6 +204,15 @@ describe('isEmberCoreModule', () => {
     expect(emberUtils.isEmberCoreModule(context, node, 'Route')).toBeTruthy();
   });
 
+  it('should handle native class with mixin', () => {
+    const context = new FauxContext(
+      "import Route from '@ember/routing/route'; class MyRoute extends Route.extend(SomeMixin) {}",
+      'example-app/routes/path/to/some-route.js'
+    );
+    const node = context.ast.body[1];
+    expect(emberUtils.isEmberCoreModule(context, node, 'Route')).toBeTruthy();
+  });
+
   it('should check if current file is a route with native class', () => {
     const context = new FauxContext(
       "import Route from '@ember/routing/route'; class MyRoute extends Route {}",
@@ -738,6 +747,24 @@ describe('isEmberProxy', () => {
     expect(emberUtils.isEmberProxy(context, node)).toBeTruthy();
   });
 
+  it('should detect ObjectProxy with mixin', () => {
+    const context = new FauxContext(`
+      import ObjectProxy from '@ember/object/proxy';
+      class MyProxy extends ObjectProxy.extend(SomeMixin) {}
+    `);
+    const node = context.ast.body[1];
+    expect(emberUtils.isEmberProxy(context, node)).toBeTruthy();
+  });
+
+  it('should detect ObjectProxy with classic class', () => {
+    const context = new FauxContext(`
+      import ObjectProxy from '@ember/object/proxy';
+      ObjectProxy.extend({});
+    `);
+    const node = context.ast.body[1].expression;
+    expect(emberUtils.isEmberProxy(context, node)).toBeTruthy();
+  });
+
   it('should not detect random code', () => {
     const context = new FauxContext('someFunctionCall();');
     const node = context.ast.body[0].expression;