Repository: vuejs/eslint-plugin-vue
Pull request #1432 (2021-02-05 00:48:25)

Change vue/html-button-has-type rule
- Enable type checking
- Format
- Exclude from recommended category
- Exclude `Button` component from check


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 6b9680a9..d00e5478 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -263,7 +263,6 @@ 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 |  |
@@ -290,6 +289,7 @@ For example:
 | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: |
 | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
 | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name |  |
+| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute |  |
 | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
 | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: |
diff --git a/docs/rules/html-button-has-type.md b/docs/rules/html-button-has-type.md
index 580e6fd4..af81eb9e 100644
--- a/docs/rules/html-button-has-type.md
+++ b/docs/rules/html-button-has-type.md
@@ -9,9 +9,8 @@ description: disallow usage of button without an explicit type attribute
 > 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. 
+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
@@ -49,14 +48,14 @@ This rule aims to warn if no type or an invalid type is used on a button type at
 ```
 
 - `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`.
+  - `true` (default) ... allow value `button`.
+  - `false"` ... disallow value `button`.
+- `submit` ... `<button type="submit"></button>`
+  - `true` (default) ... allow value `submit`.
+  - `false"` ... disallow value `submit`.
 - `reset` ... `<button type="reset"></button>`
-    - `true` (default) ... allow value `reset`.
-    - `false"` ... disallow value `reset`.
+  - `true` (default) ... allow value `reset`.
+  - `false"` ... disallow value `reset`.
 
 ## :mag: Implementation
 
diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js
index c425c60e..61781f75 100644
--- a/lib/configs/recommended.js
+++ b/lib/configs/recommended.js
@@ -8,7 +8,6 @@ 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/rules/html-button-has-type.js b/lib/rules/html-button-has-type.js
index fdf06b21..4ef7d2fe 100644
--- a/lib/rules/html-button-has-type.js
+++ b/lib/rules/html-button-has-type.js
@@ -24,8 +24,9 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow usage of button without an explicit type attribute',
-      category: 'recommended',
+      description:
+        'disallow usage of button without an explicit type attribute',
+      categories: null,
       url: 'https://eslint.vuejs.org/rules/html-button-has-type.html'
     },
     fixable: null,
@@ -51,16 +52,42 @@ module.exports = {
     ],
     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.',
