Repository: jsx-eslint/eslint-plugin-react (JavaScript)

Issue:
Add Rule option "disallowedFor" for component prop
Suppose we have a component that accept a prop named string.

``` javascript
function Comp(props) {
    return <div>{props.string}</div>;
}

function App() {
    return <Comp string="213"  />;
}

```

As needs change, we need to replace string prop by another named number.

``` javascript

function Comp(props) {
    return <div>{props.number || props.string}</div>;
}
Comp.propTypes = {
    number: PropTypes.number,
    /**
     * @deprecated use number instead
     */
    string: PropTypes.string
};
Comp.defaultProps = {
    string: '123',
    number: 123
};
```

The string prop will works in a period of time, but it's better to replace it with number prop.
This rule option can be use to avoid using deprecated prop for Comp.

``` json
{
"react/forbid-component-props": [
    "error", { "forbid": [{"propName": "string", "disallowedList": ["Comp"]}] }
  ]
}

```
![image](https://user-images.githubusercontent.com/19347280/189034303-da35ea57-1f86-4cb0-bc51-a406ed5bbb69.png)




Discussion:
Since it's marked as deprecated with jsdoc, there's `eslint-plugin-deprecated` - and if you're using TS, it will catch it too.

Why can't you just remove it and make a major bump?
Hello, I would also like this: my use case is for discouraging the usage of props in components that I don't control, and thus can't annotate or alter the component myself.

Specifically, I want to disallow the `required` parameter on Ant Design's `Form.Item` to avoid confusion with a require parameter I'm adding in HOCs that we create. And generally I dislike the `required` parameter in Ant Design 4.x even without the conflicting HOC parameter, I find it inherently confusing - it only applies styling, and doesn't actually enforce validation, which is very surprising to me.

I could wrap `Form.Item` in an HOC and use that instead, but that doesn't prevent others from using the original component, so it doesn't really get me very far. I also don't want to create an HOC and than ban the original altogether, because that would be a much larger change for not a lot of benefit.

I want to be able to mark props as disallowed in libraries that I don't control, and `allowedFor` on this rule seems like the way to accomplish that. I'm not often one to question library's decisions, sometimes there are exceptions and I want to just avoid using a feature because it is too confusing or troublesome, and this is one of those cases.
@cincodenada you can prevent them from using the original component using `no-restricted-imports` and/or `no-restricted-properties`, fwiw.

That said, thanks, that is a reasonable use case.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b264f6356c..60030fb736 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 * [`display-name`]: add `checkContextObjects` option ([#3529][] @JulesBlm)
 * [`jsx-first-prop-new-line`]: add `multiprop` option ([#3533][] @haydncomley)
 * [`no-deprecated`]: add React 18 deprecations ([#3548][] @sergei-startsev)
+* [`forbid-component-props`]: add `disallowedFor` option ([#3417][] @jacketwpbb)
 
 ### Fixed
 * [`no-array-index-key`]: consider flatMap ([#3530][] @k-yle)
@@ -26,6 +27,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 [#3533]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3533
 [#3530]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3530
 [#3529]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3529
+[#3417]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3417
 
 ## [7.32.2] - 2023.01.28
 
diff --git a/docs/rules/forbid-component-props.md b/docs/rules/forbid-component-props.md
index ddcfc12eb7..2d624be31c 100644
--- a/docs/rules/forbid-component-props.md
+++ b/docs/rules/forbid-component-props.md
@@ -50,8 +50,18 @@ custom message, and a component allowlist:
 ```js
 {
   "propName": "someProp",
-  "allowedFor": [SomeComponent, AnotherComponent],
-  "message": "Avoid using someProp"
+  "allowedFor": ["SomeComponent", "AnotherComponent"],
+  "message": "Avoid using someProp except SomeComponent and AnotherComponent"
+}
+```
+
+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"
 }
 ```
 
diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js
index 95ba3a5377..cca553e123 100644
--- a/lib/rules/forbid-component-props.js
+++ b/lib/rules/forbid-component-props.js
@@ -39,26 +39,37 @@ module.exports = {
         forbid: {
           type: 'array',
           items: {
-            anyOf: [{
-              type: 'string',
-            }, {
-              type: 'object',
-              properties: {
-                propName: {
-                  type: 'string',
-                },
-                allowedFor: {
-                  type: 'array',
-                  uniqueItems: true,
-                  items: {
-                    type: 'string',
+            anyOf: [
+              { type: 'string' },
+              {
+                type: 'object',
+                properties: {
+                  propName: { type: 'string' },
+                  allowedFor: {
+                    type: 'array',
+                    uniqueItems: true,
+                    items: { type: 'string' },
                   },
+                  message: { type: 'string' },
                 },
-                message: {
-                  type: 'string',
+                additionalProperties: false,
+              },
+              {
+                type: 'object',
+                properties: {
+                  propName: { type: 'string' },
+                  disallowedFor: {
+                    type: 'array',
+                    uniqueItems: true,
+                    minItems: 1,
+                    items: { type: 'string' },
+                  },
+                  message: { type: 'string' },
                 },
+                required: ['disallowedFor'],
+                additionalProperties: false,
               },
-            }],
+            ],
           },
         },
       },
@@ -71,6 +82,7 @@ module.exports = {
       const propName = typeof value === 'string' ? value : value.propName;
       const options = {
         allowList: typeof value === 'string' ? [] : (value.allowedFor || []),
+        disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []),
         message: typeof value === 'string' ? null : value.message,
       };
       return [propName, options];
@@ -78,9 +90,17 @@ module.exports = {
 
     function isForbidden(prop, tagName) {
       const options = forbid.get(prop);
-      const allowList = options ? options.allowList : undefined;
+      if (!options) {
+        return false;
+      }
+
+      // 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;
+
       // if the tagName is undefined (`<this.something>`), we assume it's a forbidden element
-      return typeof allowList !== 'undefined' && (typeof tagName === 'undefined' || allowList.indexOf(tagName) === -1);
+      return typeof tagName === 'undefined' || isTagForbidden;
     }
 
     return {


Test patch:
diff --git a/tests/lib/rules/forbid-component-props.js b/tests/lib/rules/forbid-component-props.js
index ac27537fd4..5f1aa7e68d 100644
--- a/tests/lib/rules/forbid-component-props.js
+++ b/tests/lib/rules/forbid-component-props.js
@@ -151,12 +151,103 @@ ruleTester.run('forbid-component-props', rule, {
         },
       ],
     },
+    {
+      code: `
+        const item = (<Foo className="foo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['ReactModal'],
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        const item = (<Foo className="foo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['ReactModal'],
+            },
+          ],
+        },
+      ],
+    },
     {
       code: `
         <fbt:param name="Total number of files" number={true} />
       `,
       features: ['jsx namespace'],
     },
+    {
+      code: `
+        const item = (
+          <Foo className="bar">
+            <ReactModal style={{color: "red"}} />
+          </Foo>
+        );
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['OtherModal', 'ReactModal'],
+            },
+            {
+              propName: 'style',
+              disallowedFor: ['Foo'],
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        const item = (
+          <Foo className="bar">
+            <ReactModal style={{color: "red"}} />
+          </Foo>
+        );
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['OtherModal', 'ReactModal'],
+            },
+            {
+              propName: 'style',
+              allowedFor: ['ReactModal'],
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        const item = (<this.ReactModal className="foo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['ReactModal'],
+            },
+          ],
+        },
+      ],
+    },
   ]),
 
   invalid: parsers.all([
@@ -235,6 +326,34 @@ ruleTester.run('forbid-component-props', rule, {
         },
       ],
     },
+    {
+      code: `
+        var First = createReactClass({
+          propTypes: externalPropTypes,
+          render: function() {
+            return <Foo style={{color: "red"}} />;
+          }
+        });
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'style',
+              disallowedFor: ['Foo'],
+            },
+          ],
+        },
+      ],
+      errors: [
+        {
+          messageId: 'propIsForbidden',
+          data: { prop: 'style' },
+          line: 5,
+          type: 'JSXAttribute',
+        },
+      ],
+    },
     {
       code: `
         const item = (<Foo className="foo" />);
@@ -282,6 +401,78 @@ ruleTester.run('forbid-component-props', rule, {
         },
       ],
     },
+    {
+      code: `
+        const item = (<this.ReactModal className="foo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['this.ReactModal'],
+            },
+          ],
+        },
+      ],
+      errors: [
+        {
+          messageId: 'propIsForbidden',
+          data: { prop: 'className' },
+          line: 2,
+          column: 40,
+          type: 'JSXAttribute',
+        },
+      ],
+    },
+    {
+      code: `
+        const item = (<ReactModal className="foo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['ReactModal'],
+            },
+          ],
+        },
+      ],
+      errors: [
+        {
+          messageId: 'propIsForbidden',
+          data: { prop: 'className' },
+          line: 2,
+          column: 35,
+          type: 'JSXAttribute',
+        },
+      ],
+    },
+    {
+      code: `
+        const item = (<AntdLayout.Content className="antdFoo" />);
+      `,
+      options: [
+        {
+          forbid: [
+            {
+              propName: 'className',
+              disallowedFor: ['AntdLayout.Content'],
+            },
+          ],
+        },
+      ],
+      errors: [
+        {
+          messageId: 'propIsForbidden',
+          data: { prop: 'className' },
+          line: 2,
+          column: 43,
+          type: 'JSXAttribute',
+        },
+      ],
+    },
     {
       code: `
         const item = (<Foo className="foo" />);