Repository: ember-cli/eslint-plugin-ember
Pull request #747 (2020-03-29 17:09:30)

Add autofixer to `no-get-with-default` rule
The [no-get-with-default](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/no-get-with-default.md) rule is now a recommended rule in the v8 release so it's even more useful to have an autofixer for it.

Before:
```js
const test = this.getWithDefault('property', []);
```
After:
```js
const test = (this.get('property') === undefined ? [] : this.get('property'));
```

This rule converts to `this.get('property')` for safety (in case of nested paths). The `no-get` rule can then convert it to ES5 getters (`this.property`) if safe.

CC: @steventsao. Follow-up to this comment: https://github.com/ember-cli/eslint-plugin-ember/pull/594#pullrequestreview-318738412


Patch:
diff --git a/README.md b/README.md
index 79dea616..a6670ccd 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 |    | Rule ID | Description |
 |:---|:--------|:------------|
 | :white_check_mark: | [avoid-leaking-state-in-ember-objects](./docs/rules/avoid-leaking-state-in-ember-objects.md) | disallow state leakage |
-| :white_check_mark: | [no-get-with-default](./docs/rules/no-get-with-default.md) | disallow usage of the Ember's `getWithDefault` function |
+| :white_check_mark::wrench: | [no-get-with-default](./docs/rules/no-get-with-default.md) | disallow usage of the Ember's `getWithDefault` function |
 | :white_check_mark::wrench: | [no-get](./docs/rules/no-get.md) | require using ES5 getters instead of Ember's `get` / `getProperties` functions |
 |  | [no-proxies](./docs/rules/no-proxies.md) | disallow using array or object proxies |
 | :white_check_mark: | [require-super-in-init](./docs/rules/require-super-in-init.md) | require `this._super` to be called in `init` hooks |
diff --git a/docs/rules/computed-property-getters.md b/docs/rules/computed-property-getters.md
index 72024389..5f2d344d 100644
--- a/docs/rules/computed-property-getters.md
+++ b/docs/rules/computed-property-getters.md
@@ -20,10 +20,8 @@ String option:
 ### always-with-setter
 
 ```javascript
-import EmberObject, { computed } from '@ember/object';
-
 // BAD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', {
     get() {
       // ...
@@ -32,14 +30,14 @@ EmberObject.extend({
 });
 
 // GOOD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', function () {
     // ...
   })
 });
 
 // GOOD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', {
     set() {
       // ...
@@ -54,10 +52,8 @@ EmberObject.extend({
 ### always
 
 ```javascript
-import EmberObject, { computed } from '@ember/object';
-
 // GOOD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', {
     get() {
       // ...
@@ -66,7 +62,7 @@ EmberObject.extend({
 });
 
 // BAD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', function () {
     // ...
   })