+      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) {
+  /**
+   * @param {RuleContext} context - The rule context.
+   * @returns {RuleListener} AST event handlers.
+   */
+  create(context) {
+    /**
+     * @typedef {object} Configuration
+     * @property {boolean} button
+     * @property {boolean} submit
+     * @property {boolean} reset
+     */
+    /** @type {Configuration} */
     const configuration = Object.assign({}, optionDefaults, context.options[0])
 
-    function report (node, messageId, data) {
+    /**
+     *
+     * @param {string} type
+     * @returns {type is 'button' | 'submit' | 'reset'}
+     */
+    function isButtonType(type) {
+      return type === 'button' || type === 'submit' || type === 'reset'
+    }
+    /**
+     * @param {ASTNode} node
+     * @param {string} messageId
+     * @param {any} [data]
+     */
+    function report(node, messageId, data) {
       context.report({
         node,
         messageId,
@@ -68,34 +95,51 @@ module.exports = {
       })
     }
 
-    function validateAttribute (attribute) {
+    /**
+     * @param {VAttribute} attribute
+     */
+    function validateAttribute(attribute) {
       const value = attribute.value
+      if (!value || !value.value) {
+        report(value || attribute, 'emptyTypeAttribute')
+        return
+      }
+
       const strValue = value.value
-      if (strValue === '') {
-        report(value, 'emptyTypeAttribute')
-      } else if (!(strValue in configuration)) {
+      if (!isButtonType(strValue)) {
         report(value, 'invalidTypeAttribute', { value: strValue })
       } else if (!configuration[strValue]) {
         report(value, 'forbiddenTypeAttribute', { value: strValue })
       }
     }
 
-    function validateDirective (directive) {
+    /**
+     * @param {VDirective} directive
+     */
+    function validateDirective(directive) {
       const value = directive.value
-      if (!value.expression) {
-        report(value, 'emptyTypeAttribute')
+      if (!value || !value.expression) {
+        report(value || directive, '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')
+      /**
+       * @param {VElement} node
+       */
+      "VElement[rawName='button']"(node) {
+        const typeAttr = utils.getAttribute(node, 'type')
+        if (typeAttr) {
+          validateAttribute(typeAttr)
+          return
         }
+        const typeDir = utils.getDirective(node, 'bind', 'type')
+        if (typeDir) {
+          validateDirective(typeDir)
+          return
+        }
+
+        report(node.startTag, 'missingTypeAttribute')
       }
     })
   }


Test patch:
diff --git a/tests/lib/rules/html-button-has-type.js b/tests/lib/rules/html-button-has-type.js
index 4fd40d2a..d6f40487 100644
--- a/tests/lib/rules/html-button-has-type.js
+++ b/tests/lib/rules/html-button-has-type.js
@@ -8,20 +8,19 @@
 // Requirements
 // ------------------------------------------------------------------------------
 
-var rule = require('../../../lib/rules/html-button-has-type')
+const rule = require('../../../lib/rules/html-button-has-type')
 
-var RuleTester = require('eslint').RuleTester
+const RuleTester = require('eslint').RuleTester
 
 // ------------------------------------------------------------------------------
 // Tests
 // ------------------------------------------------------------------------------
 
-var ruleTester = new RuleTester({
+const ruleTester = new RuleTester({
   parser: require.resolve('vue-eslint-parser'),
   parserOptions: { ecmaVersion: 2015 }
 })
 ruleTester.run('html-button-has-type', rule, {
-
   valid: [
     {
       filename: 'test.vue',
@@ -37,7 +36,8 @@ ruleTester.run('html-button-has-type', rule, {
     },
     {
       filename: 'test.vue',
-      code: '<template><slot><button type="button">Hello World</button></slot></template>'
+      code:
+        '<template><slot><button type="button">Hello World</button></slot></template>'
     },
     {
       filename: 'test.vue',
@@ -54,6 +54,10 @@ ruleTester.run('html-button-has-type', rule, {
     {
       filename: 'test.vue',
       code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><Button>Hello World</Button></template>`
     }
   ],
 
@@ -61,47 +65,75 @@ ruleTester.run('html-button-has-type', rule, {
     {
       filename: 'test.vue',
       code: '<template><button>Hello World</button></template>',
-      errors: [{
-        message: 'Missing an explicit type attribute for button.'
-      }]
+      errors: [
+        {
+          message: 'Missing an explicit type attribute for button.',
+          column: 11
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: '<template><button type="">Hello World</button></template>',
-      errors: [{
-        message: 'A value must be set for button type attribute.'
-      }]
+      errors: [
+        {
+          message: 'A value must be set for button type attribute.',
+          column: 24
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><button type>Hello World</button></template>',
+      errors: [
+        {
+          message: 'A value must be set for button type attribute.',
+          column: 19
+        }
+      ]
     },
     {
       filename: 'test.vue',
       code: '<template><button type="foo">Hello World</button></template>',
-      errors: [{
-        message: 'foo is an invalid value for button type attribute.'
-      }]
+      errors: [
+        {
+          message: 'foo is an invalid value for button type attribute.',
+          column: 24
+        }
+      ]
     },
     {
       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.'
-      }]
+      errors: [
+        {
+          message: 'button is a forbidden value for button type attribute.',
+          column: 24
+        }
+      ]
     },
     {
       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.'
-      }]
+      errors: [
+        {
+          message: 'submit is a forbidden value for button type attribute.',
+          column: 24
+        }
+      ]
     },
     {
       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.'
-      }]
+      errors: [
+        {
+          message: 'reset is a forbidden value for button type attribute.',
+          column: 24
+        }
+      ]
     },
     {
       filename: 'test.vue',
@@ -113,13 +145,19 @@ ruleTester.run('html-button-has-type', rule, {
              </template>`,
       errors: [
         {
-          message: 'button is a forbidden value for button type attribute.'
+          message: 'button is a forbidden value for button type attribute.',
+          line: 2,
+          column: 30
         },
         {
-          message: 'submit is a forbidden value for button type attribute.'
+          message: 'submit is a forbidden value for button type attribute.',
+          line: 3,
+          column: 30
         },
         {
-          message: 'reset is a forbidden value for button type attribute.'
+          message: 'reset is a forbidden value for button type attribute.',
+          line: 4,
+          column: 30
         }
       ]
     },
@@ -135,25 +173,34 @@ ruleTester.run('html-button-has-type', rule, {
              </template>`,
       errors: [
         {
-          message: 'reset is a forbidden value for button type attribute.'
+          message: 'reset is a forbidden value for button type attribute.',
+          line: 4,
+          column: 30
         },
         {
-          message: 'A value must be set for button type attribute.'
+          message: 'A value must be set for button type attribute.',
+          line: 5,
+          column: 30
         },
         {
-          message: 'foo is an invalid value for button type attribute.'
+          message: 'foo is an invalid value for button type attribute.',
+          line: 6,
+          column: 30
         }
       ]
     },
     {
       filename: 'test.vue',
-      code: '<template><button>Hello World</button><button>Hello World</button></template>',
+      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.',
+          column: 11
         },
         {
-          message: 'Missing an explicit type attribute for button.'
+          message: 'Missing an explicit type attribute for button.',
+          column: 39
         }
       ]
     },
@@ -162,7 +209,18 @@ ruleTester.run('html-button-has-type', rule, {
       code: `<template><button :type="">Hello World</button></template>`,
       errors: [
         {
-          message: 'A value must be set for button type attribute.'
+          message: 'A value must be set for button type attribute.',
+          column: 25
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><button v-bind:type>Hello World</button></template>`,
+      errors: [
+        {
+          message: 'A value must be set for button type attribute.',
+          column: 19
         }
       ]
     }