diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bec3e261d..93b89a71e2 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 `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 diff --git a/docs/rules/forbid-dom-props.md b/docs/rules/forbid-dom-props.md index 0b6a323c4f..cf13beab36 100644 --- a/docs/rules/forbid-dom-props.md +++ b/docs/rules/forbid-dom-props.md @@ -2,7 +2,7 @@ -This rule prevents passing of props to elements. This rule only applies to DOM Nodes (e.g. `
`) and not Components (e.g. ``). +This rule prevents passing of props to elements. This rule only applies to DOM Nodes (e.g. `
`), and not Components (e.g. ``). The list of forbidden props can be customized with the `forbid` option. ## Rule Details @@ -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 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) => ( + +); +``` + +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', disallowedValues: ['someValue'] }] }`. + +```jsx +const First = (props) => ( +
+); +``` + +```jsx +const First = (props) => ( + +); +``` + +Examples of **correct** code for this rule, when configured with `{ forbid: [{ propName: 'someProp', disallowedValues: ['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..be7337a57f 100644 --- a/lib/rules/forbid-dom-props.js +++ b/lib/rules/forbid-dom-props.js @@ -18,23 +18,33 @@ const DEFAULTS = []; // Rule Definition // ------------------------------------------------------------------------------ +/** @typedef {{ disallowList: null | string[]; message: null | string; disallowedValues: string[] | null }} 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; + } + + 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} */ @@ -70,6 +80,13 @@ module.exports = { type: 'string', }, }, + disallowedValues: { + type: 'array', + uniqueItems: true, + items: { + type: 'string', + }, + }, message: { type: 'string', }, @@ -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, }]; })); @@ -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, }, }); }, diff --git a/tests/lib/rules/forbid-dom-props.js b/tests/lib/rules/forbid-dom-props.js index a11d39fe00..67cf34da1e 100644 --- a/tests/lib/rules/forbid-dom-props.js +++ b/tests/lib/rules/forbid-dom-props.js @@ -112,6 +112,75 @@ ruleTester.run('forbid-dom-props', rule, { }, ], }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + disallowedValues: [], + }, + ], + }, + ], + }, + { + code: ` + const First = (props) => ( + + ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + disallowedValues: ['someValue'], + }, + ], + }, + ], + }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + disallowedValues: ['someValue'], + }, + ], + }, + ], + }, + { + code: ` + const First = (props) => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propName: 'someProp', + disallowedValues: ['someValue'], + disallowedFor: ['span'], + }, + ], + }, + ], + }, ]), invalid: parsers.all([ @@ -191,6 +260,58 @@ 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', + disallowedValues: ['someValue'], + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbiddenWithValue', + data: { prop: 'someProp', propValue: 'someValue' }, + line: 3, + column: 16, + type: 'JSXAttribute', + }, + ], + }, { code: ` const First = (props) => ( @@ -324,5 +445,70 @@ 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'], + 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', + }, + ], + }, ]), });