Repository: vuejs/eslint-plugin-vue (js)

Issue:
Button tags require type.
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->
All `<button>` tags require the type to be set.
**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
- [ ] Enforces code style
- [X] Warns about a potential error
- [ ] Suggests an alternate way of doing something
- [ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**
```vue
<button>Test</button>
<button class="btn btn-danger">Delete</button>
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
Forgetting the `type` attribute on a button defaults it to being a submit type. This is nearly never what is intended, especially in your average one-page application.

`type="button"` stops the webpage from submitting the form, instead allowing the `@click` handler to work without the page reloading soon after.

`type="submit"` can be used explicitly to allow the default behaviour.

Pull request:
New: vue/html-button-has-type rule

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 0fd8e2fa..6b9680a9 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -263,6 +263,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 |:--------|:------------|:---|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: |
 | [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements |  |
+| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute |  |
 | [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  |
diff --git a/docs/rules/html-button-has-type.md b/docs/rules/html-button-has-type.md
new file mode 100644
index 00000000..580e6fd4
--- /dev/null
+++ b/docs/rules/html-button-has-type.md
@@ -0,0 +1,64 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/html-button-has-type
+description: disallow usage of button without an explicit type attribute
+---
+# vue/html-button-has-type
+
+> disallow usage of button without an explicit type attribute
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :gear: This rule is included in `"plugin:vue/recommended"`.
+
+Forgetting the type attribute on a button defaults it to being a submit type. 
+This is nearly never what is intended, especially in your average one-page application.
+
+## :book: Rule Details
+
+This rule aims to warn if no type or an invalid type is used on a button type attribute.
+
+<eslint-code-block :rules="{'vue/html-button-has-type': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <button type="button">Hello World</button>
+  <button type="submit">Hello World</button>
+  <button type="reset">Hello World</button>
+
+  <!-- ✗ BAD -->
+  <button>Hello World</button>
+  <button type="">Hello World</button>
+  <button type="foo">Hello World</button>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/html-button-has-type": ["error", {
+    "button": true,
+    "submit": true,
+    "reset": true
+  }]
+}
+```
+
+- `button` ... `<button type="button"></button>`
+    - `true` (default) ... allow value `button`.
+    - `false"` ... disallow value `button`.
+- `sumbit` ... `<button type="sumbit"></button>`
+    - `true` (default) ... allow value `submit`.
+    - `false"` ... disallow value `submit`.
+- `reset` ... `<button type="reset"></button>`
+    - `true` (default) ... allow value `reset`.
+    - `false"` ... disallow value `reset`.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/html-button-has-type.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/html-button-has-type.js)
diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js
index 61781f75..c425c60e 100644
--- a/lib/configs/recommended.js
+++ b/lib/configs/recommended.js
@@ -8,6 +8,7 @@ module.exports = {
   rules: {
     'vue/attributes-order': 'warn',
     'vue/component-tags-order': 'warn',
+    'vue/html-button-has-type': 'warn',
     'vue/no-lone-template': 'warn',
     'vue/no-multiple-slot-args': 'warn',
     'vue/no-v-html': 'warn',
diff --git a/lib/index.js b/lib/index.js
index 2d600117..cc18e305 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -29,6 +29,7 @@ module.exports = {
     eqeqeq: require('./rules/eqeqeq'),
     'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
     'func-call-spacing': require('./rules/func-call-spacing'),
+    'html-button-has-type': require('./rules/html-button-has-type'),
     'html-closing-bracket-newline': require('./rules/html-closing-bracket-newline'),
     'html-closing-bracket-spacing': require('./rules/html-closing-bracket-spacing'),
     'html-comment-content-newline': require('./rules/html-comment-content-newline'),
diff --git a/lib/rules/html-button-has-type.js b/lib/rules/html-button-has-type.js
new file mode 100644
index 00000000..fdf06b21
--- /dev/null
+++ b/lib/rules/html-button-has-type.js
@@ -0,0 +1,102 @@
+/**
+ * @fileoverview Disallow usage of button without an explicit type attribute
+ * @author Jonathan Santerre <jonathan@santerre.dev>
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const optionDefaults = {
+  button: true,
+  submit: true,
+  reset: true
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow usage of button without an explicit type attribute',
+      category: 'recommended',
+      url: 'https://eslint.vuejs.org/rules/html-button-has-type.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          button: {
+            default: optionDefaults.button,
+            type: 'boolean'
+          },
+          submit: {
+            default: optionDefaults.submit,
+            type: 'boolean'
+          },
+          reset: {
+            default: optionDefaults.reset,
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      missingTypeAttribute: 'Missing an explicit type attribute for button.',
+      invalidTypeAttribute: '{{value}} is an invalid value for button type attribute.',
+      forbiddenTypeAttribute: '{{value}} is a forbidden value for button type attribute.',
+      emptyTypeAttribute: 'A value must be set for button type attribute.'
+    }
+  },
+
+  create: function (context) {
+    const configuration = Object.assign({}, optionDefaults, context.options[0])
+
+    function report (node, messageId, data) {
+      context.report({
+        node,
+        messageId,
+        data
+      })
+    }
+
+    function validateAttribute (attribute) {
+      const value = attribute.value
+      const strValue = value.value
+      if (strValue === '') {
+        report(value, 'emptyTypeAttribute')
+      } else if (!(strValue in configuration)) {
+        report(value, 'invalidTypeAttribute', { value: strValue })
+      } else if (!configuration[strValue]) {
+        report(value, 'forbiddenTypeAttribute', { value: strValue })
+      }
+    }
+
+    function validateDirective (directive) {
+      const value = directive.value
+      if (!value.expression) {
+        report(value, 'emptyTypeAttribute')
+      }
+    }
+
+    return utils.defineTemplateBodyVisitor(context, {
+      "VElement[name='button']" (node) {
+        if (utils.hasAttribute(node, 'type')) {
+          validateAttribute(utils.getAttribute(node, 'type'))
+        } else if (utils.hasDirective(node, 'bind', 'type')) {
+          validateDirective(utils.getDirective(node, 'bind', 'type'))
+        } else {
+          report(node, 'missingTypeAttribute')
+        }
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/html-button-has-type.js b/tests/lib/rules/html-button-has-type.js
new file mode 100644
index 00000000..4fd40d2a
--- /dev/null
+++ b/tests/lib/rules/html-button-has-type.js
@@ -0,0 +1,170 @@
+/**
+ * @fileoverview Prevent usage of button without an explicit type attribute
+ * @author Jonathan Santerre
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+var rule = require('../../../lib/rules/html-button-has-type')
+
+var RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+var ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+ruleTester.run('html-button-has-type', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: '<template><button type="button">Hello World</button></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button type="submit">Hello World</button></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button type="reset">Hello World</button></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><slot><button type="button">Hello World</button></slot></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: `<template>
+                <button type="button">Hello World</button>
+                <button type="submit">Hello World</button>
+                <button type="reset">Hello World</button>
+             </template>`
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><button :type="buttonType">Hello World</button></template>`
+    },
+    {
+      filename: 'test.vue',
+      code: ''
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><button>Hello World</button></template>',
+      errors: [{
+        message: 'Missing an explicit type attribute for button.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button type="">Hello World</button></template>',
+      errors: [{
+        message: 'A value must be set for button type attribute.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button type="foo">Hello World</button></template>',
+      errors: [{
+        message: 'foo is an invalid value for button type attribute.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      options: [{ button: false }],
+      code: '<template><button type="button">Hello World</button></template>',
+      errors: [{
+        message: 'button is a forbidden value for button type attribute.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      options: [{ submit: false }],
+      code: '<template><button type="submit">Hello World</button></template>',
+      errors: [{
+        message: 'submit is a forbidden value for button type attribute.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      options: [{ reset: false }],
+      code: '<template><button type="reset">Hello World</button></template>',
+      errors: [{
+        message: 'reset is a forbidden value for button type attribute.'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      options: [{ button: false, submit: false, reset: false }],
+      code: `<template>
+                <button type="button">Hello World</button>
+                <button type="submit">Hello World</button>
+                <button type="reset">Hello World</button>
+             </template>`,
+      errors: [
+        {
+          message: 'button is a forbidden value for button type attribute.'
+        },
+        {
+          message: 'submit is a forbidden value for button type attribute.'
+        },
+        {
+          message: 'reset is a forbidden value for button type attribute.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      options: [{ button: true, submit: true, reset: false }],
+      code: `<template>
+                <button type="button">Hello World</button>
+                <button type="submit">Hello World</button>
+                <button type="reset">Hello World</button>
+                <button type="">Hello World</button>
+                <button type="foo">Hello World</button>
+             </template>`,
+      errors: [
+        {
+          message: 'reset is a forbidden value for button type attribute.'
+        },
+        {
+          message: 'A value must be set for button type attribute.'
+        },
+        {
+          message: 'foo is an invalid value for button type attribute.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button>Hello World</button><button>Hello World</button></template>',
+      errors: [
+        {
+          message: 'Missing an explicit type attribute for button.'
+        },
+        {
+          message: 'Missing an explicit type attribute for button.'
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><button :type="">Hello World</button></template>`,
+      errors: [
+        {
+          message: 'A value must be set for button type attribute.'
+        }
+      ]
+    }
+  ]
+})