Skip to content

Commit

Permalink
[New] forbid-component-props: add allowedForPatterns/`disallowedF…
Browse files Browse the repository at this point in the history
…orPatterns` options
  • Loading branch information
Efimenko authored and ljharb committed Aug 21, 2024
1 parent 4ecf034 commit 3e01e10
Show file tree
Hide file tree
Showing 3 changed files with 278 additions and 16 deletions.
26 changes: 18 additions & 8 deletions docs/rules/forbid-component-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,17 @@ custom message, and a component allowlist:
}
```

For glob string patterns:
Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item.

```js
{
"propName": "someProp",
"disallowedFor": ["SomeComponent", "AnotherComponent"],
"message": "Avoid using someProp for SomeComponent and AnotherComponent"
}
```

For `propNamePattern` glob string patterns:

```js
{
Expand All @@ -65,23 +75,23 @@ For glob string patterns:
}
```

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

```js
{
"propName": "someProp",
"disallowedFor": ["SomeComponent", "AnotherComponent"],
"message": "Avoid using someProp for SomeComponent and AnotherComponent"
"allowedForPatterns": ["*Component"],
"message": "Avoid using `someProp` except components that match the `*Component` pattern"
}
```

For glob string patterns:
Use `disallowedForPatterns` for glob string patterns:

```js
{
"propNamePattern": "**-**",
"disallowedFor": ["MyComponent"],
"message": "Avoid using kebab-case for MyComponent"
"propName": "someProp",
"disallowedForPatterns": ["*Component"],
"message": "Avoid using `someProp` for components that match the `*Component` pattern"
}
```

Expand Down
75 changes: 67 additions & 8 deletions lib/rules/forbid-component-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ module.exports = {
uniqueItems: true,
items: { type: 'string' },
},
allowedForPatterns: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
message: { type: 'string' },
},
additionalProperties: false,
Expand All @@ -66,12 +71,20 @@ module.exports = {
minItems: 1,
items: { type: 'string' },
},
disallowedForPatterns: {
type: 'array',
uniqueItems: true,
minItems: 1,
items: { type: 'string' },
},
message: { type: 'string' },
},
required: ['disallowedFor'],
anyOf: [
{ required: ['disallowedFor'] },
{ required: ['disallowedForPatterns'] },
],
additionalProperties: false,
},

{
type: 'object',
properties: {
Expand All @@ -81,6 +94,11 @@ module.exports = {
uniqueItems: true,
items: { type: 'string' },
},
allowedForPatterns: {
type: 'array',
uniqueItems: true,
items: { type: 'string' },
},
message: { type: 'string' },
},
additionalProperties: false,
Expand All @@ -95,9 +113,18 @@ module.exports = {
minItems: 1,
items: { type: 'string' },
},
disallowedForPatterns: {
type: 'array',
uniqueItems: true,
minItems: 1,
items: { type: 'string' },
},
message: { type: 'string' },
},
required: ['disallowedFor'],
anyOf: [
{ required: ['disallowedFor'] },
{ required: ['disallowedForPatterns'] },
],
additionalProperties: false,
},
],
Expand All @@ -114,8 +141,10 @@ module.exports = {
const propPattern = value.propNamePattern;
const prop = propName || propPattern;
const options = {
allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
allowList: [].concat(value.allowedFor || []),
allowPatternList: [].concat(value.allowedForPatterns || []),
disallowList: [].concat(value.disallowedFor || []),
disallowPatternList: [].concat(value.disallowedForPatterns || []),
message: typeof value === 'string' ? null : value.message,
isPattern: !!value.propNamePattern,
};
Expand All @@ -140,10 +169,40 @@ module.exports = {
return false;
}

function checkIsTagForbiddenByAllowOptions() {
if (options.allowList.indexOf(tagName) !== -1) {
return false;
}

if (options.allowPatternList.length === 0) {
return true;
}

return options.allowPatternList.every(
(pattern) => !minimatch(tagName, pattern)
);
}

function checkIsTagForbiddenByDisallowOptions() {
if (options.disallowList.indexOf(tagName) !== -1) {
return true;
}

if (options.disallowPatternList.length === 0) {
return false;
}

return options.disallowPatternList.some(
(pattern) => minimatch(tagName, pattern)
);
}

const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0;

// disallowList should have a least one item (schema configuration)
const isTagForbidden = options.disallowList.length > 0
? options.disallowList.indexOf(tagName) !== -1
: options.allowList.indexOf(tagName) === -1;
const isTagForbidden = hasDisallowOptions
? checkIsTagForbiddenByDisallowOptions()
: checkIsTagForbiddenByAllowOptions();

// if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
return typeof tagName === 'undefined' || isTagForbidden;
Expand Down
193 changes: 193 additions & 0 deletions tests/lib/rules/forbid-component-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,78 @@ ruleTester.run('forbid-component-props', rule, {
},
],
},
{
code: `
const rootElement = (
<Root>
<SomeIcon className="size-lg" />
<AnotherIcon className="size-lg" />
<SomeSvg className="size-lg" />
<UICard className="size-lg" />
<UIButton className="size-lg" />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
allowedForPatterns: ['*Icon', '*Svg', 'UI*'],
},
],
},
],
},
{
code: `
const rootElement = (
<Root>
<SomeIcon className="size-lg" />
<AnotherIcon className="size-lg" />
<SomeSvg className="size-lg" />
<UICard className="size-lg" />
<UIButton className="size-lg" />
<ButtonLegacy className="size-lg" />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
allowedFor: ['ButtonLegacy'],
allowedForPatterns: ['*Icon', '*Svg', 'UI*'],
},
],
},
],
},
{
code: `
const rootElement = (
<Root>
<SomeIcon className="size-lg" />
<AnotherIcon className="size-lg" />
<SomeSvg className="size-lg" />
<UICard className="size-lg" />
<UIButton className="size-lg" />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
disallowedFor: ['Modal'],
disallowedForPatterns: ['*Legacy', 'Shared*'],
},
],
},
],
},
]),

