Skip to content

Commit 3e01e10

Browse files
Efimenkoljharb
authored andcommitted
[New] forbid-component-props: add allowedForPatterns/disallowedForPatterns options
1 parent 4ecf034 commit 3e01e10

File tree

3 files changed

+278
-16
lines changed

3 files changed

+278
-16
lines changed

docs/rules/forbid-component-props.md

+18-8
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,17 @@ custom message, and a component allowlist:
5555
}
5656
```
5757

58-
For glob string patterns:
58+
Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item.
59+
60+
```js
61+
{
62+
"propName": "someProp",
63+
"disallowedFor": ["SomeComponent", "AnotherComponent"],
64+
"message": "Avoid using someProp for SomeComponent and AnotherComponent"
65+
}
66+
```
67+
68+
For `propNamePattern` glob string patterns:
5969

6070
```js
6171
{
@@ -65,23 +75,23 @@ For glob string patterns:
6575
}
6676
```
6777

68-
Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item.
78+
Use `allowedForPatterns` for glob string patterns:
6979

7080
```js
7181
{
7282
"propName": "someProp",
73-
"disallowedFor": ["SomeComponent", "AnotherComponent"],
74-
"message": "Avoid using someProp for SomeComponent and AnotherComponent"
83+
"allowedForPatterns": ["*Component"],
84+
"message": "Avoid using `someProp` except components that match the `*Component` pattern"
7585
}
7686
```
7787

78-
For glob string patterns:
88+
Use `disallowedForPatterns` for glob string patterns:
7989

8090
```js
8191
{
82-
"propNamePattern": "**-**",
83-
"disallowedFor": ["MyComponent"],
84-
"message": "Avoid using kebab-case for MyComponent"
92+
"propName": "someProp",
93+
"disallowedForPatterns": ["*Component"],
94+
"message": "Avoid using `someProp` for components that match the `*Component` pattern"
8595
}
8696
```
8797

lib/rules/forbid-component-props.js

+67-8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ module.exports = {
5252
uniqueItems: true,
5353
items: { type: 'string' },
5454
},
55+
allowedForPatterns: {
56+
type: 'array',
57+
uniqueItems: true,
58+
items: { type: 'string' },
59+
},
5560
message: { type: 'string' },
5661
},
5762
additionalProperties: false,
@@ -66,12 +71,20 @@ module.exports = {
6671
minItems: 1,
6772
items: { type: 'string' },
6873
},
74+
disallowedForPatterns: {
75+
type: 'array',
76+
uniqueItems: true,
77+
minItems: 1,
78+
items: { type: 'string' },
79+
},
6980
message: { type: 'string' },
7081
},
71-
required: ['disallowedFor'],
82+
anyOf: [
83+
{ required: ['disallowedFor'] },
84+
{ required: ['disallowedForPatterns'] },
85+
],
7286
additionalProperties: false,
7387
},
74-
7588
{
7689
type: 'object',
7790
properties: {
@@ -81,6 +94,11 @@ module.exports = {
8194
uniqueItems: true,
8295
items: { type: 'string' },
8396
},
97+
allowedForPatterns: {
98+
type: 'array',
99+
uniqueItems: true,
100+
items: { type: 'string' },
101+
},
84102
message: { type: 'string' },
85103
},
86104
additionalProperties: false,
@@ -95,9 +113,18 @@ module.exports = {
95113
minItems: 1,
96114
items: { type: 'string' },
97115
},
116+
disallowedForPatterns: {
117+
type: 'array',
118+
uniqueItems: true,
119+
minItems: 1,
120+
items: { type: 'string' },
121+
},
98122
message: { type: 'string' },
99123
},
100-
required: ['disallowedFor'],
124+
anyOf: [
125+
{ required: ['disallowedFor'] },
126+
{ required: ['disallowedForPatterns'] },
127+
],
101128
additionalProperties: false,
102129
},
103130
],
@@ -114,8 +141,10 @@ module.exports = {
114141
const propPattern = value.propNamePattern;
115142
const prop = propName || propPattern;
116143
const options = {
117-
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
118-
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
144+
allowList: [].concat(value.allowedFor || []),
145+
allowPatternList: [].concat(value.allowedForPatterns || []),
146+
disallowList: [].concat(value.disallowedFor || []),
147+
disallowPatternList: [].concat(value.disallowedForPatterns || []),
119148
message: typeof value === 'string' ? null : value.message,
120149
isPattern: !!value.propNamePattern,
121150
};
@@ -140,10 +169,40 @@ module.exports = {
140169
return false;
141170
}
142171

172+
function checkIsTagForbiddenByAllowOptions() {
173+
if (options.allowList.indexOf(tagName) !== -1) {
174+
return false;
175+
}
176+
177+
if (options.allowPatternList.length === 0) {
178+
return true;
179+
}
180+
181+
return options.allowPatternList.every(
182+
(pattern) => !minimatch(tagName, pattern)
183+
);
184+
}
185+
186+
function checkIsTagForbiddenByDisallowOptions() {
187+
if (options.disallowList.indexOf(tagName) !== -1) {
188+
return true;
189+
}
190+
191+
if (options.disallowPatternList.length === 0) {
192+
return false;
193+
}
194+
195+
return options.disallowPatternList.some(
196+
(pattern) => minimatch(tagName, pattern)
197+
);
198+
}
199+
200+
const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0;
201+
143202
// disallowList should have a least one item (schema configuration)
144-
const isTagForbidden = options.disallowList.length > 0
145-
? options.disallowList.indexOf(tagName) !== -1
146-
: options.allowList.indexOf(tagName) === -1;
203+
const isTagForbidden = hasDisallowOptions
204+
? checkIsTagForbiddenByDisallowOptions()
205+
: checkIsTagForbiddenByAllowOptions();
147206

148207
// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
149208
return typeof tagName === 'undefined' || isTagForbidden;

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

+193
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,78 @@ ruleTester.run('forbid-component-props', rule, {
250250
},
251251
],
252252
},
253+
{
254+
code: `
255+
const rootElement = (
256+
<Root>
257+
<SomeIcon className="size-lg" />
258+
<AnotherIcon className="size-lg" />
259+
<SomeSvg className="size-lg" />
260+
<UICard className="size-lg" />
261+
<UIButton className="size-lg" />
262+
</Root>
263+
);
264+
`,
265+
options: [
266+
{
267+
forbid: [
268+
{
269+
propName: 'className',
270+
allowedForPatterns: ['*Icon', '*Svg', 'UI*'],
271+
},
272+
],
273+
},
274+
],
275+
},
276+
{
277+
code: `
278+
const rootElement = (
279+
<Root>
280+
<SomeIcon className="size-lg" />
281+
<AnotherIcon className="size-lg" />
282+
<SomeSvg className="size-lg" />
283+
<UICard className="size-lg" />
284+
<UIButton className="size-lg" />
285+
<ButtonLegacy className="size-lg" />
286+
</Root>
287+
);
288+
`,
289+
options: [
290+
{
291+
forbid: [
292+
{
293+
propName: 'className',
294+
allowedFor: ['ButtonLegacy'],
295+
allowedForPatterns: ['*Icon', '*Svg', 'UI*'],
296+
},
297+
],
298+
},
299+
],
300+
},
301+
{
302+
code: `
303+
const rootElement = (
304+
<Root>
305+
<SomeIcon className="size-lg" />
306+
<AnotherIcon className="size-lg" />
307+
<SomeSvg className="size-lg" />
308+
<UICard className="size-lg" />
309+
<UIButton className="size-lg" />
310+
</Root>
311+
);
312+
`,
313+
options: [
314+
{
315+
forbid: [
316+
{
317+
propName: 'className',
318+
disallowedFor: ['Modal'],
319+
disallowedForPatterns: ['*Legacy', 'Shared*'],
320+
},
321+
],
322+
},
323+
],
324+
},
253325
]),
254326

255327
invalid: parsers.all([
@@ -679,5 +751,126 @@ ruleTester.run('forbid-component-props', rule, {
679751
},
680752
],
681753
},
754+
{
755+
code: `
756+
const rootElement = () => (
757+
<Root>
758+
<SomeIcon className="size-lg" />
759+
<SomeSvg className="size-lg" />
760+
</Root>
761+
);
762+
`,
763+
options: [
764+
{
765+
forbid: [
766+
{
767+
propName: 'className',
768+
message: 'className available only for icons',
769+
allowedForPatterns: ['*Icon'],
770+
},
771+
],
772+
},
773+
],
774+
errors: [
775+
{
776+
message: 'className available only for icons',
777+
line: 5,
778+
column: 22,
779+
type: 'JSXAttribute',
780+
},
781+
],
782+
},
783+
{
784+
code: `
785+
const rootElement = () => (
786+
<Root>
787+
<UICard style={{backgroundColor: black}}/>
788+
<SomeIcon className="size-lg" />
789+
<SomeSvg className="size-lg" style={{fill: currentColor}} />
790+
</Root>
791+
);
792+
`,
793+
options: [
794+
{
795+
forbid: [
796+
{
797+
propName: 'className',
798+
message: 'className available only for icons',
799+
allowedForPatterns: ['*Icon'],
800+
},
801+
{
802+
propName: 'style',
803+
message: 'style available only for SVGs',
804+
allowedForPatterns: ['*Svg'],
805+
},
806+
],
807+
},
808+
],
809+
errors: [
810+
{
811+
message: 'style available only for SVGs',
812+
line: 4,
813+
column: 21,
814+
type: 'JSXAttribute',
815+
},
816+
{
817+
message: 'className available only for icons',
818+
line: 6,
819+
column: 22,
820+
type: 'JSXAttribute',
821+
},
822+
],
823+
},
824+
{
825+
code: `
826+
const rootElement = (
827+
<Root>
828+
<SomeIcon className="size-lg" />
829+
<AnotherIcon className="size-lg" />
830+
<SomeSvg className="size-lg" />
831+
<UICard className="size-lg" />
832+
<ButtonLegacy className="size-lg" />
833+
</Root>
834+
);
835+
`,
836+
options: [
837+
{
838+
forbid: [
839+
{
840+
propName: 'className',
841+
disallowedFor: ['SomeSvg'],
842+
disallowedForPatterns: ['UI*', '*Icon'],
843+
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
844+
},
845+
],
846+
},
847+
],
848+
errors: [
849+
{
850+
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
851+
line: 4,
852+
column: 23,
853+
type: 'JSXAttribute',
854+
},
855+
{
856+
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
857+
line: 5,
858+
column: 26,
859+
type: 'JSXAttribute',
860+
},
861+
{
862+
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
863+
line: 6,
864+
column: 22,
865+
type: 'JSXAttribute',
866+
},
867+
{
868+
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
869+
line: 7,
870+
column: 21,
871+
type: 'JSXAttribute',
872+
},
873+
],
874+
},
682875
]),
683876
});

0 commit comments

Comments
 (0)