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

Issue:
feat: no-danger find props on all components (not just native)
Unless I am mistaken, it seems today that the rule `react/no-danger` has specific logic to only find usages on native elements. See the `isDOMComponent` check here:

https://github.com/jsx-eslint/eslint-plugin-react/blob/8c9ec8a3929403ac34a22c11cb9bd2575ba766bd/lib/rules/no-danger.js#L63

We think it could be useful to support non-native components as well, especially in cases like `styled-components` wrappers, or components that want to offer this prop to be forwarded internally to a native element.

This could be a new default behavior, or an opt-in behavior via a new rule option.

Pull request:
[New] `no-danger`: add `customComponentNames` option
Closes #3434 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4cd79ec..57d22ee4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,16 +13,18 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 * [`jsx-handler-names`]: support ignoring component names ([#3772][] @akulsr0)
 * version settings: Allow react defaultVersion to be configurable ([#3771][] @onlywei)
 * [`jsx-closing-tag-location`]: add `line-aligned` option ([#3777] @kimtaejin3)
+* [`no-danger`]: add `customComponentNames` option ([#3748][] @akulsr0)
 
 ### Changed
 * [Refactor] `variableUtil`: Avoid creating a single flat variable scope for each lookup ([#3782][] @DanielRosenwasser)
 
-e[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
+[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
 [#3777]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3777
 [#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774
 [#3772]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3772
 [#3771]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3771
 [#3759]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3759
+[#3748]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3748
 [#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724
 [#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694
 
@@ -60,7 +62,7 @@ e[#3782]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3782
 
 ### Fixed
 * [`boolean-prop-naming`]: avoid a crash with a non-TSTypeReference type ([#3718][] @developer-bandi)
-* [`jsx-no-leaked-render`]: invalid report if left side is boolean ([#3746][] @akulsr0)
+* [`jsx-no-leaked-render`]: invalid report if left eside is boolean ([#3746][] @akulsr0)
 * [`jsx-closing-bracket-location`]: message shows `{{details}}` when there are no details ([#3759][] @mdjermanovic)
 * [`no-invalid-html-attribute`]: ensure error messages are correct ([#3759][] @mdjermanovic, @ljharb)
 
diff --git a/docs/rules/no-danger.md b/docs/rules/no-danger.md
index 417989e0..6ffe8b9a 100644
--- a/docs/rules/no-danger.md
+++ b/docs/rules/no-danger.md
@@ -24,6 +24,20 @@ var React = require('react');
 var Hello = <div>Hello World</div>;
 ```
 
+## Rule Options
+
+```js
+...
+"react/no-danger": [<enabled>, {
+  "customComponentNames": Array<string>,
+}]
+...
+```
+
+### customComponentNames
+
+Defaults to `[]`, if you want to enable this rule for all custom components you can pass `customComponentNames` as `['*']`, or else you can pass specific components name to the array.
+
 ## When Not To Use It
 
 If you are certain the content passed to dangerouslySetInnerHTML is sanitized HTML you can disable this rule.
diff --git a/lib/rules/no-danger.js b/lib/rules/no-danger.js
index eecdd87e..be5c961e 100644
--- a/lib/rules/no-danger.js
+++ b/lib/rules/no-danger.js
@@ -7,6 +7,7 @@
 
 const has = require('hasown');
 const fromEntries = require('object.fromentries/polyfill')();
+const minimatch = require('minimatch');
 
 const docsUrl = require('../util/docsUrl');
 const jsxUtil = require('../util/jsx');
@@ -55,13 +56,32 @@ module.exports = {
 
     messages,
 
-    schema: [],
+    schema: [{
+      type: 'object',
+      properties: {
+        customComponentNames: {
+          items: {
+            type: 'string',
+          },
+          minItems: 0,
+          type: 'array',
+          uniqueItems: true,
+        },
+      },
+    }],
   },
 
   create(context) {
+    const configuration = context.options[0] || {};
+    const customComponentNames = configuration.customComponentNames || [];
+
     return {
       JSXAttribute(node) {
-        if (jsxUtil.isDOMComponent(node.parent) && isDangerous(node.name.name)) {
+        const functionName = node.parent.name.name;
+
+        const enableCheckingCustomComponent = customComponentNames.some((name) => minimatch(functionName, name));
+
+        if ((enableCheckingCustomComponent || jsxUtil.isDOMComponent(node.parent)) && isDangerous(node.name.name)) {
           report(context, messages.dangerousProp, 'dangerousProp', {
             node,
             data: {


Test patch:
diff --git a/tests/lib/rules/no-danger.js b/tests/lib/rules/no-danger.js
index 11618c77..e354ad28 100644
--- a/tests/lib/rules/no-danger.js
+++ b/tests/lib/rules/no-danger.js
@@ -32,6 +32,26 @@ ruleTester.run('no-danger', rule, {
     { code: '<App />;' },
     { code: '<App dangerouslySetInnerHTML={{ __html: "" }} />;' },
     { code: '<div className="bar"></div>;' },
+    {
+      code: '<div className="bar"></div>;',
+      options: [{ customComponentNames: ['*'] }],
+    },
+    {
+      code: `
+        function App() {
+          return <Title dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['Home'] }],
+    },
+    {
+      code: `
+        function App() {
+          return <TextMUI dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['MUI*'] }],
+    },
   ]),
   invalid: parsers.all([
     {
@@ -43,5 +63,71 @@ ruleTester.run('no-danger', rule, {
         },
       ],
     },
+    {
+      code: '<App dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;',
+      options: [{ customComponentNames: ['*'] }],
+      errors: [
+        {
+          messageId: 'dangerousProp',
+          data: { name: 'dangerouslySetInnerHTML' },
+        },
+      ],
+    },
+    {
+      code: `
+        function App() {
+          return <Title dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['Title'] }],
+      errors: [
+        {
+          messageId: 'dangerousProp',
+          data: { name: 'dangerouslySetInnerHTML' },
+        },
+      ],
+    },
+    {
+      code: `
+        function App() {
+          return <TextFoo dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['*Foo'] }],
+      errors: [
+        {
+          messageId: 'dangerousProp',
+          data: { name: 'dangerouslySetInnerHTML' },
+        },
+      ],
+    },
+    {
+      code: `
+        function App() {
+          return <FooText dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['Foo*'] }],
+      errors: [
+        {
+          messageId: 'dangerousProp',
+          data: { name: 'dangerouslySetInnerHTML' },
+        },
+      ],
+    },
+    {
+      code: `
+        function App() {
+          return <TextMUI dangerouslySetInnerHTML={{ __html: "<span>hello</span>" }} />;
+        }
+      `,
+      options: [{ customComponentNames: ['*MUI'] }],
+      errors: [
+        {
+          messageId: 'dangerousProp',
+          data: { name: 'dangerouslySetInnerHTML' },
+        },
+      ],
+    },
   ]),
 });