Repository: yannickcr/eslint-plugin-react
Pull request #1525 (2017-11-09 01:12:56)

Add a rule enforcing explicit "type" attributes for buttons
By default, `button` elements have `type="submit"`, which usually isn't what you actually want. This rule enforces the button type to be explicit

See also: https://github.com/evcohen/eslint-plugin-jsx-a11y/issues/186#issuecomment-283727911


Patch:
diff --git a/docs/rules/button-has-type.md b/docs/rules/button-has-type.md
new file mode 100644
index 00000000..32ae8dcd
--- /dev/null
+++ b/docs/rules/button-has-type.md
@@ -0,0 +1,58 @@
+# Prevent usage of `button` elements without an explicit `type` attribute (react/button-has-type)
+
+The default value of `type` attribute for `button` HTML element is `"submit"` which is often not the desired behavior and may lead to unexpected page reloads.
+This rules enforces an explicit `type` attribute for all the `button` elements and checks that its value is valid per spec (i.e., is one of `"button"`, `"submit"`, and `"reset"`).
+
+## Rule Details
+
+The following patterns are considered errors:
+
+```jsx
+var Hello = <button>Hello</button>
+var Hello = <button type="foo">Hello</button>
+
+var Hello = React.createElement('button', {}, 'Hello')
+var Hello = React.createElement('button', {type: 'foo'}, 'Hello')
+```
+
+The following patterns are **not** considered errors:
+
+```jsx
+var Hello = <span>Hello</span>
+var Hello = <span type="foo">Hello</span>
+var Hello = <button type="button">Hello</button>
+var Hello = <button type="submit">Hello</button>
+var Hello = <button type="reset">Hello</button>
+
+var Hello = React.createElement('span', {}, 'Hello')
+var Hello = React.createElement('span', {type: 'foo'}, 'Hello')
+var Hello = React.createElement('button', {type: 'button'}, 'Hello')
+var Hello = React.createElement('button', {type: 'submit'}, 'Hello')
+var Hello = React.createElement('button', {type: 'reset'}, 'Hello')
+```
+
+## Rule Options
+
+```js
+...
+"react/default-props-match-prop-types": [<enabled>, {
+  "button": <boolean>,
+  "submit": <boolean>,
+  "reset": <boolean>
+}]
+...
+```
+
+You can forbid particular type attribute values by passing `false` as corresponding option (by default all of them are `true`).
+
+The following patterns are considered errors when using `"react/default-props-match-prop-types": ["error", {reset: false}]`:
+
+```jsx
+var Hello = <button type="reset">Hello</button>
+
+var Hello = React.createElement('button', {type: 'reset'}, 'Hello')
+```
+
+## When Not To Use It
+
+If you use only `"submit"` buttons, you can disable this rule
diff --git a/index.js b/index.js
index a798443e..6c1a933a 100644
--- a/index.js
+++ b/index.js
@@ -29,6 +29,7 @@ const allRules = {
   'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
   'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
   'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
+  'button-has-type': require('./lib/rules/button-has-type'),
   'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
   'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
   'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),
diff --git a/lib/rules/button-has-type.js b/lib/rules/button-has-type.js
new file mode 100644
index 00000000..e720bcd0
--- /dev/null
+++ b/lib/rules/button-has-type.js
@@ -0,0 +1,121 @@
+/**
+ * @fileoverview Forbid "button" element without an explicit "type" attribute
+ * @author Filipp Riabchun
+ */
+'use strict';
+
+const getProp = require('jsx-ast-utils/getProp');
+const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+function isCreateElement(node) {
+  return node.callee
+    && node.callee.type === 'MemberExpression'
+    && node.callee.property.name === 'createElement'
+    && node.arguments.length > 0;
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Forbid "button" element without an explicit "type" attribute',
+      category: 'Possible Errors',
+      recommended: false
+    },
+    schema: [{
+      type: 'object',
+      properties: {
+        button: {
+          default: true,
+          type: 'boolean'
+        },
+        submit: {
+          default: true,
+          type: 'boolean'
+        },
+        reset: {
+          default: true,
+          type: 'boolean'
+        }
+      },
+      additionalProperties: false
+    }]
+  },
+
+  create: function(context) {
+    const configuration = Object.assign({
+      button: true,
+      submit: true,
+      reset: true
+    }, context.options[0]);
+
+    function reportMissing(node) {
+      context.report({
+        node: node,
+        message: 'Missing an explicit type attribute for button'
+      });
+    }
+
+    function checkValue(node, value) {
+      if (!(value in configuration)) {
+        context.report({
+          node: node,
+          message: `"${value}" is an invalid value for button type attribute`
+        });
+      } else if (!configuration[value]) {
+        context.report({
+          node: node,
+          message: `"${value}" is a forbidden value for button type attribute`
+        });
+      }
+    }
+
+    return {
+      JSXElement: function(node) {
+        if (node.openingElement.name.name !== 'button') {
+          return;
+        }
+
+        const typeProp = getProp(node.openingElement.attributes, 'type');
+
+        if (!typeProp) {
+          reportMissing(node);
+          return;
+        }
+
+        checkValue(node, getLiteralPropValue(typeProp));
+      },
+      CallExpression: function(node) {
+        if (!isCreateElement(node)) {
+          return;
+        }
+
+        if (node.arguments[0].type !== 'Literal' || node.arguments[0].value !== 'button') {
+          return;
+        }
+
+        if (!node.arguments[1] || node.arguments[1].type !== 'ObjectExpression') {
+          reportMissing(node);
+          return;
+        }
+
+        const props = node.arguments[1].properties;
+        const typeProp = props.find(prop => prop.key && prop.key.name === 'type');
+
+        if (!typeProp || typeProp.value.type !== 'Literal') {
+          reportMissing(node);
+          return;
+        }
+
+        checkValue(node, typeProp.value.value);
+      }
+    };
+  }
+};


