Skip to content

Commit 831a97c

Browse files
committed
[New] forbid-dom-props: Add valueRegex option for forbidden props
Discussion: #3876
1 parent efc021f commit 831a97c

File tree

4 files changed

+239
-9
lines changed

4 files changed

+239
-9
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
1111
* [`no-unknown-property`]: support `onBeforeToggle`, `popoverTarget`, `popoverTargetAction` attributes ([#3865][] @acusti)
1212
* [types] fix types of flat configs ([#3874][] @ljharb)
1313

14+
### Added
15+
* [`forbid-dom-props`]: Add `valueRegex` option for forbidden props ([#3876][] @makxca)
16+
17+
[#3876]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3876
1418
[#3874]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3874
1519
[#3865]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3865
1620

Diff for: docs/rules/forbid-dom-props.md

+47-2
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,63 @@ Examples of **correct** code for this rule:
4444

4545
### `forbid`
4646

47-
An array of strings, with the names of props that are forbidden. The default value of this option `[]`.
47+
An array of strings, with the names of props that are forbidden. The default value of this option is `[]`.
4848
Each array element can either be a string with the property name or object specifying the property name, an optional
49-
custom message, and a DOM nodes disallowed list (e.g. `<div />`):
49+
custom message, DOM nodes disallowed list (e.g. `<div />`) and a specific regular expression for prohibited prop values:
5050

5151
```js
5252
{
5353
"propName": "someProp",
5454
"disallowedFor": ["DOMNode", "AnotherDOMNode"],
55+
"valueRegex": "^someValue$",
5556
"message": "Avoid using someProp"
5657
}
5758
```
5859

60+
Example of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`.
61+
62+
```jsx
63+
const First = (props) => (
64+
<span someProp="bar" />
65+
);
66+
```
67+
68+
Example of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`.
69+
70+
```jsx
71+
const First = (props) => (
72+
<div someProp="bar" />
73+
);
74+
```
75+
76+
Examples of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', valueRegex: '^someValue$' }] }`.
77+
78+
```jsx
79+
const First = (props) => (
80+
<div someProp="someValue" />
81+
);
82+
```
83+
84+
```jsx
85+
const First = (props) => (
86+
<span someProp="someValue" />
87+
);
88+
```
89+
90+
Examples of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', valueRegex: '^someValue$' }] }`.
91+
92+
```jsx
93+
const First = (props) => (
94+
<Foo someProp="someValue" />
95+
);
96+
```
97+
98+
```jsx
99+
const First = (props) => (
100+
<div someProp="value" />
101+
);
102+
```
103+
59104
### Related rules
60105

61106
- [forbid-component-props](./forbid-component-props.md)

Diff for: lib/rules/forbid-dom-props.js

+26-7
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,33 @@ const DEFAULTS = [];
1818
// Rule Definition
1919
// ------------------------------------------------------------------------------
2020

21+
/** @typedef {{ disallowList: null | string[]; message: null | string; valueRegex: null | RegExp }} ForbidMapType */
2122
/**
22-
* @param {Map<string, object>} forbidMap // { disallowList: null | string[], message: null | string }
23+
* @param {Map<string, ForbidMapType>} forbidMap
2324
* @param {string} prop
25+
* @param {string} propValue
2426
* @param {string} tagName
2527
* @returns {boolean}
2628
*/
27-
function isForbidden(forbidMap, prop, tagName) {
29+
function isForbidden(forbidMap, prop, propValue, tagName) {
2830
const options = forbidMap.get(prop);
29-
return options && (
30-
typeof tagName === 'undefined'
31-
|| !options.disallowList
31+
32+
if (!options) {
33+
return false;
34+
}
35+
36+
return (
37+
!options.disallowList
3238
|| options.disallowList.indexOf(tagName) !== -1
39+
) && (
40+
!options.valueRegex
41+
|| options.valueRegex.test(propValue)
3342
);
3443
}
3544

3645
const messages = {
3746
propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes',
47+
propIsForbiddenWithValue: 'Prop "{{prop}}" with value "{{propValue}}" is forbidden on DOM Nodes',
3848
};
3949

4050
/** @type {import('eslint').Rule.RuleModule} */
@@ -70,6 +80,9 @@ module.exports = {
7080
type: 'string',
7181
},
7282
},
83+
valueRegex: {
84+
type: 'string',
85+
},
7386
message: {
7487
type: 'string',
7588
},
@@ -91,6 +104,7 @@ module.exports = {
91104
return [propName, {
92105
disallowList: typeof value === 'string' ? null : (value.disallowedFor || null),
93106
message: typeof value === 'string' ? null : value.message,
107+
valueRegex: typeof value.valueRegex === 'string' ? new RegExp(value.valueRegex) : null,
94108
}];
95109
}));
96110

@@ -103,17 +117,22 @@ module.exports = {
103117
}
104118

105119
const prop = node.name.name;
120+
const propValue = node.value.value;
106121

107-
if (!isForbidden(forbid, prop, tag)) {
122+
if (!isForbidden(forbid, prop, propValue, tag)) {
108123
return;
109124
}
110125

111126
const customMessage = forbid.get(prop).message;
127+
const isRegexSpecified = forbid.get(prop).valueRegex !== null;
128+
const message = customMessage || (isRegexSpecified && messages.propIsForbiddenWithValue) || messages.propIsForbidden;
129+
const messageId = !customMessage && ((isRegexSpecified && 'propIsForbiddenWithValue') || 'propIsForbidden');
112130

113-
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
131+
report(context, message, messageId, {
114132
node,
115133
data: {
116134
prop,
135+
propValue,
117136
},
118137
});
119138
},

Diff for: tests/lib/rules/forbid-dom-props.js

+162
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,58 @@ ruleTester.run('forbid-dom-props', rule, {
112112
},
113113
],
114114
},
115+
{
116+
code: `
117+
const First = (props) => (
118+
<Foo someProp="someValue" />
119+
);
120+
`,
121+
options: [
122+
{
123+
forbid: [
124+
{
125+
propName: 'someProp',
126+
valueRegex: '^someValue$',
127+
},
128+
],
129+
},
130+
],
131+
},
132+
{
133+
code: `
134+
const First = (props) => (
135+
<div someProp="value" />
136+
);
137+
`,
138+
options: [
139+
{
140+
forbid: [
141+
{
142+
propName: 'someProp',
143+
valueRegex: '^someValue$',
144+
},
145+
],
146+
},
147+
],
148+
},
149+
{
150+
code: `
151+
const First = (props) => (
152+
<div someProp="someValue" />
153+
);
154+
`,
155+
options: [
156+
{
157+
forbid: [
158+
{
159+
propName: 'someProp',
160+
valueRegex: '^someValue$',
161+
disallowedFor: ['span'],
162+
},
163+
],
164+
},
165+
],
166+
},
115167
]),
116168

117169
invalid: parsers.all([
@@ -191,6 +243,58 @@ ruleTester.run('forbid-dom-props', rule, {
191243
},
192244
],
193245
},
246+
{
247+
code: `
248+
const First = (props) => (
249+
<span otherProp="bar" />
250+
);
251+
`,
252+
options: [
253+
{
254+
forbid: [
255+
{
256+
propName: 'otherProp',
257+
disallowedFor: ['span'],
258+
},
259+
],
260+
},
261+
],
262+
errors: [
263+
{
264+
messageId: 'propIsForbidden',
265+
data: { prop: 'otherProp' },
266+
line: 3,
267+
column: 17,
268+
type: 'JSXAttribute',
269+
},
270+
],
271+
},
272+
{
273+
code: `
274+
const First = (props) => (
275+
<div someProp="someValue" />
276+
);
277+
`,
278+
options: [
279+
{
280+
forbid: [
281+
{
282+
propName: 'someProp',
283+
valueRegex: '^someValue$',
284+
},
285+
],
286+
},
287+
],
288+
errors: [
289+
{
290+
messageId: 'propIsForbiddenWithValue',
291+
data: { prop: 'someProp', propValue: 'someValue' },
292+
line: 3,
293+
column: 16,
294+
type: 'JSXAttribute',
295+
},
296+
],
297+
},
194298
{
195299
code: `
196300
const First = (props) => (
@@ -324,5 +428,63 @@ ruleTester.run('forbid-dom-props', rule, {
324428
},
325429
],
326430
},
431+
{
432+
code: `
433+
const First = (props) => (
434+
<div className="foo">
435+
<input className="boo" />
436+
<span className="foobar">Foobar</span>
437+
<div otherProp="bar" />
438+
<p thirdProp="bar" />
439+
<div thirdProp="baz" />
440+
<p thirdProp="baz" />
441+
</div>
442+
);
443+
`,
444+
options: [
445+
{
446+
forbid: [
447+
{
448+
propName: 'className',
449+
disallowedFor: ['div', 'span'],
450+
message: 'Please use class instead of ClassName',
451+
},
452+
{ propName: 'otherProp', message: 'Avoid using otherProp' },
453+
{
454+
propName: 'thirdProp',
455+
disallowedFor: ['p'],
456+
valueRegex: '^baz$',
457+
message: 'Do not use thirdProp with value baz on p',
458+
},
459+
],
460+
},
461+
],
462+
errors: [
463+
{
464+
message: 'Please use class instead of ClassName',
465+
line: 3,
466+
column: 16,
467+
type: 'JSXAttribute',
468+
},
469+
{
470+
message: 'Please use class instead of ClassName',
471+
line: 5,
472+
column: 19,
473+
type: 'JSXAttribute',
474+
},
475+
{
476+
message: 'Avoid using otherProp',
477+
line: 6,
478+
column: 18,
479+
type: 'JSXAttribute',
480+
},
481+
{
482+
message: 'Do not use thirdProp with value baz on p',
483+
line: 9,
484+
column: 16,
485+
type: 'JSXAttribute',
486+
},
487+
],
488+
},
327489
]),
328490
});

0 commit comments

Comments
 (0)