Repository: vuejs/eslint-plugin-vue
Pull request #612 (2018-10-14 05:32:19)

⭐️New: Add rule `no-boolean-default`
_I haven't added documentation yet but I can add one if you guys like the rule._

The rule prevents Boolean props from having a default value. I put this under `essential` as it follows HTML standards of always defaulting boolean attributes to false. Otherwise, users would have to do `:prop="false"` to turn it off.

The rule has the following options:
`'constructor'` (default) enforces that Boolean props must use the constructor.
`'no-default'` allows a prop definition object, but enforces that the `default` property not be defined.
`'default-fase'` enforces that the default can be set but must be set to `false`.

hirokiosame: Thank you @mysticatea I made the updates! Let me know if there's anything else I can do.
hirokiosame: @armano2 Oops I misread his comment to add the spread outside of the prop schema. I updated the PR, thanks for catching
armano2: @hirokiosame thank you 👍 
michalsnik: @armano2 I updated this PR, please take a look one more time :)
hirokiosame: Looks great, thanks for the updates @michalsnik 

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index f7e7d7ec..6b8b2df5 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -149,12 +149,14 @@ For example:
 | [vue/eqeqeq](./eqeqeq.md) | require the use of `===` and `!==` | :wrench: |
 | [vue/key-spacing](./key-spacing.md) | enforce consistent spacing between keys and values in object literal properties | :wrench: |
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  |
+| [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
 | [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/no-boolean-default.md b/docs/rules/no-boolean-default.md
new file mode 100644
index 00000000..74e53f92
--- /dev/null
+++ b/docs/rules/no-boolean-default.md
@@ -0,0 +1,49 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-boolean-default
+description: disallow boolean defaults
+---
+# vue/no-boolean-default
+> disallow boolean defaults
+
+- :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.
+
+The rule prevents Boolean props from having a default value.
+
+
+## :book: Rule Details
+The rule is to enforce the HTML standard of always defaulting boolean attributes to false.
+
+<eslint-code-block fix :rules="{'vue/no-boolean-default': ['error']}">
+
+```vue
+<script>
+export default {
+  props: {
+    foo: {
+      type: Boolean,
+      default: true
+    },
+    bar: {
+      type: Boolean
+    }
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+- `'no-default'` (default) allows a prop definition object, but enforces that the `default` property not be defined.
+- `'default-false'` enforces that the default can be set but must be set to `false`.
+
+```json
+  "vue/no-boolean-default": ["error", "no-default|default-false"]
+```
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-boolean-default.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-boolean-default.js)
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..a7627fca 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -32,6 +32,7 @@ module.exports = {
     'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
     'name-property-casing': require('./rules/name-property-casing'),
     'no-async-in-computed-properties': require('./rules/no-async-in-computed-properties'),
+    'no-boolean-default': require('./rules/no-boolean-default'),
     'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
     'no-dupe-keys': require('./rules/no-dupe-keys'),
     'no-duplicate-attributes': require('./rules/no-duplicate-attributes'),
@@ -68,6 +69,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/no-boolean-default.js b/lib/rules/no-boolean-default.js
new file mode 100644
index 00000000..d0e3f36a
--- /dev/null
+++ b/lib/rules/no-boolean-default.js
@@ -0,0 +1,92 @@
+/**
+ * @fileoverview Prevents boolean defaults from being set
+ * @author Hiroki Osame
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+function isBooleanProp (prop) {
+  return (
+    prop.type === 'Property' &&
+    prop.key.type === 'Identifier' &&
+    prop.key.name === 'type' &&
+    prop.value.type === 'Identifier' &&
+    prop.value.name === 'Boolean'
+  )
+}
+
+function getBooleanProps (props) {
+  return props
+    .filter(prop => (
+      prop.value &&
+      prop.value.properties &&
+      prop.value.properties.find(isBooleanProp)
+    ))
+}
+
+function getDefaultNode (propDef) {
+  return propDef.value.properties.find(p => {
+    return (
+      p.type === 'Property' &&
+      p.key.type === 'Identifier' &&
+      p.key.name === 'default'
+    )
+  })
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow boolean defaults',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-boolean-default.html'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        enum: ['default-false', 'no-default']
+      }
+    ]
+  },
+
+  create (context) {
+    return utils.executeOnVueComponent(context, (obj) => {
+      const props = utils.getComponentProps(obj)
+      const booleanProps = getBooleanProps(props)
+
+      if (!booleanProps.length) return
+
+      const booleanType = context.options[0] || 'no-default'
+
+      booleanProps.forEach((propDef) => {
+        const defaultNode = getDefaultNode(propDef)
+
+        switch (booleanType) {
+          case 'no-default':
+            if (defaultNode) {
+              context.report({
+                node: defaultNode,
+                message: 'Boolean prop should not set a default (Vue defaults it to false).'
+              })
+            }
+            break
+
+          case 'default-false':
+            if (defaultNode.value.value !== false) {
+              context.report({
+                node: defaultNode,
+                message: 'Boolean prop should be defaulted to false.'
+              })
+            }
+            break
+        }
+      })
+    })
+  }
+}
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/no-boolean-default.js b/tests/lib/rules/no-boolean-default.js
new file mode 100644
index 00000000..9f87a3a3
--- /dev/null
+++ b/tests/lib/rules/no-boolean-default.js
@@ -0,0 +1,163 @@
+/**
+ * @fileoverview Prevents boolean defaults from being set
+ * @author Hiroki Osame
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/no-boolean-default')
+
+var RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module'
+  }
+})
+ruleTester.run('no-boolean-default', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            enabled: Boolean
+          }
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        const props = {};
+        export default {
+          props: {
+            ...props,
+            enabled: Boolean
+          }
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        const data = {};
+        export default {
+          props: {
+            enabled: {
+              type: Boolean,
+              ...data
+            }
+          }
+        }
+      `,
+      options: ['no-default']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        const data = {};
+        export default {
+          props: {
+            enabled: {
+              type: Boolean,
+              default: false,
+              ...data
+            }
+          }
+        }
+      `,
+      options: ['default-false']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        const data = {};
+        export default {
+          props: {
+            enabled: data
+          }
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        const data = {};
+        export default {
+          props: {
+            enabled: {
+              ...data
+            }
+          }
+        }
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            enabled: {
+              type: Boolean,
+              default: true,
+            }
+          }
+        }
+      `,
+      options: ['default-false'],
+      errors: [{
+        message: 'Boolean prop should be defaulted to false.',
+        line: 6
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            enabled: {
+              type: Boolean,
+              default: null,
+            }
+          }
+        }
+      `,
+      options: ['default-false'],
+      errors: [{
+        message: 'Boolean prop should be defaulted to false.',
+        line: 6
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            enabled: {
+              type: Boolean,
+              default: false,
+            }
+          }
+        }
+      `,
+      options: ['no-default'],
+      errors: [{
+        message: 'Boolean prop should not set a default (Vue defaults it to false).',
+        line: 6
+      }]
+    }
+  ]
+})
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']
+    }
+  ]
+})