Skip to content

Commit

Permalink
[New] forbid-dom-props: Add disallowedValues option for forbidden p…
Browse files Browse the repository at this point in the history
…rops

Discussion: jsx-eslint#3876
  • Loading branch information
makxca committed Jan 13, 2025
1 parent efc021f commit de2e1ea
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 10 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* [`no-unknown-property`]: support `onBeforeToggle`, `popoverTarget`, `popoverTargetAction` attributes ([#3865][] @acusti)
* [types] fix types of flat configs ([#3874][] @ljharb)

### Added
* [`forbid-dom-props`]: Add `disallowedValues` option for forbidden props ([#3876][] @makxca)

[#3876]: https://github.com/jsx-eslint/eslint-plugin-react/issues/3876
[#3874]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3874
[#3865]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3865

Expand Down
51 changes: 48 additions & 3 deletions docs/rules/forbid-dom-props.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- end auto-generated rule header -->

This rule prevents passing of props to elements. This rule only applies to DOM Nodes (e.g. `<div />`) and not Components (e.g. `<Component />`).
This rule prevents passing of props to elements. This rule only applies to DOM Nodes (e.g. `<div />`), and not Components (e.g. `<Component />`).
The list of forbidden props can be customized with the `forbid` option.

## Rule Details
Expand Down Expand Up @@ -44,18 +44,63 @@ Examples of **correct** code for this rule:

### `forbid`

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

```js
{
"propName": "someProp",
"disallowedFor": ["DOMNode", "AnotherDOMNode"],
"disallowedValues": ["someValue"],
"message": "Avoid using someProp"
}
```

Example of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`.

```jsx
const First = (props) => (
<span someProp="bar" />
);
```

Example of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`.

```jsx
const First = (props) => (
<div someProp="bar" />
);
```

Examples of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedValues: ['someValue'] }] }`.

```jsx
const First = (props) => (
<div someProp="someValue" />
);
```

```jsx
const First = (props) => (
<span someProp="someValue" />
);
```

Examples of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedValues: ['someValue'] }] }`.

```jsx
const First = (props) => (
<Foo someProp="someValue" />
);
```

```jsx
const First = (props) => (
<div someProp="value" />
);
```

### Related rules

- [forbid-component-props](./forbid-component-props.md)
37 changes: 30 additions & 7 deletions lib/rules/forbid-dom-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,33 @@ const DEFAULTS = [];
// Rule Definition
// ------------------------------------------------------------------------------

/** @typedef {{ disallowList: null | string[]; message: null | string; disallowedValues: string[] | null }} ForbidMapType */
/**
* @param {Map<string, object>} forbidMap // { disallowList: null | string[], message: null | string }
* @param {Map<string, ForbidMapType>} forbidMap
* @param {string} prop
* @param {string} propValue
* @param {string} tagName
* @returns {boolean}
*/
function isForbidden(forbidMap, prop, tagName) {
function isForbidden(forbidMap, prop, propValue, tagName) {
const options = forbidMap.get(prop);
return options && (
typeof tagName === 'undefined'
|| !options.disallowList

if (!options) {
return false;
}

return (
!options.disallowList
|| options.disallowList.indexOf(tagName) !== -1
) && (
!options.disallowedValues
|| options.disallowedValues.indexOf(propValue) !== -1
);
}

const messages = {
propIsForbidden: 'Prop "{{prop}}" is forbidden on DOM Nodes',
propIsForbiddenWithValue: 'Prop "{{prop}}" with value "{{propValue}}" is forbidden on DOM Nodes',
};

/** @type {import('eslint').Rule.RuleModule} */
Expand Down Expand Up @@ -70,6 +80,13 @@ module.exports = {
type: 'string',
},
},
disallowedValues: {
type: 'array',
uniqueItems: true,
items: {
type: 'string',
},
},
message: {
type: 'string',
},
Expand All @@ -90,6 +107,7 @@ module.exports = {
const propName = typeof value === 'string' ? value : value.propName;
return [propName, {
disallowList: typeof value === 'string' ? null : (value.disallowedFor || null),
disallowedValues: typeof value === 'string' ? null : (value.disallowedValues || null),
message: typeof value === 'string' ? null : value.message,
}];
}));
Expand All @@ -103,17 +121,22 @@ module.exports = {
}

const prop = node.name.name;
const propValue = node.value.value;

if (!isForbidden(forbid, prop, tag)) {
if (!isForbidden(forbid, prop, propValue, tag)) {
return;
}

const customMessage = forbid.get(prop).message;
const isValuesListSpecified = forbid.get(prop).disallowedValues !== null;
const message = customMessage || (isValuesListSpecified && messages.propIsForbiddenWithValue) || messages.propIsForbidden;
const messageId = !customMessage && ((isValuesListSpecified && 'propIsForbiddenWithValue') || 'propIsForbidden');

report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', {
report(context, message, messageId, {
node,
data: {
prop,
propValue,
},
});
},
Expand Down
186 changes: 186 additions & 0 deletions tests/lib/rules/forbid-dom-props.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,75 @@ ruleTester.run('forbid-dom-props', rule, {
},
],
},
{
code: `
const First = (props) => (
<div someProp="someValue" />
);
`,
options: [
{
forbid: [
{
propName: 'someProp',
disallowedValues: [],
},
],
},
],
},
{
code: `
const First = (props) => (
<Foo someProp="someValue" />
);
`,
options: [
{
forbid: [
{
propName: 'someProp',
disallowedValues: ['someValue'],
},
],
},
],
},
{
code: `
const First = (props) => (
<div someProp="value" />
);
`,
options: [
{
forbid: [
{
propName: 'someProp',
disallowedValues: ['someValue'],
},
],
},
],
},
{
code: `
const First = (props) => (
<div someProp="someValue" />
);
`,
options: [
{
forbid: [
{
propName: 'someProp',
disallowedValues: ['someValue'],
disallowedFor: ['span'],
},
],
},
],
},
]),

invalid: parsers.all([
Expand Down Expand Up @@ -191,6 +260,58 @@ ruleTester.run('forbid-dom-props', rule, {
},
],
},
{
code: `
const First = (props) => (
<span otherProp="bar" />
);
`,
options: [
{
forbid: [
{
propName: 'otherProp',
disallowedFor: ['span'],
},
],
},
],
errors: [
{
messageId: 'propIsForbidden',
data: { prop: 'otherProp' },
line: 3,
column: 17,
type: 'JSXAttribute',
},
],
},
{
code: `
const First = (props) => (
<div someProp="someValue" />
);
`,
options: [
{
forbid: [
{
propName: 'someProp',
disallowedValues: ['someValue'],
},
],
},
],
errors: [
{
messageId: 'propIsForbiddenWithValue',
data: { prop: 'someProp', propValue: 'someValue' },
line: 3,
column: 16,
type: 'JSXAttribute',
},
],
},
{
code: `
const First = (props) => (
Expand Down Expand Up @@ -324,5 +445,70 @@ ruleTester.run('forbid-dom-props', rule, {
},
],
},
{
code: `
const First = (props) => (
<div className="foo">
<input className="boo" />
<span className="foobar">Foobar</span>
<div otherProp="bar" />
<p thirdProp="foo" />
<div thirdProp="baz" />
<p thirdProp="bar" />
<p thirdProp="baz" />
</div>
);
`,
options: [
{
forbid: [
{
propName: 'className',
disallowedFor: ['div', 'span'],
message: 'Please use class instead of ClassName',
},
{ propName: 'otherProp', message: 'Avoid using otherProp' },
{
propName: 'thirdProp',
disallowedFor: ['p'],
disallowedValues: ['bar', 'baz'],
message: 'Do not use thirdProp with values bar and baz on p',
},
],
},
],
errors: [
{
message: 'Please use class instead of ClassName',
line: 3,
column: 16,
type: 'JSXAttribute',
},
{
message: 'Please use class instead of ClassName',
line: 5,
column: 19,
type: 'JSXAttribute',
},
{
message: 'Avoid using otherProp',
line: 6,
column: 18,
type: 'JSXAttribute',
},
{
message: 'Do not use thirdProp with values bar and baz on p',
line: 9,
column: 16,
type: 'JSXAttribute',
},
{
message: 'Do not use thirdProp with values bar and baz on p',
line: 10,
column: 16,
type: 'JSXAttribute',
},
],
},
]),
});

0 comments on commit de2e1ea

Please sign in to comment.