invalid: parsers.all([
Expand Down Expand Up @@ -679,5 +751,126 @@ ruleTester.run('forbid-component-props', rule, {
},
],
},
{
code: `
const rootElement = () => (
<Root>
<SomeIcon className="size-lg" />
<SomeSvg className="size-lg" />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
message: 'className available only for icons',
allowedForPatterns: ['*Icon'],
},
],
},
],
errors: [
{
message: 'className available only for icons',
line: 5,
column: 22,
type: 'JSXAttribute',
},
],
},
{
code: `
const rootElement = () => (
<Root>
<UICard style={{backgroundColor: black}}/>
<SomeIcon className="size-lg" />
<SomeSvg className="size-lg" style={{fill: currentColor}} />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
message: 'className available only for icons',
allowedForPatterns: ['*Icon'],
},
{
propName: 'style',
message: 'style available only for SVGs',
allowedForPatterns: ['*Svg'],
},
],
},
],
errors: [
{
message: 'style available only for SVGs',
line: 4,
column: 21,
type: 'JSXAttribute',
},
{
message: 'className available only for icons',
line: 6,
column: 22,
type: 'JSXAttribute',
},
],
},
{
code: `
const rootElement = (
<Root>
<SomeIcon className="size-lg" />
<AnotherIcon className="size-lg" />
<SomeSvg className="size-lg" />
<UICard className="size-lg" />
<ButtonLegacy className="size-lg" />
</Root>
);
`,
options: [
{
forbid: [
{
propName: 'className',
disallowedFor: ['SomeSvg'],
disallowedForPatterns: ['UI*', '*Icon'],
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
},
],
},
],
errors: [
{
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
line: 4,
column: 23,
type: 'JSXAttribute',
},
{
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
line: 5,
column: 26,
type: 'JSXAttribute',
},
{
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
line: 6,
column: 22,
type: 'JSXAttribute',
},
{
message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns',
line: 7,
column: 21,
type: 'JSXAttribute',
},
],
},
]),
});

0 comments on commit 3e01e10

Please sign in to comment.