diff --git a/docs/rules/button-has-type.md b/docs/rules/button-has-type.md new file mode 100644 index 0000000000..32ae8dcd3e --- /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 = +var Hello = + +var Hello = React.createElement('button', {}, 'Hello') +var Hello = React.createElement('button', {type: 'foo'}, 'Hello') +``` + +The following patterns are **not** considered errors: + +```jsx +var Hello = Hello +var Hello = Hello +var Hello = +var Hello = +var Hello = + +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": [, { + "button": , + "submit": , + "reset": +}] +... +``` + +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 = + +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 a798443eb4..6c1a933ac9 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 0000000000..e720bcd040 --- /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); + } + }; + } +}; diff --git a/tests/lib/rules/button-has-type.js b/tests/lib/rules/button-has-type.js new file mode 100644 index 0000000000..151ffa9a2e --- /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: ''}, + {code: ''}, + {code: '