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

Issue:
New Linter Rule - set() must return a value
having a linter rule for this would help prevent some bugs
(at least until the behavior changes to be what people expect)
https://github.com/emberjs/rfcs/pull/79

Bad:
```js
height: 100,
goldenRatioWidth: computed('height', {
  get() {
    return this.get('height') * 1.618;
  },
  set(_, value) {
    this.set('height', value / 1.618);
  }
})
```

Good
```js
height: 100,
goldenRatioWidth: computed('height', {
  get() {
    return this.get('height') * 1.618;
  },
  set(_, value) {
    const newHeight = value / 1.618;
    this.set('height', newHeight);
    return newHeight;
  }
})
```

`set()` must have a `return`.

(I mistakenly created this over [here](https://github.com/ember-cli/ember-cli-eslint/issues/232) first)

Pull request description:
Add new 'require-return-from-computed' rule
Fixes #247.

Patch:
diff --git a/README.md b/README.md
index d1128e89..14c90916 100644
--- a/README.md
+++ b/README.md
@@ -115,6 +115,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :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 |
 |  | [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | Catch usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. |
 | :white_check_mark: | [no-side-effects](./docs/rules/no-side-effects.md) | Warns about unexpected side effects in computed properties |
+|  | [require-return-from-computed](./docs/rules/require-return-from-computed.md) | Warns about missing return statements in computed properties |
 | :white_check_mark: | [require-super-in-init](./docs/rules/require-super-in-init.md) | Enforces super calls in init hooks |
 | :white_check_mark: | [routes-segments-snake-case](./docs/rules/routes-segments-snake-case.md) | Enforces usage of snake_cased dynamic segments in routes |
 
diff --git a/docs/rules/require-return-from-computed.md b/docs/rules/require-return-from-computed.md
new file mode 100644
index 00000000..0d03fc99
--- /dev/null
+++ b/docs/rules/require-return-from-computed.md
@@ -0,0 +1,44 @@
+## Always return a value from a computed property function
+
+### Rule name: `require-return-from-computed`
+
+When using computed properties always return a value.
+
+```javascript
+import Ember from 'ember';
+
+const {
+  Component,
+  computed
+} = Ember;
+
+export default Component.extend({
+  firstName: null,
+  lastName: null,
+
+  // GOOD
+  fullName: computed('firstName', 'lastName', {
+    get(key) {
+      return `${this.get('firstName')} ${this.get('lastName')}`;
+    },
+    set(key, value) {
+      let [firstName, lastName] = value.split(/\s+/);
+      this.set('firstName', firstName);
+      this.set('lastName',  lastName);
+      return value;
+    }
+  }),
+
+  // BAD
+  fullName: computed('firstName', 'lastName', {
+    get(key) {
+      return `${this.get('firstName')} ${this.get('lastName')}`;
+    },
+    set(key, value) {
+      let [firstName, lastName] = value.split(/\s+/);
+      this.set('firstName', firstName);
+      this.set('lastName',  lastName);
+    }
+  })
+});
+```
diff --git a/lib/index.js b/lib/index.js
index ba66f742..54252130 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -38,6 +38,7 @@ module.exports = {
     'order-in-controllers': require('./rules/order-in-controllers'),
     'order-in-models': require('./rules/order-in-models'),
     'order-in-routes': require('./rules/order-in-routes'),
+    'require-return-from-computed': require('./rules/require-return-from-computed'),
     'require-super-in-init': require('./rules/require-super-in-init'),
     'routes-segments-snake-case': require('./rules/routes-segments-snake-case'),
     'use-brace-expansion': require('./rules/use-brace-expansion'),
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index d350e052..f2a7276d 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -40,8 +40,9 @@ module.exports = {
   "ember/order-in-controllers": "off",
   "ember/order-in-models": "off",
   "ember/order-in-routes": "off",
+  "ember/require-return-from-computed": "off",
   "ember/require-super-in-init": "error",
   "ember/routes-segments-snake-case": "error",
   "ember/use-brace-expansion": "error",
   "ember/use-ember-get-and-set": "off"
-}
+}
\ No newline at end of file
diff --git a/lib/rules/require-return-from-computed.js b/lib/rules/require-return-from-computed.js
new file mode 100644
index 00000000..ee9863de
--- /dev/null
+++ b/lib/rules/require-return-from-computed.js
@@ -0,0 +1,66 @@
+'use strict';
+
+const ember = require('../utils/ember');
+
+//------------------------------------------------------------------------------
+// General rule - Always return a value from computed properties
+//------------------------------------------------------------------------------
+
+function isReachable(segment) {
+  return segment.reachable;
+}
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Warns about missing return statements in computed properties',
+      category: 'Possible Errors',
+      recommended: false,
+      url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-return-from-computed.md'
+    },
+    fixable: null, // or "code" or "whitespace"
+  },
+
+  create(context) {
+    const message = 'Always return a value from computed properties';
+
+    const report = function (node) {
+      context.report(node, message);
+    };
+
+    let funcInfo = {
+      upper: null,
+      codePath: null,
+      shouldCheck: false,
+      node: null
+    };
+
+    function checkLastSegment(node) {
+      if (funcInfo.shouldCheck &&
+          funcInfo.codePath.currentSegments.some(isReachable)
+      ) {
+        report(node);
+      }
+    }
+
+    return {
+      onCodePathStart(codePath) {
+        funcInfo = {
+          upper: funcInfo,
+          codePath,
+          shouldCheck: context.getAncestors().findIndex(ember.isComputedProp) > -1,
+        };
+      },
+
+      onCodePathEnd() {
+        funcInfo = funcInfo.upper;
+      },
+
+      'FunctionExpression:exit': function (node) {
+        if (ember.isComputedProp(node.parent) || ember.isComputedProp(node.parent.parent.parent)) {
+          checkLastSegment(node);
+        }
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/require-return-from-computed.js b/tests/lib/rules/require-return-from-computed.js
new file mode 100644
index 00000000..a0d61185
--- /dev/null
+++ b/tests/lib/rules/require-return-from-computed.js
@@ -0,0 +1,85 @@
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-return-from-computed');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const eslintTester = new RuleTester();
+eslintTester.run('require-return-from-computed', rule, {
+  valid: [
+    {
+      code: 'let foo = computed("test", function() { return ""; })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'let foo = computed("test", { get() { return true; }, set() { return true; } })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'let foo = computed("test", function() { if (true) { return ""; } return ""; })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'let foo = computed("test", { get() { data.forEach(function() { }); return true; }, set() { return true; } })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    },
+    {
+      code: 'let foo = computed("test", function() { data.forEach(function() { }); return ""; })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+    }
+  ],
+  invalid: [
+    {
+      code: 'let foo = computed("test", function() { })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Always return a value from computed properties'
+      }]
+    },
+    {
+      code: 'let foo = computed("test", function() { if (true) { return ""; } })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Always return a value from computed properties'
+      }]
+    },
+    {
+      code: 'let foo = computed("test", { get() {}, set() {} })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [
+        {
+          message: 'Always return a value from computed properties'
+        },
+        {
+          message: 'Always return a value from computed properties'
+        }
+      ]
+    },
+    {
+      code: 'let foo = computed("test", function() { })',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Always return a value from computed properties'
+      }]
+    },
+    {
+      code: 'let foo = computed({ get() { return "foo"; }, set() { }})',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Always return a value from computed properties'
+      }]
+    },
+    {
+      code: 'let foo = computed({ get() { }, set() { return "foo"; }})',
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Always return a value from computed properties'
+      }]
+    }
+  ],
+});