From a561e629988a447b0ee72a9c49291d1430ec6ffe Mon Sep 17 00:00:00 2001 From: Max Kudrin Date: Sun, 12 Jan 2025 00:01:05 +0300 Subject: [PATCH] [New] forbid-dom-props: Add `valueRegex` option for forbidden props Discussion: jsx-eslint#3876 --- CHANGELOG.md | 4 + docs/rules/forbid-dom-props.md | 49 ++++++++- lib/rules/forbid-dom-props.js | 37 +++++-- tests/lib/rules/forbid-dom-props.js | 161 ++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bec3e261d..3f1fe1c3f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `valueRegex` 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 diff --git a/docs/rules/forbid-dom-props.md b/docs/rules/forbid-dom-props.md index 0b6a323c4f..e07bcb0b4e 100644 --- a/docs/rules/forbid-dom-props.md +++ b/docs/rules/forbid-dom-props.md @@ -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. `
`): +custom message, DOM nodes disallowed list (e.g. `
`) and a specific regular expression for prohibited prop values: ```js { "propName": "someProp", "disallowedFor": ["DOMNode", "AnotherDOMNode"], + "valueRegex": "^someValue$", "message": "Avoid using someProp" } ``` +Example of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`. + +```jsx +const First = (props) => ( + +); +``` + +Example of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedFor: ['span'] }] }`. + +```jsx +const First = (props) => ( +
+); +``` + +Examples of **incorrect** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', valueRegex: '^someValue$' }] }`. + +```jsx +const First = (props) => ( +
+); +``` + +```jsx +const First = (props) => ( + +); +``` + +Examples of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', valueRegex: '^someValue$' }] }`. + +```jsx +const First = (props) => ( + +); +``` + +```jsx +const First = (props) => ( +
+); +``` + ### Related rules - [forbid-component-props](./forbid-component-props.md) diff --git a/lib/rules/forbid-dom-props.js b/lib/rules/forbid-dom-props.js index 4638a8700d..c691fbd5a5 100644 --- a/lib/rules/forbid-dom-props.js +++ b/lib/rules/forbid-dom-props.js @@ -18,23 +18,37 @@ const DEFAULTS = []; // Rule Definition // ------------------------------------------------------------------------------ +/** @typedef {{ disallowList: null | string[]; message: null | string; valueRegex: null | RegExp }} ForbidMapType */ /** - * @param {Map} forbidMap // { disallowList: null | string[], message: null | string } + * @param {Map} 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; + } + + if (typeof tagName === 'undefined') { + return true; + } + + return ( + !options.disallowList || options.disallowList.indexOf(tagName) !== -1 + ) && ( + !options.valueRegex + || options.valueRegex.test(propValue) ); } 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} */ @@ -70,6 +84,9 @@ module.exports = { type: 'string', }, }, + valueRegex: { + type: 'string', + }, message: { type: 'string', }, @@ -91,6 +108,7 @@ module.exports = { return [propName, { disallowList: typeof value === 'string' ? null : (value.disallowedFor || null), message: typeof value === 'string' ? null : value.message, + valueRegex: typeof value.valueRegex === 'string' ? new RegExp(value.valueRegex) : null, }]; })); @@ -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 isRegexSpecified = forbid.get(prop).valueRegex !== null; + const message = customMessage || (isRegexSpecified && messages.propIsForbiddenWithValue) || messages.propIsForbidden; + const messageId = !customMessage && ((isRegexSpecified && 'propIsForbiddenWithValue') || 'propIsForbidden'); - report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', { + report(context, message, messageId, { node, data: { prop, + propValue, }, }); }, diff --git a/tests/lib/rules/forbid-dom-props.js b/tests/lib/rules/forbid-dom-props.js index a11d39fe00..8a87007e49 100644 --- a/tests/lib/rules/forbid-dom-props.js +++ b/tests/lib/rules/forbid-dom-props.js @@ -112,6 +112,58 @@ ruleTester.run('forbid-dom-props', rule, { }, ], }, + { + code: ` + const First = (props) => ( + + ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + valueRegex: '^someValue$', + }, + ], + }, + ], + }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + valueRegex: '^someValue$', + }, + ], + }, + ], + }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + valueRegex: '^someValue$', + disallowedFor: ['span'], + }, + ], + }, + ], + }, ]), invalid: parsers.all([ @@ -191,6 +243,57 @@ ruleTester.run('forbid-dom-props', rule, { }, ], }, + { + code: ` + const First = (props) => ( + + ); + `, + options: [ + { + forbid: [ + { + propName: 'otherProp', + disallowedFor: ['span'], + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbidden', + data: { prop: 'otherProp' }, + line: 3, + column: 17, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + valueRegex: '^someValue$', + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbiddenWithValue', + line: 3, + column: 16, + type: 'JSXAttribute', + }, + ], + }, { code: ` const First = (props) => ( @@ -324,5 +427,63 @@ ruleTester.run('forbid-dom-props', rule, { }, ], }, + { + code: ` + const First = (props) => ( +
+ + Foobar +
+

+

+

+

+ ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + disallowedFor: ['div', 'span'], + message: 'Please use class instead of ClassName', + }, + { propName: 'otherProp', message: 'Avoid using otherProp' }, + { + propName: 'thirdProp', + disallowedFor: ['p'], + valueRegex: '^baz$', + message: 'Do not use thirdProp with value 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 value baz on p', + line: 9, + column: 16, + type: 'JSXAttribute', + }, + ], + }, ]), });