Test patch:
diff --git a/tests/lib/rules/button-has-type.js b/tests/lib/rules/button-has-type.js
new file mode 100644
index 00000000..151ffa9a
--- /dev/null
+++ b/tests/lib/rules/button-has-type.js
@@ -0,0 +1,89 @@
+/**
+ * @fileoverview Forbid "button" element without an explicit "type" attribute
+ * @author Filipp Riabchun
+ */
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/button-has-type');
+const RuleTester = require('eslint').RuleTester;
+
+const parserOptions = {
+  ecmaVersion: 8,
+  sourceType: 'module',
+  ecmaFeatures: {
+    experimentalObjectRestSpread: true,
+    jsx: true
+  }
+};
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({parserOptions});
+ruleTester.run('button-has-type', rule, {
+  valid: [
+    {code: '<span/>'},
+    {code: '<span type="foo"/>'},
+    {code: '<button type="button"/>'},
+    {code: '<button type="submit"/>'},
+    {code: '<button type="reset"/>'},
+    {
+      code: '<button type="button"/>',
+      options: [{reset: false}]
+    },
+    {code: 'React.createElement("span")'},
+    {code: 'React.createElement("span", {type: "foo"})'},
+    {code: 'React.createElement("button", {type: "button"})'},
+    {code: 'React.createElement("button", {type: "submit"})'},
+    {code: 'React.createElement("button", {type: "reset"})'},
+    {
+      code: 'React.createElement("button", {type: "button"})',
+      options: [{reset: false}]
+    }
+  ],
+  invalid: [
+    {
+      code: '<button/>',
+      errors: [{
+        message: 'Missing an explicit type attribute for button'
+      }]
+    },
+    {
+      code: '<button type="foo"/>',
+      errors: [{
+        message: '"foo" is an invalid value for button type attribute'
+      }]
+    },
+    {
+      code: '<button type="reset"/>',
+      options: [{reset: false}],
+      errors: [{
+        message: '"reset" is a forbidden value for button type attribute'
+      }]
+    },
+    {
+      code: 'React.createElement("button")',
+      errors: [{
+        message: 'Missing an explicit type attribute for button'
+      }]
+    },
+    {
+      code: 'React.createElement("button", {type: "foo"})',
+      errors: [{
+        message: '"foo" is an invalid value for button type attribute'
+      }]
+    },
+    {
+      code: 'React.createElement("button", {type: "reset"})',
+      options: [{reset: false}],
+      errors: [{
+        message: '"reset" is a forbidden value for button type attribute'
+      }]
+    }
+  ]
+});