Repository: vuejs/eslint-plugin-vue
Pull request #481 (2018-05-27 09:39:14)

⭐️New: Add `vue/v-on-parens` rule
A rule that enforces the consistent use of parentheses after method calls — not actually method calls without parentheses, but you get what I mean — in `v-on` directives.

* `'always'`: `<button @click="foo()">Foo</button>`
* `'never'`: `<button @click="foo">Foo</button>`

I think this rule is useful because it enforces consistency (always good) and when set to `'never'` results in smaller output (without unnecessary "function wrapping").

---

The rule includes — based on my tests with real-world projects — reliable fixers.
It does **not** support the new 2.4.0+ `v-on` object syntax because other rules also don't.

I'd be happy about feedback on:

- what the name of this rule should be
  - I don't like the current one a lot, but am not quite sure which name would be better: maybe `v-on-avoid-inline-statements` (that sounds better, but `'always'` and `'never'` no longer make that much sense)?
- how the error messages and docs are worded
  - Are they too complicated?
- whether this rule should be `'recommended'`

chrisvfritz: Hm, my initial thought is there might be two possible rules here:

- `v-on-avoid-inline-statement` (values: "error", "off")
- `v-on-function-call` (values: "always", "never", "only-args", "off")

I'll walk through these in a little more detail.

### `v-on-avoid-inline-statement`

I think "avoid" is an important word here, because there will always be unavoidable exceptions when a new variable is defined in the template scope (e.g. with `v-for` and `slot-scope`). For example:

```vue
<button 
  v-for="todo in todos"
  @click="openTodoModal(todo)"
/>
```

The rule could even help people avoid:

```vue
<input @input="$emit('input', $event.target.value)">
```

in favor of:

```vue
<template>
  <input @input="handleInput">
</template>

<script>
export default {
  methods: {
    handleInput(event) {
      this.$emit('input', event.target.value)
    }
  }
}
</script>
```

Then I would personally argue that this is unhelpfully extreme, but I've known people (particularly coming from React) who might prefer this pattern. For people like me though, an option to whitelist certain methods, like `$emit`, could be helpful. 

I think this rule should probably stay uncategorized for now though.

### `v-on-function-call`

This one is similar to the first option, but covers a greater variety of use cases:

- Some people might always want the parentheses (e.g. `@click="doSomething()"` instead of `@click="doSomething"` to make it clearer that a function is being called on click.
- Some might only want parentheses used when the function has arguments (e.g. `@click="doSomething('hello')"` is OK, but not `@click="doSomething()"`.
- Some might never want to call a functional inline, like those I mentioned in the first rule idea.

Again, I don't think this rule should be categorized for now.

What do you think @michalsnik?




michalsnik: I like the idea behind `v-on-function-call` @chrisvfritz 👍 
michalsnik: Now that I wanted to update this PR and merge, I think that current implementation is perfectly fine and I'm happy to proceed with it as is. So that we only report methods that don't have any arguments. So if anyone would want to pass argument - he/she can and the rule will not throw any warnings about parantheses being used. For that case we can have separate rule if we'll find it valuable.
michalsnik: @armano2 @chrisvfritz can you take a look once again?
michalsnik: @ota-meshi @armano2 I updated this PR, can give it another look? :)

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f7e7d7ec..3af0a000 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -155,6 +155,7 @@ For example:
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
 | [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators | :wrench: |
 | [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators | :wrench: |
+| [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: |
 
 ## Deprecated
 
diff --git a/docs/rules/v-on-function-call.md b/docs/rules/v-on-function-call.md
new file mode 100644
index 00000000..a3a5a3c8
--- /dev/null
+++ b/docs/rules/v-on-function-call.md
@@ -0,0 +1,77 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/v-on-function-call
+description: enforce or forbid parentheses after method calls without arguments in `v-on` directives
+---
+# vue/v-on-function-call
+> enforce or forbid parentheses after method calls without arguments in `v-on` directives
+
+- :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.
+
+## :book: Rule Details
+
+:-1: Example of **incorrect** code for this rule:
+
+```html
+<button v-on:click="closeModal()">
+  Close
+</button>
+```
+
+:+1: Example of **correct** code for this rule:
+
+```html
+<button v-on:click="closeModal">
+  Close
+</button>
+```
+
+## :wrench: Options
+
+Default is set to `never`.
+
+```
+'vue/v-on-parens': [2, 'always'|'never']
+```
+
+### `"always"` - Always use parentheses in `v-on` directives
+
+:-1: Example of **incorrect** code:
+
+```html
+<button v-on:click="closeModal">
+  Close
+</button>
+```
+
+:+1: Example of **correct** code:
+
+```html
+<button v-on:click="closeModal()">
+  Close
+</button>
+```
+
+### `"never"` - Never use parentheses in `v-on` directives for method calls without arguments
+
+:-1: Example of **incorrect** code:
+
+```html
+<button v-on:click="closeModal()">
+  Close
+</button>
+```
+
+:+1: Example of **correct** code:
+
+```html
+<button v-on:click="closeModal">
+  Close
+</button>
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/v-on-function-call.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/v-on-function-call.js)
diff --git a/lib/index.js b/lib/index.js
index 88173639..6a26fe06 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -68,6 +68,7 @@ module.exports = {
     'this-in-template': require('./rules/this-in-template'),
     'use-v-on-exact': require('./rules/use-v-on-exact'),
     'v-bind-style': require('./rules/v-bind-style'),
+    'v-on-function-call': require('./rules/v-on-function-call'),
     'v-on-style': require('./rules/v-on-style'),
     'valid-template-root': require('./rules/valid-template-root'),
     'valid-v-bind': require('./rules/valid-v-bind'),
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index d41798c8..a63b477e 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -22,6 +22,8 @@ const defaultOrder = [
   'data',
   'computed',
   'watch',
+  'asyncData',
+  'fetch',
   'LIFECYCLE_HOOKS',
   'methods',
   ['template', 'render'],
diff --git a/lib/rules/v-on-function-call.js b/lib/rules/v-on-function-call.js
new file mode 100644
index 00000000..5dd76b2e
--- /dev/null
+++ b/lib/rules/v-on-function-call.js
@@ -0,0 +1,60 @@
+/**
+ * @author Niklas Higi
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce or forbid parentheses after method calls without arguments in `v-on` directives',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
+    },
+    fixable: 'code',
+    schema: [
+      { enum: ['always', 'never'] }
+    ]
+  },
+
+  create (context) {
+    const always = context.options[0] === 'always'
+
+    return utils.defineTemplateBodyVisitor(context, {
+      "VAttribute[directive=true][key.name='on'][key.argument!=null] > VExpressionContainer > Identifier" (node) {
+        if (!always) return
+        context.report({
+          node,
+          loc: node.loc,
+          message: "Method calls inside of 'v-on' directives must have parentheses."
+        })
+      },
+
+      "VAttribute[directive=true][key.name='on'][key.argument!=null] VOnExpression > ExpressionStatement > *" (node) {
+        if (!always && node.type === 'CallExpression' && node.arguments.length === 0) {
+          context.report({
+            node,
+            loc: node.loc,
+            message: "Method calls without arguments inside of 'v-on' directives must not have parentheses.",
+            fix: fixer => {
+              const nodeString = context.getSourceCode().getText().substring(node.range[0], node.range[1])
+              // This ensures that parens are also removed if they contain whitespace
+              const parensLength = nodeString.match(/\(\s*\)\s*$/)[0].length
+              return fixer.removeRange([node.end - parensLength, node.end])
+            }
+          })
+        }
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js
new file mode 100644
index 00000000..cfcbc005
--- /dev/null
+++ b/tests/lib/rules/v-on-function-call.js
@@ -0,0 +1,72 @@
+/**
+ * @author Niklas Higi
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/v-on-function-call')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('v-on-function-call', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo(123)"></div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo(123)"></div></template>',
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo()"></div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo"></div></template>',
+      options: ['never']
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo"></div></template>',
+      output: `<template><div @click="foo"></div></template>`,
+      errors: ["Method calls inside of 'v-on' directives must have parentheses."],
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo()"></div></template>',
+      output: `<template><div @click="foo"></div></template>`,
+      errors: ["Method calls without arguments inside of 'v-on' directives must not have parentheses."],
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @click="foo( )"></div></template>',
+      output: `<template><div @click="foo"></div></template>`,
+      errors: ["Method calls without arguments inside of 'v-on' directives must not have parentheses."],
+      options: ['never']
+    }
+  ]
+})