Skip to content

Commit 8795fde

Browse files
authored
Merge pull request jsx-eslint#1525 from storybooks/jsx-no-typeless-button
Add a rule enforcing explicit "type" attributes for buttons
2 parents e7e2940 + fdacc74 commit 8795fde

File tree

4 files changed

+269
-0
lines changed

4 files changed

+269
-0
lines changed

docs/rules/button-has-type.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Prevent usage of `button` elements without an explicit `type` attribute (react/button-has-type)
2+
3+
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.
4+
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"`).
5+
6+
## Rule Details
7+
8+
The following patterns are considered errors:
9+
10+
```jsx
11+
var Hello = <button>Hello</button>
12+
var Hello = <button type="foo">Hello</button>
13+
14+
var Hello = React.createElement('button', {}, 'Hello')
15+
var Hello = React.createElement('button', {type: 'foo'}, 'Hello')
16+
```
17+
18+
The following patterns are **not** considered errors:
19+
20+
```jsx
21+
var Hello = <span>Hello</span>
22+
var Hello = <span type="foo">Hello</span>
23+
var Hello = <button type="button">Hello</button>
24+
var Hello = <button type="submit">Hello</button>
25+
var Hello = <button type="reset">Hello</button>
26+
27+
var Hello = React.createElement('span', {}, 'Hello')
28+
var Hello = React.createElement('span', {type: 'foo'}, 'Hello')
29+
var Hello = React.createElement('button', {type: 'button'}, 'Hello')
30+
var Hello = React.createElement('button', {type: 'submit'}, 'Hello')
31+
var Hello = React.createElement('button', {type: 'reset'}, 'Hello')
32+
```
33+
34+
## Rule Options
35+
36+
```js
37+
...
38+
"react/default-props-match-prop-types": [<enabled>, {
39+
"button": <boolean>,
40+
"submit": <boolean>,
41+
"reset": <boolean>
42+
}]
43+
...
44+
```
45+
46+
You can forbid particular type attribute values by passing `false` as corresponding option (by default all of them are `true`).
47+
48+
The following patterns are considered errors when using `"react/default-props-match-prop-types": ["error", {reset: false}]`:
49+
50+
```jsx
51+
var Hello = <button type="reset">Hello</button>
52+
53+
var Hello = React.createElement('button', {type: 'reset'}, 'Hello')
54+
```
55+
56+
## When Not To Use It
57+
58+
If you use only `"submit"` buttons, you can disable this rule

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const allRules = {
2929
'jsx-no-duplicate-props': require('./lib/rules/jsx-no-duplicate-props'),
3030
'jsx-no-literals': require('./lib/rules/jsx-no-literals'),
3131
'jsx-no-target-blank': require('./lib/rules/jsx-no-target-blank'),
32+
'button-has-type': require('./lib/rules/button-has-type'),
3233
'jsx-no-undef': require('./lib/rules/jsx-no-undef'),
3334
'jsx-curly-brace-presence': require('./lib/rules/jsx-curly-brace-presence'),
3435
'jsx-pascal-case': require('./lib/rules/jsx-pascal-case'),

lib/rules/button-has-type.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @fileoverview Forbid "button" element without an explicit "type" attribute
3+
* @author Filipp Riabchun
4+
*/
5+
'use strict';
6+
7+
const getProp = require('jsx-ast-utils/getProp');
8+
const getLiteralPropValue = require('jsx-ast-utils/getLiteralPropValue');
9+
10+
// ------------------------------------------------------------------------------
11+
// Helpers
12+
// ------------------------------------------------------------------------------
13+
14+
function isCreateElement(node) {
15+
return node.callee
16+
&& node.callee.type === 'MemberExpression'
17+
&& node.callee.property.name === 'createElement'
18+
&& node.arguments.length > 0;
19+
}
20+
21+
// ------------------------------------------------------------------------------
22+
// Rule Definition
23+
// ------------------------------------------------------------------------------
24+
25+
module.exports = {
26+
meta: {
27+
docs: {
28+
description: 'Forbid "button" element without an explicit "type" attribute',
29+
category: 'Possible Errors',
30+
recommended: false
31+
},
32+
schema: [{
33+
type: 'object',
34+
properties: {
35+
button: {
36+
default: true,
37+
type: 'boolean'
38+
},
39+
submit: {
40+
default: true,
41+
type: 'boolean'
42+
},
43+
reset: {
44+
default: true,
45+
type: 'boolean'
46+
}
47+
},
48+
additionalProperties: false
49+
}]
50+
},
51+
52+
create: function(context) {
53+
const configuration = Object.assign({
54+
button: true,
55+
submit: true,
56+
reset: true
57+
}, context.options[0]);
58+
59+
function reportMissing(node) {
60+
context.report({
61+
node: node,
62+
message: 'Missing an explicit type attribute for button'
63+
});
64+
}
65+
66+
function checkValue(node, value) {
67+
if (!(value in configuration)) {
68+
context.report({
69+
node: node,
70+
message: `"${value}" is an invalid value for button type attribute`
71+
});
72+
} else if (!configuration[value]) {
73+
context.report({
74+
node: node,
75+
message: `"${value}" is a forbidden value for button type attribute`
76+
});
77+
}
78+
}
79+
80+
return {
81+
JSXElement: function(node) {
82+
if (node.openingElement.name.name !== 'button') {
83+
return;
84+
}
85+
86+
const typeProp = getProp(node.openingElement.attributes, 'type');
87+
88+
if (!typeProp) {
89+
reportMissing(node);
90+
return;
91+
}
92+
93+
checkValue(node, getLiteralPropValue(typeProp));
94+
},
95+
CallExpression: function(node) {
96+
if (!isCreateElement(node)) {
97+
return;
98+
}
99+
100+
if (node.arguments[0].type !== 'Literal' || node.arguments[0].value !== 'button') {
101+
return;
102+
}
103+
104+
if (!node.arguments[1] || node.arguments[1].type !== 'ObjectExpression') {
105+
reportMissing(node);
106+
return;
107+
}
108+
109+
const props = node.arguments[1].properties;
110+
const typeProp = props.find(prop => prop.key && prop.key.name === 'type');
111+
112+
if (!typeProp || typeProp.value.type !== 'Literal') {
113+
reportMissing(node);
114+
return;
115+
}
116+
117+
checkValue(node, typeProp.value.value);
118+
}
119+
};
120+
}
121+
};

tests/lib/rules/button-has-type.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* @fileoverview Forbid "button" element without an explicit "type" attribute
3+
* @author Filipp Riabchun
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const rule = require('../../../lib/rules/button-has-type');
12+
const RuleTester = require('eslint').RuleTester;
13+
14+
const parserOptions = {
15+
ecmaVersion: 8,
16+
sourceType: 'module',
17+
ecmaFeatures: {
18+
experimentalObjectRestSpread: true,
19+
jsx: true
20+
}
21+
};
22+
23+
// ------------------------------------------------------------------------------
24+
// Tests
25+
// ------------------------------------------------------------------------------
26+
27+
const ruleTester = new RuleTester({parserOptions});
28+
ruleTester.run('button-has-type', rule, {
29+
valid: [
30+
{code: '<span/>'},
31+
{code: '<span type="foo"/>'},
32+
{code: '<button type="button"/>'},
33+
{code: '<button type="submit"/>'},
34+
{code: '<button type="reset"/>'},
35+
{
36+
code: '<button type="button"/>',
37+
options: [{reset: false}]
38+
},
39+
{code: 'React.createElement("span")'},
40+
{code: 'React.createElement("span", {type: "foo"})'},
41+
{code: 'React.createElement("button", {type: "button"})'},
42+
{code: 'React.createElement("button", {type: "submit"})'},
43+
{code: 'React.createElement("button", {type: "reset"})'},
44+
{
45+
code: 'React.createElement("button", {type: "button"})',
46+
options: [{reset: false}]
47+
}
48+
],
49+
invalid: [
50+
{
51+
code: '<button/>',
52+
errors: [{
53+
message: 'Missing an explicit type attribute for button'
54+
}]
55+
},
56+
{
57+
code: '<button type="foo"/>',
58+
errors: [{
59+
message: '"foo" is an invalid value for button type attribute'
60+
}]
61+
},
62+
{
63+
code: '<button type="reset"/>',
64+
options: [{reset: false}],
65+
errors: [{
66+
message: '"reset" is a forbidden value for button type attribute'
67+
}]
68+
},
69+
{
70+
code: 'React.createElement("button")',
71+
errors: [{
72+
message: 'Missing an explicit type attribute for button'
73+
}]
74+
},
75+
{
76+
code: 'React.createElement("button", {type: "foo"})',
77+
errors: [{
78+
message: '"foo" is an invalid value for button type attribute'
79+
}]
80+
},
81+
{
82+
code: 'React.createElement("button", {type: "reset"})',
83+
options: [{reset: false}],
84+
errors: [{
85+
message: '"reset" is a forbidden value for button type attribute'
86+
}]
87+
}
88+
]
89+
});

0 commit comments

Comments
 (0)