@@ -76,17 +72,15 @@ EmberObject.extend({
 ### never
 
 ```javascript
-import EmberObject, { computed } from '@ember/object';
-
 // GOOD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', function () {
     // ...
   })
 });
 
 // BAD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', {
     get() {
       // ...
@@ -95,7 +89,7 @@ EmberObject.extend({
 });
 
 // BAD
-EmberObject.extend({
+Ember.Object.extend({
   fullName: computed('firstName', 'lastName', {
     get() {
       // ...
diff --git a/docs/rules/jquery-ember-run.md b/docs/rules/jquery-ember-run.md
index 6805659f..d2a7714b 100644
--- a/docs/rules/jquery-ember-run.md
+++ b/docs/rules/jquery-ember-run.md
@@ -9,9 +9,7 @@ Using plain jQuery invokes actions out of the Ember Run Loop. In order to have a
 Examples of **incorrect** code for this rule:
 
 ```js
-import $ from 'jquery';
-
-$('#something-rendered-by-jquery-plugin').on('click', () => {
+Ember.$('#something-rendered-by-jquery-plugin').on('click', () => {
   this._handlerActionFromController();
 });
 ```
@@ -19,11 +17,8 @@ $('#something-rendered-by-jquery-plugin').on('click', () => {
 Examples of **correct** code for this rule:
 
 ```javascript
-import $ from 'jquery';
-import { bind } from '@ember/runloop';
-
-$('#something-rendered-by-jquery-plugin').on(
+Ember.$('#something-rendered-by-jquery-plugin').on(
   'click',
-  bind(this, this._handlerActionFromController)
+  Ember.run.bind(this, this._handlerActionFromController)
 );
 ```
diff --git a/docs/rules/no-attrs-snapshot.md b/docs/rules/no-attrs-snapshot.md
index 355aabdb..72945c69 100644
--- a/docs/rules/no-attrs-snapshot.md
+++ b/docs/rules/no-attrs-snapshot.md
@@ -15,9 +15,8 @@ If for some reason you need to do a comparison of arguments we suggest that you
 Examples of **incorrect** code for this rule:
 
 ```js
-import Component from '@ember/component';
 
-export default Component({
+export default Ember.Component({
   init(...args) {
     this._super(...args);
     this.updated = false;
@@ -37,9 +36,8 @@ export default Component({
 Examples of **correct** code for this rule:
 
 ```js
-import Component from '@ember/component';
 
-export default Component({
+export default Ember.Component({
   init(...args) {
     this._super(...args);
     this._valueCache = this.value;
diff --git a/docs/rules/no-computed-properties-in-native-classes.md b/docs/rules/no-computed-properties-in-native-classes.md
index 307b943c..b5a14642 100644
--- a/docs/rules/no-computed-properties-in-native-classes.md
+++ b/docs/rules/no-computed-properties-in-native-classes.md
@@ -11,7 +11,6 @@ This rule disallows using computed properties with native classes.
 Examples of **incorrect** code for this rule:
 
 ```js
-import Component from '@ember/component';
 import { and, or, alias } from '@ember/object/computed';
 
 export default class MyComponent extends Component {
@@ -20,7 +19,6 @@ export default class MyComponent extends Component {
 ```
 
 ```js
-import Component from '@ember/component';
 import { computed } from '@ember/object';
 
 export default class MyComponent extends Component {
@@ -34,14 +32,14 @@ Examples of **correct** code for this rule:
 import { computed } from '@ember/object';
 import Component from '@ember/component';
 
-export default Component.extend({});
+export default Ember.Component.extend({});
 ```
 
 ```js
 import { alias, or, and } from '@ember/object/computed';
 import Component from '@ember/component';
 
-export default Component.extend({});
+export default Ember.Component.extend({});
 ```
 
 ```js
diff --git a/docs/rules/no-get-with-default.md b/docs/rules/no-get-with-default.md
index 250f0d0a..db07571c 100644
--- a/docs/rules/no-get-with-default.md
+++ b/docs/rules/no-get-with-default.md
@@ -1,12 +1,15 @@
 # no-get-with-default
 
+: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.
+
 This rule attempts to catch and prevent the use of `getWithDefault`.
 
 ## Rule Details
 
-Even though the behavior for `getWithDefault` is more defined such that it only falls back to the default value on `undefined`,
-its inconsistency with the native `||` is confusing to many developers who assume otherwise. This rule encourages developers to use
-the native `||` operator instead.
+Even though the behavior for `getWithDefault` is more defined such that it only falls back to the default value on `undefined`, its inconsistency with the native `||` is confusing to many developers who assume otherwise. Instead, this rule encourages developers to use:
+
+- `||` operator
+- ternary operator
 
 In addition, [Nullish Coalescing Operator `??`](https://github.com/tc39/proposal-nullish-coalescing) will land in the JavaScript language soon so developers can leverage safe property access with native support instead of using `getWithDefault`.
 
diff --git a/docs/rules/no-observers.md b/docs/rules/no-observers.md
index 5fe66514..7168c2a8 100644
--- a/docs/rules/no-observers.md
+++ b/docs/rules/no-observers.md
@@ -18,10 +18,8 @@ Observers do have some limited uses. They let you reflect state from your applic
 Examples of **incorrect** code for this rule:
 
 ```js
-import { observer } from '@ember/object';
-
 export default Controller.extend({
-  change: observer('text', function () {
+  change: Ember.observer('text', function () {
     console.log(`change detected: ${this.text}`);
   })
 });
diff --git a/docs/rules/no-side-effects.md b/docs/rules/no-side-effects.md
index b2292a84..24a694bc 100644
--- a/docs/rules/no-side-effects.md
+++ b/docs/rules/no-side-effects.md
@@ -7,8 +7,12 @@ When using computed properties do not introduce side effects. It will make reaso
 ## Examples
 
 ```javascript
-import Component from '@ember/component';
-import { alias, filterBy } from '@ember/object/computed';
+import Ember from 'ember';
+
+const {
+  Component,
+  computed: { filterBy, alias }
+} = Ember;
 
 export default Component.extend({
   init(...args) {
diff --git a/docs/rules/require-super-in-init.md b/docs/rules/require-super-in-init.md
index bc60a436..c829d6c9 100644
--- a/docs/rules/require-super-in-init.md
+++ b/docs/rules/require-super-in-init.md
@@ -9,9 +9,7 @@ When overriding the `init` lifecycle hook inside Ember Components, Controllers,
 Examples of **incorrect** code for this rule:
 
 ```javascript
-import Component from '@ember/component';
-
-export default Component.extend({
+export default Ember.Component.extend({
   init() {
     this.set('items', []);
   }
@@ -21,9 +19,7 @@ export default Component.extend({
 Examples of **correct** code for this rule:
 
 ```javascript
-import Component from '@ember/component';
-
-export default Component.extend({
+export default Ember.Component.extend({
   init(...args) {
     this._super(...args);
     this.set('items', []);
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 8cebc5d8..e704d4be 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -5,15 +5,26 @@
  * definitions, execute "npm run update"
  */
 module.exports = {
+  "ember/alias-model-in-controller": "off",
   "ember/avoid-leaking-state-in-ember-objects": "error",
   "ember/avoid-using-needs-in-controllers": "error",
+  "ember/classic-decorator-hooks": "off",
+  "ember/classic-decorator-no-classic-methods": "off",
   "ember/closure-actions": "error",
+  "ember/computed-property-getters": "off",
   "ember/jquery-ember-run": "error",
+  "ember/named-functions-in-promises": "off",
   "ember/new-module-imports": "error",
+  "ember/no-actions-hash": "off",
   "ember/no-arrow-function-computed-properties": "error",
   "ember/no-attrs-in-components": "error",
   "ember/no-attrs-snapshot": "error",
   "ember/no-capital-letters-in-routes": "error",
+  "ember/no-classic-classes": "off",
+  "ember/no-classic-components": "off",
+  "ember/no-component-lifecycle-hooks": "off",
+  "ember/no-computed-properties-in-native-classes": "off",
+  "ember/no-controllers": "off",
   "ember/no-deeply-nested-dependent-keys-with-each": "error",
   "ember/no-duplicate-dependent-keys": "error",
   "ember/no-ember-super-in-es-classes": "error",
@@ -27,6 +38,7 @@ module.exports = {
   "ember/no-incorrect-computed-macros": "error",
   "ember/no-invalid-debug-function-arguments": "error",
   "ember/no-invalid-dependent-keys": "error",
+  "ember/no-invalid-test-waiters": "off",
   "ember/no-jquery": "error",
   "ember/no-legacy-test-waiters": "error",
   "ember/no-mixins": "error",
@@ -36,18 +48,29 @@ module.exports = {
   "ember/no-on-calls-in-components": "error",
   "ember/no-pause-test": "error",
   "ember/no-private-routing-service": "error",
+  "ember/no-proxies": "off",
+  "ember/no-replace-test-comments": "off",
   "ember/no-restricted-resolver-tests": "error",
   "ember/no-side-effects": "error",
   "ember/no-test-and-then": "error",
   "ember/no-test-import-export": "error",
   "ember/no-test-module-for": "error",
+  "ember/no-unnecessary-index-route": "off",
   "ember/no-unnecessary-route-path-option": "error",
+  "ember/no-unnecessary-service-injection-argument": "off",
   "ember/no-volatile-computed-properties": "error",
+  "ember/order-in-components": "off",
+  "ember/order-in-controllers": "off",
+  "ember/order-in-models": "off",
+  "ember/order-in-routes": "off",
   "ember/require-computed-macros": "error",
   "ember/require-computed-property-dependencies": "error",
   "ember/require-return-from-computed": "error",
   "ember/require-super-in-init": "error",
+  "ember/require-tagless-components": "off",
+  "ember/route-path-style": "off",
   "ember/routes-segments-snake-case": "error",
   "ember/use-brace-expansion": "error",
-  "ember/use-ember-data-rfc-395-imports": "error"
+  "ember/use-ember-data-rfc-395-imports": "error",
+  "ember/use-ember-get-and-set": "off"
 }
\ No newline at end of file
diff --git a/lib/rules/no-get-with-default.js b/lib/rules/no-get-with-default.js
index 31b8f1e7..cf7826f8 100644
--- a/lib/rules/no-get-with-default.js
+++ b/lib/rules/no-get-with-default.js
@@ -15,6 +15,7 @@ module.exports = {
       url:
         'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-get-with-default.md',
     },
+    fixable: 'code',
     schema: [],
   },
   create(context) {
@@ -28,7 +29,13 @@ module.exports = {
           node.arguments.length === 2
         ) {
           // Example: this.getWithDefault('foo', 'bar');
-          context.report(node, ERROR_MESSAGE);
+          context.report({
+            node,
+            message: ERROR_MESSAGE,
+            fix(fixer) {
+              return fix(fixer, context, node, node.arguments[0], node.arguments[1], false);
+            },
+          });
         }
 
         if (
@@ -38,9 +45,37 @@ module.exports = {
           types.isThisExpression(node.arguments[0])
         ) {
           // Example: getWithDefault(this, 'foo', 'bar');
-          context.report(node, ERROR_MESSAGE);
+          context.report({
+            node,
+            message: ERROR_MESSAGE,
+            fix(fixer) {
+              return fix(fixer, context, node, node.arguments[1], node.arguments[2], true);
+            },
+          });
         }
       },
     };
   },
 };
+
+/**
+ * @param {fixer} fixer
+ * @param {context} context
+ * @param {node} node - node with: this.getWithDefault('foo', 'bar');
+ * @param {node} nodeProperty - node with: 'foo'
+ * @param {node} nodeDefault - node with: 'bar'
+ */
+function fix(fixer, context, node, nodeProperty, nodeDefault, useImportedGet) {
+  const sourceCode = context.getSourceCode();
+
+  const nodePropertySourceText = sourceCode.getText(nodeProperty);
+  const nodeDefaultSourceText = sourceCode.getText(nodeDefault);
+
+  // We convert it to use `this.get('property')` here for safety.
+  // The `no-get` rule can then convert it to ES5 getters (`this.property`) if safe.
+  const fixed = useImportedGet
+    ? `(get(this, ${nodePropertySourceText}) === undefined ? ${nodeDefaultSourceText} : get(this, ${nodePropertySourceText}))`
+    : `(this.get(${nodePropertySourceText}) === undefined ? ${nodeDefaultSourceText} : this.get(${nodePropertySourceText}))`;
+
+  return fixer.replaceText(node, fixed);
+}
diff --git a/scripts/update-rules.js b/scripts/update-rules.js
index b0903f5d..c92ffff1 100644
--- a/scripts/update-rules.js
+++ b/scripts/update-rules.js
@@ -96,9 +96,8 @@ ${deprecatedRules
 const recommendedRules = rules.reduce((obj, entry) => {
   const name = `ember/${entry[0]}`;
   const recommended = entry[1].meta.docs.recommended;
-  if (recommended) {
-    obj[name] = 'error'; // eslint-disable-line no-param-reassign
-  }
+  const status = recommended ? 'error' : 'off';
+  obj[name] = status; // eslint-disable-line no-param-reassign
   return obj;
 }, {});
 


Test patch:
diff --git a/docs/rules/no-ember-testing-in-module-scope.md b/docs/rules/no-ember-testing-in-module-scope.md
index 451540c8..1e826272 100644
--- a/docs/rules/no-ember-testing-in-module-scope.md
+++ b/docs/rules/no-ember-testing-in-module-scope.md
@@ -14,8 +14,6 @@ of destructuring.
 Examples of **incorrect** code for this rule:
 
 ```js
-import Component from '@ember/component';
-
 export default Component.extend({
   init() {
     this.isTesting = Ember.testing;
@@ -25,9 +23,8 @@ export default Component.extend({
 
 ```js
 import Ember from 'ember';
-import Component from '@ember/component';
 
-export default Component.extend({
+export default Ember.Component.extend({
   isTesting: Ember.testing
 });
 ```
@@ -48,7 +45,6 @@ Examples of **correct** code for this rule:
 
 ```javascript
 import Ember from 'ember';
-import Component from '@ember/component';
 
 export default Component.extend({
   someMethod() {
@@ -63,7 +59,6 @@ export default Component.extend({
 
 ```js
 import Ember from 'ember';
-import Service from '@ember/service';
 
 export default Service.extend({
   foo() {
diff --git a/tests/lib/rules/no-get-with-default.js b/tests/lib/rules/no-get-with-default.js
index f43911f9..d55c9e30 100644
--- a/tests/lib/rules/no-get-with-default.js
+++ b/tests/lib/rules/no-get-with-default.js
@@ -17,25 +17,70 @@ ruleTester.run('no-get-with-default', rule, {
     "getWithDefault.testMethod(testClass, 'key', [])",
   ],
   invalid: [
+    // this.getWithDefault
     {
-      code: "const test = this.getWithDefault('key', []);",
+      code: "const test = this.getWithDefault('key', []);", // With a string property.
+      output: "const test = (this.get('key') === undefined ? [] : this.get('key'));",
       errors: [
         {
           message: ERROR_MESSAGE,
           type: 'CallExpression',
         },
       ],
-      output: null,
     },
     {
-      code: "const test = getWithDefault(this, 'key', []);",
+      code: 'const test = this.getWithDefault(SOME_VARIABLE, []);', // With a variable property.
+      output:
+        'const test = (this.get(SOME_VARIABLE) === undefined ? [] : this.get(SOME_VARIABLE));',
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: "this.getWithDefault('name', '').trim()",
+      output: "(this.get('name') === undefined ? '' : this.get('name')).trim()", // Parenthesis matter here.
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+
+    // getWithDefault (imported)
+    {
+      code: "const test = getWithDefault(this, 'key', []);", // With a string property.
+      output: "const test = (get(this, 'key') === undefined ? [] : get(this, 'key'));",
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: 'const test = getWithDefault(this, SOME_VARIABLE, []);', // With a variable property.
+      output:
+        'const test = (get(this, SOME_VARIABLE) === undefined ? [] : get(this, SOME_VARIABLE));',
+      errors: [
+        {
+          message: ERROR_MESSAGE,
+          type: 'CallExpression',
+        },
+      ],
+    },
+    {
+      code: "getWithDefault(this, 'name', '').trim()",
+      output: "(get(this, 'name') === undefined ? '' : get(this, 'name')).trim()", // Parenthesis matter here.
       errors: [
         {
           message: ERROR_MESSAGE,
           type: 'CallExpression',
         },
       ],
-      output: null,
     },
   ],
 });
diff --git a/tests/rule-setup.js b/tests/rule-setup.js
index 501cbcb6..c4073df5 100644
--- a/tests/rule-setup.js
+++ b/tests/rule-setup.js
@@ -7,10 +7,7 @@ const recommendedRules = require('../lib/recommended-rules.js');
 const octaneRules = require('../lib/octane-rules.js');
 
 const RULE_NAMES = Object.keys(rules);
-const RECOMMENDED_RULE_NAMES = Object.keys(recommendedRules).map((name) =>
-  name.replace('ember/', '')
-);
-const OCTANE_RULE_NAMES = Object.keys(octaneRules).map((name) => name.replace('ember/', ''));
+const OCTANE_RULE_NAMES = Object.keys(octaneRules);
 
 describe('rules setup is correct', function () {
   it('should have a list of exported rules and rules directory that match', function () {
@@ -23,16 +20,17 @@ describe('rules setup is correct', function () {
     );
   });
 
-  it('should list all recommended rules in the recommended rules file', function () {
-    const recommendedRuleNamesFromMeta = RULE_NAMES.filter(
-      (key) => rules[key].meta.docs.recommended
+  it('should list all rules in the recommended rules file', function () {
+    expect(RULE_NAMES).toStrictEqual(
+      Object.keys(recommendedRules).map((file) => file.replace('ember/', ''))
     );
-    expect(RECOMMENDED_RULE_NAMES).toStrictEqual(recommendedRuleNamesFromMeta);
   });
 
-  it('should list all octane rules in the octane rules file', function () {
-    const octaneRuleNamesFromMeta = RULE_NAMES.filter((key) => rules[key].meta.docs.octane);
-    expect(OCTANE_RULE_NAMES).toStrictEqual(octaneRuleNamesFromMeta);
+  it('should list all rules in the octane rules file', function () {
+    const octaneRuleNames = Object.keys(rules).filter((key) => rules[key].meta.docs.octane);
+    expect(OCTANE_RULE_NAMES.map((file) => file.replace('ember/', ''))).toStrictEqual(
+      octaneRuleNames
+    );
   });
 
   it('should have tests for all rules', function () {