Skip to content

Commit

Permalink
Merge pull request #1525 from storybooks/jsx-no-typeless-button
Browse files Browse the repository at this point in the history
Add a rule enforcing explicit "type" attributes for buttons
  • Loading branch information
ljharb committed Nov 11, 2017
2 parents e7e2940 + fdacc74 commit 8795fde
Show file tree
Hide file tree
Showing 4 changed files with 269 additions and 0 deletions.
58 changes: 58 additions & 0 deletions 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
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -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'),
Expand Down
121 changes: 121 additions & 0 deletions 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);
}
};
}
};
89 changes: 89 additions & 0 deletions 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'
}]
}
]
});

0 comments on commit 8795fde

Please sign in to comment.