Skip to content

Commit 8af0ff1

Browse files
committed
Add new rule: forbid-elements
May specify a list of forbidden elements and their desired replacement elements.
1 parent a1b8d45 commit 8af0ff1

File tree

5 files changed

+325
-0
lines changed

5 files changed

+325
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Finally, enable all of the rules that you would like to use. Use [our preset](#
8282

8383
* [react/display-name](docs/rules/display-name.md): Prevent missing `displayName` in a React component definition
8484
* [react/forbid-component-props](docs/rules/forbid-component-props.md): Forbid certain props on Components
85+
* [react/forbid-elements](docs/rules/forbid-elements.md): Forbid certain elements
8586
* [react/forbid-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes
8687
* [react/no-children-prop](docs/rules/no-children-prop.md): Prevent passing children as props
8788
* [react/no-danger](docs/rules/no-danger.md): Prevent usage of dangerous JSX properties

docs/rules/forbid-elements.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Forbid certain elements (forbid-elements)
2+
3+
You may want to forbid usage of certain elements in favor of others, (e.g. forbid all `<div />` and use `<Box />` instead). This rule allows you to configure a list of forbidden elements and to specify their desired replacements.
4+
5+
## Rule Details
6+
7+
This rule checks all JSX components and verifies that no forbidden elements are used. When it detects a forbidden element, the error message will contain the specified elements that you should use instead. This rule is off by default. If on, no elements are forbidden by default.
8+
9+
## Rule Options
10+
11+
```js
12+
...
13+
"forbid-elements": [<enabled>, { "forbid": [<string|object>] }]
14+
...
15+
```
16+
17+
### `forbid`
18+
19+
An array of strings, objects, or a mixture of string and objects. If an item is a string, then an element matching the string will be forbidden and no replacement will be suggested. If an item is an object, then the keys will be the forbidden element and their values can be either a string or an array of strings of desired replacement elements.
20+
21+
The following patterns are considered warnings:
22+
23+
```jsx
24+
// [1, {forbid: ['button']}]
25+
<button />
26+
27+
// [1, {forbid: ['button', 'input']}]
28+
<div><button /><input /></div>
29+
30+
// [1, {forbid: [{button: 'Button']}]
31+
<button />
32+
33+
// [1, {forbid: [{div: ['Box', 'View']}, 'button']]
34+
<div><button /></div>
35+
```
36+
37+
## When not to use
38+
39+
If you don't want to forbid any elements.

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var rules = {
4444
'jsx-space-before-closing': require('./lib/rules/jsx-space-before-closing'),
4545
'no-direct-mutation-state': require('./lib/rules/no-direct-mutation-state'),
4646
'forbid-component-props': require('./lib/rules/forbid-component-props'),
47+
'forbid-elements': require('./lib/rules/forbid-elements'),
4748
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
4849
'prefer-es6-class': require('./lib/rules/prefer-es6-class'),
4950
'jsx-key': require('./lib/rules/jsx-key'),

lib/rules/forbid-elements.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/**
2+
* @fileoverview Forbid certain elements
3+
* @author Kenneth Chung
4+
*/
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Rule Definition
9+
// ------------------------------------------------------------------------------
10+
11+
module.exports = {
12+
meta: {
13+
docs: {
14+
description: 'Forbid certain elements',
15+
category: 'Best Practices',
16+
recommended: false
17+
},
18+
19+
schema: [{
20+
type: 'object',
21+
properties: {
22+
forbid: {
23+
type: 'array',
24+
items: {
25+
anyOf: [
26+
{type: 'string'},
27+
{
28+
type: 'object',
29+
additionalProperties: {
30+
anyOf: [
31+
{type: 'string'},
32+
{type: 'array', items: {type: 'string'}}
33+
]
34+
}
35+
}
36+
]
37+
}
38+
}
39+
},
40+
additionalProperties: false
41+
}]
42+
},
43+
44+
create: function(context) {
45+
var sourceCode = context.getSourceCode();
46+
var configuration = context.options[0] || {};
47+
var forbidConfiguration = configuration.forbid || [];
48+
var forbiddenElements = {};
49+
50+
function formatReplacements(replacements) {
51+
if (!replacements || replacements.length === 0) {
52+
return null;
53+
}
54+
55+
if (typeof replacements === 'string') {
56+
replacements = [replacements];
57+
}
58+
59+
replacements = replacements.map(function(replacement) {
60+
return '<' + replacement + '>';
61+
});
62+
63+
if (replacements.length === 1) {
64+
return replacements[0];
65+
}
66+
67+
if (replacements.length === 2) {
68+
return replacements.join(' or ');
69+
}
70+
71+
var last = replacements.pop();
72+
return replacements.join(', ') + ', or ' + last;
73+
}
74+
75+
function errorMessageForName(name) {
76+
var message = '<' + name + '> is forbidden';
77+
var replacements = forbiddenElements[name];
78+
79+
if (replacements) {
80+
message += ', use ' + replacements + ' instead';
81+
}
82+
83+
return message;
84+
}
85+
86+
forbidConfiguration.forEach(function(item) {
87+
if (typeof item === 'string') {
88+
forbiddenElements[item] = null;
89+
} else {
90+
Object.keys(item).forEach(function(key) {
91+
forbiddenElements[key] = formatReplacements(item[key]);
92+
});
93+
}
94+
});
95+
96+
return {
97+
JSXOpeningElement: function(node) {
98+
var name = sourceCode.getText(node.name);
99+
100+
if (forbiddenElements.hasOwnProperty(name)) {
101+
context.report({
102+
node: node,
103+
message: errorMessageForName(name)
104+
});
105+
}
106+
}
107+
};
108+
}
109+
};

tests/lib/rules/forbid-elements.js

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* @fileoverview Tests for forbid-elements
3+
*/
4+
'use strict';
5+
6+
// -----------------------------------------------------------------------------
7+
// Requirements
8+
// -----------------------------------------------------------------------------
9+
10+
var rule = require('../../../lib/rules/forbid-elements');
11+
var RuleTester = require('eslint').RuleTester;
12+
13+
var parserOptions = {
14+
ecmaVersion: 6,
15+
ecmaFeatures: {
16+
experimentalObjectRestSpread: true,
17+
jsx: true
18+
}
19+
};
20+
21+
require('babel-eslint');
22+
23+
// -----------------------------------------------------------------------------
24+
// Tests
25+
// -----------------------------------------------------------------------------
26+
27+
var ruleTester = new RuleTester();
28+
ruleTester.run('forbid-elements', rule, {
29+
valid: [
30+
{
31+
code: '<button />',
32+
options: [],
33+
parserOptions: parserOptions
34+
},
35+
{
36+
code: '<button />',
37+
options: [{forbid: []}],
38+
parserOptions: parserOptions
39+
},
40+
{
41+
code: '<button />',
42+
options: [{forbid: [{}]}],
43+
parserOptions: parserOptions
44+
}
45+
],
46+
47+
invalid: [
48+
{
49+
code: '<button />',
50+
options: [{forbid: ['button']}],
51+
parserOptions: parserOptions,
52+
errors: [{message: '<button> is forbidden'}]
53+
},
54+
{
55+
code: '<div><Modal /><button /></div>',
56+
options: [{forbid: ['button', 'Modal']}],
57+
parserOptions: parserOptions,
58+
errors: [
59+
{message: '<Modal> is forbidden'},
60+
{message: '<button> is forbidden'}
61+
]
62+
},
63+
{
64+
code: '<dotted.component />',
65+
options: [{forbid: ['dotted.component']}],
66+
parserOptions: parserOptions,
67+
errors: [
68+
{message: '<dotted.component> is forbidden'}
69+
]
70+
},
71+
{
72+
code: '<button />',
73+
options: [{forbid: [{button: 'Button'}]}],
74+
parserOptions: parserOptions,
75+
errors: [{message: '<button> is forbidden, use <Button> instead'}]
76+
},
77+
{
78+
code: '<dotted.Component />',
79+
options: [{forbid: [{'dotted.Component': 'Component'}]}],
80+
parserOptions: parserOptions,
81+
errors: [{message: '<dotted.Component> is forbidden, use <Component> instead'}]
82+
},
83+
{
84+
code: '<Component />',
85+
options: [{forbid: [{Component: 'dotted.Component'}]}],
86+
parserOptions: parserOptions,
87+
errors: [{message: '<Component> is forbidden, use <dotted.Component> instead'}]
88+
},
89+
{
90+
code: '<div><button><input /></button></div>',
91+
options: [{forbid: [{button: 'Button', input: 'Input'}]}],
92+
parserOptions: parserOptions,
93+
errors: [
94+
{message: '<button> is forbidden, use <Button> instead'},
95+
{message: '<input> is forbidden, use <Input> instead'}
96+
]
97+
},
98+
{
99+
code: '<div><button><input /></button></div>',
100+
options: [{forbid: [{button: 'Button'}, {input: 'Input'}]}],
101+
parserOptions: parserOptions,
102+
errors: [
103+
{message: '<button> is forbidden, use <Button> instead'},
104+
{message: '<input> is forbidden, use <Input> instead'}
105+
]
106+
},
107+
{
108+
code: '<div><button /><input /></div>',
109+
options: [{forbid: ['input', {button: 'Button'}]}],
110+
parserOptions: parserOptions,
111+
errors: [
112+
{message: '<button> is forbidden, use <Button> instead'},
113+
{message: '<input> is forbidden'}
114+
]
115+
},
116+
{
117+
code: '<div><button /><input /></div>',
118+
options: [{forbid: [{button: 'Button'}, 'input']}],
119+
parserOptions: parserOptions,
120+
errors: [
121+
{message: '<button> is forbidden, use <Button> instead'},
122+
{message: '<input> is forbidden'}
123+
]
124+
},
125+
{
126+
code: '<div><button /><input /></div>',
127+
options: [{forbid: [{button: 'Button'}, {input: 'Input'}, 'button']}],
128+
parserOptions: parserOptions,
129+
errors: [
130+
{message: '<button> is forbidden'},
131+
{message: '<input> is forbidden, use <Input> instead'}
132+
]
133+
},
134+
{
135+
code: '<button />',
136+
options: [{forbid: [{button: ''}]}],
137+
parserOptions: parserOptions,
138+
errors: [
139+
{message: '<button> is forbidden'}
140+
]
141+
},
142+
{
143+
code: '<div />',
144+
options: [{forbid: [{div: []}]}],
145+
parserOptions: parserOptions,
146+
errors: [
147+
{message: '<div> is forbidden'}
148+
]
149+
},
150+
{
151+
code: '<div />',
152+
options: [{forbid: [{div: ['Box']}]}],
153+
parserOptions: parserOptions,
154+
errors: [
155+
{message: '<div> is forbidden, use <Box> instead'}
156+
]
157+
},
158+
{
159+
code: '<div />',
160+
options: [{forbid: [{div: ['Box', 'View']}]}],
161+
parserOptions: parserOptions,
162+
errors: [
163+
{message: '<div> is forbidden, use <Box> or <View> instead'}
164+
]
165+
},
166+
{
167+
code: '<div />',
168+
options: [{forbid: [{div: ['Box', 'View', 'Table']}]}],
169+
parserOptions: parserOptions,
170+
errors: [
171+
{message: '<div> is forbidden, use <Box>, <View>, or <Table> instead'}
172+
]
173+
}
174+
]
175+
});

0 commit comments

Comments
 (0)