Repository: ember-cli/eslint-plugin-ember
Pull request #350 (2018-12-06 18:44:23)

Introduce no-ember-super-in-es-classes rule
In parts of the community that have been experimenting with native classes ahead of Octane, trying to use `this._super()` in native class methods has become a common mistake. Sometimes folks don't know that it's incorrect, sometimes it's the product of hasty refactoring, and sometimes it's just muscle memory kicking in.

The end result, though, is erroneous code whose ultimate failure may be obvious or (worse) subtle. This PR adds a rule to prevent using `_super` in native class methods as well as a fixer to replace such instances with native `super`.

~I'm suggesting including this rule in the `recommended` set (though I suppose that would need to be timed to correspond to a major release) as it's not just a stylistic thing but a possible semantic error, but happy to have a discussion about that or any desired changes/additions to the implementation or testing.~


Patch:
diff --git a/README.md b/README.md
index 67feb549..e86b86ea 100644
--- a/README.md
+++ b/README.md
@@ -108,6 +108,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [no-attrs-snapshot](./docs/rules/no-attrs-snapshot.md) | Disallow use of attrs snapshot in `didReceiveAttrs` and `didUpdateAttrs` |
 | :white_check_mark: | [no-capital-letters-in-routes](./docs/rules/no-capital-letters-in-routes.md) | Raise an error when there is a route with uppercased letters in router.js |
 | :white_check_mark: | [no-duplicate-dependent-keys](./docs/rules/no-duplicate-dependent-keys.md) | Disallow repeating dependent keys |
+| :wrench: | [no-ember-super-in-es-classes](./docs/rules/no-ember-super-in-es-classes.md) | Prevents use of `this._super` in ES class methods |
 | :white_check_mark: | [no-ember-testing-in-module-scope](./docs/rules/no-ember-testing-in-module-scope.md) | Prevents use of Ember.testing in module scope |
 | :white_check_mark: | [no-side-effects](./docs/rules/no-side-effects.md) | Warns about unexpected side effects in computed properties |
 | :white_check_mark: | [require-super-in-init](./docs/rules/require-super-in-init.md) | Enforces super calls in init hooks |
diff --git a/docs/rules/no-ember-super-in-es-classes.md b/docs/rules/no-ember-super-in-es-classes.md
new file mode 100644
index 00000000..6a0601c5
--- /dev/null
+++ b/docs/rules/no-ember-super-in-es-classes.md
@@ -0,0 +1,34 @@
+## `this._super` is not allowed in ES class methods
+
+### Rule name: `no-ember-super-in-es-classes`
+
+While `this._super()` is the only way to invoke an overridden method in an `EmberObject.extend`-style class, the `_super` method doesn't work properly when using native class syntax. Fortunately, native classes come with their own mechanism for invoking methods from a parent.
+
+```javascript
+// Good
+import Component from '@ember/component';
+export default class MyComponent extends Component {
+  init() {
+    super.init(...arguments);
+    // Other logic
+  }
+}
+
+// Good
+import Component from '@ember/component';
+export default Component.extend({
+  init() {
+    this._super(...arguments);
+    // Other logic
+  }
+});
+
+// Bad
+import Component from '@ember/component';
+export default class MyComponent extends Component {
+  init() {
+    this._super(...arguments);
+    // Other logic
+  }
+}
+```
diff --git a/lib/index.js b/lib/index.js
index 15abe3e2..6f0bde72 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -16,6 +16,7 @@ module.exports = {
     'no-attrs-snapshot': require('./rules/no-attrs-snapshot'),
     'no-capital-letters-in-routes': require('./rules/no-capital-letters-in-routes'),
     'no-duplicate-dependent-keys': require('./rules/no-duplicate-dependent-keys'),
+    'no-ember-super-in-es-classes': require('./rules/no-ember-super-in-es-classes'),
     'no-ember-testing-in-module-scope': require('./rules/no-ember-testing-in-module-scope'),
     'no-empty-attrs': require('./rules/no-empty-attrs'),
     'no-function-prototype-extensions': require('./rules/no-function-prototype-extensions'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 038f8cdc..73b42caa 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -18,6 +18,7 @@ module.exports = {
   "ember/no-attrs-snapshot": "error",
   "ember/no-capital-letters-in-routes": "error",
   "ember/no-duplicate-dependent-keys": "error",
+  "ember/no-ember-super-in-es-classes": "off",
   "ember/no-ember-testing-in-module-scope": "error",
   "ember/no-empty-attrs": "off",
   "ember/no-function-prototype-extensions": "error",
diff --git a/lib/rules/no-ember-super-in-es-classes.js b/lib/rules/no-ember-super-in-es-classes.js
new file mode 100644
index 00000000..9898b596
--- /dev/null
+++ b/lib/rules/no-ember-super-in-es-classes.js
@@ -0,0 +1,37 @@
+'use strict';
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Prevents use of `this._super` in ES class methods',
+      category: 'Possible Errors',
+      recommended: false,
+      url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-ember-super-in-es-classes.md'
+    },
+    fixable: 'code',
+  },
+
+  create(context) {
+    return {
+      'MethodDefinition MemberExpression[object.type="ThisExpression"][property.name="_super"]': function (node) {
+        context.report({
+          node,
+          message: 'Don\'t use `this._super` in ES classes',
+          fix(fixer) {
+            let method = node;
+            while (method.type !== 'MethodDefinition') {
+              method = method.parent;
+            }
+
+            if (method.key.type === 'Identifier') {
+              return fixer.replaceText(node, `super.${method.key.name}`);
+            }
+
+            const text = context.getSourceCode().getText(method.key);
+            return fixer.replaceText(node, `super[${text}]`);
+          }
+        });
+      }
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/no-ember-super-in-es-classes.js b/tests/lib/rules/no-ember-super-in-es-classes.js
new file mode 100644
index 00000000..ed4a300f
--- /dev/null
+++ b/tests/lib/rules/no-ember-super-in-es-classes.js
@@ -0,0 +1,75 @@
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-ember-super-in-es-classes');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const eslintTester = new RuleTester();
+eslintTester.run('no-ember-super-in-es-classes', rule, {
+  valid: [
+    {
+      code: 'EmberObject.extend({ init() { this._super(); } })',
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'EmberObject.extend({ init(a, b) { this._super(a, b); } })',
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'EmberObject.extend({ init() { this._super(...arguments); } })',
+      parserOptions: { ecmaVersion: 6 }
+    }
+  ],
+  invalid: [
+    {
+      code: 'class Foo { init() { this._super(); } }',
+      output: 'class Foo { init() { super.init(); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { init(a, b) { this._super(a); } }',
+      output: 'class Foo { init(a, b) { super.init(a); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { init() { this._super(...arguments); } }',
+      output: 'class Foo { init() { super.init(...arguments); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { init() { this._super.apply(this, arguments); } }',
+      output: 'class Foo { init() { super.init.apply(this, arguments); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { init() { if (x) { this._super(1); } else { this._super(2); } } }',
+      output: 'class Foo { init() { if (x) { super.init(1); } else { super.init(2); } } }',
+      errors: [
+        { message: "Don't use `this._super` in ES classes" },
+        { message: "Don't use `this._super` in ES classes" },
+      ],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { "a b"() { this._super(); } }',
+      output: 'class Foo { "a b"() { super["a b"](); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      code: 'class Foo { [Symbol.iterator]() { this._super(); } }',
+      output: 'class Foo { [Symbol.iterator]() { super[Symbol.iterator](); } }',
+      errors: [{ message: "Don't use `this._super` in ES classes" }],
+      parserOptions: { ecmaVersion: 6 }
+    }
+  ],
+});