Skip to content

Commit 6ce58e5

Browse files
akulsr0ljharb
authored andcommitted
[New] forbid-component-props: add propNamePattern to allow / disallow prop name patterns
1 parent 9bf81af commit 6ce58e5

File tree

4 files changed

+197
-4
lines changed

4 files changed

+197
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
* support eslint v9 ([#3759][] @mdjermanovic)
1010
* export flat configs from plugin root and fix flat config crash ([#3694][] @bradzacher @mdjermanovic)
1111
* add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick)
12+
* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0)
1213

14+
[#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774
1315
[#3759]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3759
1416
[#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724
1517
[#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694

docs/rules/forbid-component-props.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Examples of **correct** code for this rule:
4444
### `forbid`
4545

4646
An array specifying the names of props that are forbidden. The default value of this option is `['className', 'style']`.
47-
Each array element can either be a string with the property name or object specifying the property name, an optional
47+
Each array element can either be a string with the property name or object specifying the property name or glob string, an optional
4848
custom message, and a component allowlist:
4949

5050
```js
@@ -55,6 +55,16 @@ custom message, and a component allowlist:
5555
}
5656
```
5757

58+
For glob string patterns:
59+
60+
```js
61+
{
62+
"propNamePattern": '**-**',
63+
"allowedFor": ['div'],
64+
"message": "Avoid using kebab-case except div"
65+
}
66+
```
67+
5868
Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item.
5969

6070
```js
@@ -65,6 +75,16 @@ Use `disallowedFor` as an exclusion list to warn on props for specific component
6575
}
6676
```
6777

78+
For glob string patterns:
79+
80+
```js
81+
{
82+
"propNamePattern": "**-**",
83+
"disallowedFor": ["MyComponent"],
84+
"message": "Avoid using kebab-case for MyComponent"
85+
}
86+
```
87+
6888
### Related rules
6989

7090
- [forbid-dom-props](./forbid-dom-props.md)

lib/rules/forbid-component-props.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
'use strict';
77

8+
const minimatch = require('minimatch');
89
const docsUrl = require('../util/docsUrl');
910
const report = require('../util/report');
1011

@@ -70,6 +71,35 @@ module.exports = {
7071
required: ['disallowedFor'],
7172
additionalProperties: false,
7273
},
74+
75+
{
76+
type: 'object',
77+
properties: {
78+
propNamePattern: { type: 'string' },
79+
allowedFor: {
80+
type: 'array',
81+
uniqueItems: true,
82+
items: { type: 'string' },
83+
},
84+
message: { type: 'string' },
85+
},
86+
additionalProperties: false,
87+
},
88+
{
89+
type: 'object',
90+
properties: {
91+
propNamePattern: { type: 'string' },
92+
disallowedFor: {
93+
type: 'array',
94+
uniqueItems: true,
95+
minItems: 1,
96+
items: { type: 'string' },
97+
},
98+
message: { type: 'string' },
99+
},
100+
required: ['disallowedFor'],
101+
additionalProperties: false,
102+
},
73103
],
74104
},
75105
},
@@ -81,16 +111,31 @@ module.exports = {
81111
const configuration = context.options[0] || {};
82112
const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => {
83113
const propName = typeof value === 'string' ? value : value.propName;
114+
const propPattern = value.propNamePattern;
115+
const prop = propName || propPattern;
84116
const options = {
85117
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
86118
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
87119
message: typeof value === 'string' ? null : value.message,
120+
isPattern: !!value.propNamePattern,
88121
};
89-
return [propName, options];
122+
return [prop, options];
90123
}));
91124

125+
function getPropOptions(prop) {
126+
// Get config options having pattern
127+
const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern);
128+
// Match current prop with pattern options, return if matched
129+
const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0]));
130+
// Get options for matched propNamePattern
131+
const propNamePatternOptions = propNamePattern && propNamePattern[1];
132+
133+
const options = forbid.get(prop) || propNamePatternOptions;
134+
return options;
135+
}
136+
92137
function isForbidden(prop, tagName) {
93-
const options = forbid.get(prop);
138+
const options = getPropOptions(prop);
94139
if (!options) {
95140
return false;
96141
}
@@ -121,7 +166,7 @@ module.exports = {
121166
return;
122167
}
123168

124-
const customMessage = forbid.get(prop).message;
169+
const customMessage = getPropOptions(prop).message;
125170

126171
report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
127172
node,

tests/lib/rules/forbid-component-props.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,23 @@ ruleTester.run('forbid-component-props', rule, {
233233
},
234234
],
235235
},
236+
{
237+
code: `
238+
const MyComponent = () => (
239+
<div aria-label="welcome" />
240+
);
241+
`,
242+
options: [
243+
{
244+
forbid: [
245+
{
246+
propNamePattern: '**-**',
247+
allowedFor: ['div'],
248+
},
249+
],
250+
},
251+
],
252+
},
236253
]),
237254

238255
invalid: parsers.all([
@@ -553,5 +570,114 @@ ruleTester.run('forbid-component-props', rule, {
553570
},
554571
],
555572
},
573+
{
574+
code: `
575+
const MyComponent = () => (
576+
<Foo kebab-case-prop={123} />
577+
);
578+
`,
579+
options: [
580+
{
581+
forbid: [
582+
{
583+
propNamePattern: '**-**',
584+
},
585+
],
586+
},
587+
],
588+
errors: [
589+
{
590+
messageId: 'propIsForbidden',
591+
data: { prop: 'kebab-case-prop' },
592+
line: 3,
593+
column: 16,
594+
type: 'JSXAttribute',
595+
},
596+
],
597+
},
598+
{
599+
code: `
600+
const MyComponent = () => (
601+
<Foo kebab-case-prop={123} />
602+
);
603+
`,
604+
options: [
605+
{
606+
forbid: [
607+
{
608+
propNamePattern: '**-**',
609+
message: 'Avoid using kebab-case',
610+
},
611+
],
612+
},
613+
],
614+
errors: [
615+
{
616+
message: 'Avoid using kebab-case',
617+
line: 3,
618+
column: 16,
619+
type: 'JSXAttribute',
620+
},
621+
],
622+
},
623+
{
624+
code: `
625+
const MyComponent = () => (
626+
<div>
627+
<div aria-label="Hello Akul" />
628+
<Foo kebab-case-prop={123} />
629+
</div>
630+
);
631+
`,
632+
options: [
633+
{
634+
forbid: [
635+
{
636+
propNamePattern: '**-**',
637+
allowedFor: ['div'],
638+
},
639+
],
640+
},
641+
],
642+
errors: [
643+
{
644+
messageId: 'propIsForbidden',
645+
data: { prop: 'kebab-case-prop' },
646+
line: 5,
647+
column: 18,
648+
type: 'JSXAttribute',
649+
},
650+
],
651+
},
652+
{
653+
code: `
654+
const MyComponent = () => (
655+
<div>
656+
<div aria-label="Hello Akul" />
657+
<h1 data-id="my-heading" />
658+
<Foo kebab-case-prop={123} />
659+
</div>
660+
);
661+
`,
662+
options: [
663+
{
664+
forbid: [
665+
{
666+
propNamePattern: '**-**',
667+
disallowedFor: ['Foo'],
668+
},
669+
],
670+
},
671+
],
672+
errors: [
673+
{
674+
messageId: 'propIsForbidden',
675+
data: { prop: 'kebab-case-prop' },
676+
line: 6,
677+
column: 18,
678+
type: 'JSXAttribute',
679+
},
680+
],
681+
},
556682
]),
557683
});

0 commit comments

Comments
 (0)