Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] forbid-dom-props: Add disallowedValues option for forbidden props #3877

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange

## Unreleased

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

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

## [7.37.4] - 2025.01.12

### Fixed
Expand Down
49 changes: 47 additions & 2 deletions docs/rules/forbid-dom-props.md
Original file line number Diff line number Diff line change
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',
},
],
},
]),
});
Loading