Repository: vuejs/eslint-plugin-vue
Pull request #78 (2017-07-15 16:21:57)

Add `return-in-computed-property` rule.
This PR implements rules proposed in #75

**DONE:**
- [x] Basic check for `return`
- [x] Check if all paths `return` value
- [x] Option: `treatUndefinedAsUnspecified` - disallows implicitly returning undefined with a `return;` statement.

armano2: @michalsnik can you review this pull request?
michalsnik: Sure, later today I'll try to push my PR and both of yours forward. Busy time :)
armano2: It's my first time working with rules from eslint (developing them) but they seems not to complex to make.

sorry for pushing you to test my code 🗡
armano2: @michalsnik cleaned up and merged
chrisvfritz: Will this also flag computed properties that technically have a return somewhere in their content, but not directly? For example:

``` js
computed: {
  foo: function () {
    function bar () {
      return this.baz * 2 
    }
    bar()
  }
}
```
armano2: @chrisvfritz yes (test for this case added)

Patch:
diff --git a/docs/rules/return-in-computed-property.md b/docs/rules/return-in-computed-property.md
new file mode 100644
index 00000000..5f66d08b
--- /dev/null
+++ b/docs/rules/return-in-computed-property.md
@@ -0,0 +1,44 @@
+# Enforces that a return statement is present in computed property (return-in-computed-property)
+
+## :book: Rule Details
+
+This rule enforces that a `return` statement is present in `computed` properties.
+
+:-1: Examples of **incorrect** code for this rule:
+
+```js
+export default {
+  computed: {
+    foo () {
+    },
+    bar: function () {
+    }
+  }
+}
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```js
+export default {
+  computed: {
+    foo () {
+      return true
+    },
+    bar: function () {
+      return false
+    }
+  }
+}
+```
+
+## :wrench: Options
+
+This rule has an object option:
+- `"treatUndefinedAsUnspecified"`: `true` (default) disallows implicitly returning undefined with a `return;` statement.
+
+```
+vue/return-in-computed-property: [2, {
+  treatUndefinedAsUnspecified: true
+}]
+```
diff --git a/lib/rules/no-invalid-v-on.js b/lib/rules/no-invalid-v-on.js
index c2a62aeb..a271d45f 100644
--- a/lib/rules/no-invalid-v-on.js
+++ b/lib/rules/no-invalid-v-on.js
@@ -33,13 +33,6 @@ const VERB_MODIFIERS = new Set([
 function create (context) {
   utils.registerTemplateBodyVisitor(context, {
     "VAttribute[directive=true][key.name='on']" (node) {
-      if (!node.key.argument) {
-        context.report({
-          node,
-          loc: node.loc,
-          message: "'v-on' directives require event names."
-        })
-      }
       for (const modifier of node.key.modifiers) {
         if (!VALID_MODIFIERS.has(modifier) && !Number.isInteger(parseInt(modifier, 10))) {
           context.report({
diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js
new file mode 100644
index 00000000..5a336781
--- /dev/null
+++ b/lib/rules/return-in-computed-property.js
@@ -0,0 +1,114 @@
+/**
+ * @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+
+function create (context) {
+  const options = context.options[0] || {}
+  const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false)
+
+  let funcInfo = {
+    funcInfo: null,
+    codePath: null,
+    hasReturn: false,
+    hasReturnValue: false,
+    node: null
+  }
+  const forbiddenNodes = []
+
+  // ----------------------------------------------------------------------
+  // Helpers
+  // ----------------------------------------------------------------------
+  function isValidReturn () {
+    if (!funcInfo.hasReturn) {
+      return false
+    }
+    return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
+  }
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  return Object.assign({},
+    {
+      onCodePathStart (codePath, node) {
+        funcInfo = {
+          codePath,
+          funcInfo: funcInfo,
+          hasReturn: false,
+          hasReturnValue: false,
+          node
+        }
+      },
+      onCodePathEnd () {
+        funcInfo = funcInfo.funcInfo
+      },
+      ReturnStatement (node) {
+        funcInfo.hasReturn = true
+        funcInfo.hasReturnValue = Boolean(node.argument)
+      },
+      'FunctionExpression:exit' (node) {
+        if (!isValidReturn()) {
+          forbiddenNodes.push({
+            hasReturn: funcInfo.hasReturn,
+            node: funcInfo.node,
+            type: 'return'
+          })
+        }
+      }
+    },
+    utils.executeOnVueComponent(context, properties => {
+      const computedProperties = utils.getComputedProperties(properties)
+
+      computedProperties.forEach(cp => {
+        forbiddenNodes.forEach(el => {
+          if (
+            cp.value &&
+            el.node.loc.start.line >= cp.value.loc.start.line &&
+            el.node.loc.end.line <= cp.value.loc.end.line
+          ) {
+            context.report({
+              node: el.node,
+              message: 'Expected to return a value in "{{name}}" computed property.',
+              data: {
+                name: cp.key
+              }
+            })
+          }
+        })
+      })
+    })
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforces that a return statement is present in computed property.',
+      category: 'Possible Errors',
+      recommended: false
+    },
+    fixable: null,  // or "code" or "whitespace"
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          treatUndefinedAsUnspecified: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+
+  create
+}


Test patch:
diff --git a/tests/lib/rules/no-invalid-v-bind.js b/tests/lib/rules/no-invalid-v-bind.js
index cf1a0a14..38592097 100644
--- a/tests/lib/rules/no-invalid-v-bind.js
+++ b/tests/lib/rules/no-invalid-v-bind.js
@@ -70,6 +70,10 @@ tester.run('no-invalid-v-bind', rule, {
     {
       filename: 'test.vue',
       code: "<template><div :aaa.camel='bbb'></div></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-bind='$attrs' /></template>"
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/no-invalid-v-on.js b/tests/lib/rules/no-invalid-v-on.js
index 820a9509..06ba18b4 100644
--- a/tests/lib/rules/no-invalid-v-on.js
+++ b/tests/lib/rules/no-invalid-v-on.js
@@ -54,14 +54,13 @@ tester.run('no-invalid-v-on', rule, {
     {
       filename: 'test.vue',
       code: '<template><div v-on:click.native.stop></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-on="$listeners"></div></template>'
     }
   ],
   invalid: [
-    {
-      filename: 'test.vue',
-      code: '<template><div v-on="foo"></div></template>',
-      errors: ["'v-on' directives require event names."]
-    },
     {
       filename: 'test.vue',
       code: '<template><div v-on:click.aaa="foo"></div></template>',
diff --git a/tests/lib/rules/return-in-computed-property.js b/tests/lib/rules/return-in-computed-property.js
new file mode 100644
index 00000000..4ee565e4
--- /dev/null
+++ b/tests/lib/rules/return-in-computed-property.js
@@ -0,0 +1,196 @@
+/**
+ * @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/return-in-computed-property')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('return-in-computed-property', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return true
+            },
+            bar: function () {
+              return false
+            },
+            bar3: {
+              set () {
+                return true
+              },
+              get () {
+                return true
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: {
+              get () {
+                return
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: false }]
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              if (a) {
+                return
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: {
+              set () {
+              },
+              get () {
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 7
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              function bar () {
+                return this.baz * 2
+              }
+              bar()
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+            },
+            bar () {
+              return
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: false }],
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: true }],
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    }
+  ]
+})