From 6ce58e52cda582171522bb27279b3329c22ae800 Mon Sep 17 00:00:00 2001 From: akulsr0 Date: Thu, 27 Jun 2024 14:23:56 +0530 Subject: [PATCH] [New] `forbid-component-props`: add `propNamePattern` to allow / disallow prop name patterns --- CHANGELOG.md | 2 + docs/rules/forbid-component-props.md | 22 +++- lib/rules/forbid-component-props.js | 51 ++++++++- tests/lib/rules/forbid-component-props.js | 126 ++++++++++++++++++++++ 4 files changed, 197 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196b55b345..54b106d9ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange * support eslint v9 ([#3759][] @mdjermanovic) * export flat configs from plugin root and fix flat config crash ([#3694][] @bradzacher @mdjermanovic) * add [`jsx-props-no-spread-multi`] ([#3724][] @SimonSchick) +* [`forbid-component-props`]: add `propNamePattern` to allow / disallow prop name patterns ([#3774][] @akulsr0) +[#3774]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3774 [#3759]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3759 [#3724]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3724 [#3694]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3694 diff --git a/docs/rules/forbid-component-props.md b/docs/rules/forbid-component-props.md index 2d624be31c..3d796c648d 100644 --- a/docs/rules/forbid-component-props.md +++ b/docs/rules/forbid-component-props.md @@ -44,7 +44,7 @@ Examples of **correct** code for this rule: ### `forbid` An array specifying the names of props that are forbidden. The default value of this option is `['className', 'style']`. -Each array element can either be a string with the property name or object specifying the property name, an optional +Each array element can either be a string with the property name or object specifying the property name or glob string, an optional custom message, and a component allowlist: ```js @@ -55,6 +55,16 @@ custom message, and a component allowlist: } ``` +For glob string patterns: + +```js +{ + "propNamePattern": '**-**', + "allowedFor": ['div'], + "message": "Avoid using kebab-case except div" +} +``` + Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item. ```js @@ -65,6 +75,16 @@ Use `disallowedFor` as an exclusion list to warn on props for specific component } ``` +For glob string patterns: + +```js +{ + "propNamePattern": "**-**", + "disallowedFor": ["MyComponent"], + "message": "Avoid using kebab-case for MyComponent" +} +``` + ### Related rules - [forbid-dom-props](./forbid-dom-props.md) diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js index a72a5afafb..20b11d9218 100644 --- a/lib/rules/forbid-component-props.js +++ b/lib/rules/forbid-component-props.js @@ -5,6 +5,7 @@ 'use strict'; +const minimatch = require('minimatch'); const docsUrl = require('../util/docsUrl'); const report = require('../util/report'); @@ -70,6 +71,35 @@ module.exports = { required: ['disallowedFor'], additionalProperties: false, }, + + { + type: 'object', + properties: { + propNamePattern: { type: 'string' }, + allowedFor: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + }, + message: { type: 'string' }, + }, + additionalProperties: false, + }, + { + type: 'object', + properties: { + propNamePattern: { type: 'string' }, + disallowedFor: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { type: 'string' }, + }, + message: { type: 'string' }, + }, + required: ['disallowedFor'], + additionalProperties: false, + }, ], }, }, @@ -81,16 +111,31 @@ module.exports = { const configuration = context.options[0] || {}; const forbid = new Map((configuration.forbid || DEFAULTS).map((value) => { const propName = typeof value === 'string' ? value : value.propName; + const propPattern = value.propNamePattern; + const prop = propName || propPattern; const options = { allowList: typeof value === 'string' ? [] : (value.allowedFor || []), disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []), message: typeof value === 'string' ? null : value.message, + isPattern: !!value.propNamePattern, }; - return [propName, options]; + return [prop, options]; })); + function getPropOptions(prop) { + // Get config options having pattern + const propNamePatternArray = Array.from(forbid.entries()).filter((propEntry) => propEntry[1].isPattern); + // Match current prop with pattern options, return if matched + const propNamePattern = propNamePatternArray.find((propPatternVal) => minimatch(prop, propPatternVal[0])); + // Get options for matched propNamePattern + const propNamePatternOptions = propNamePattern && propNamePattern[1]; + + const options = forbid.get(prop) || propNamePatternOptions; + return options; + } + function isForbidden(prop, tagName) { - const options = forbid.get(prop); + const options = getPropOptions(prop); if (!options) { return false; } @@ -121,7 +166,7 @@ module.exports = { return; } - const customMessage = forbid.get(prop).message; + const customMessage = getPropOptions(prop).message; report(context, customMessage || messages.propIsForbidden, !customMessage && 'propIsForbidden', { node, diff --git a/tests/lib/rules/forbid-component-props.js b/tests/lib/rules/forbid-component-props.js index baf02e44a3..d97299e1bd 100644 --- a/tests/lib/rules/forbid-component-props.js +++ b/tests/lib/rules/forbid-component-props.js @@ -233,6 +233,23 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const MyComponent = () => ( +
+ ); + `, + options: [ + { + forbid: [ + { + propNamePattern: '**-**', + allowedFor: ['div'], + }, + ], + }, + ], + }, ]), invalid: parsers.all([ @@ -553,5 +570,114 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const MyComponent = () => ( + + ); + `, + options: [ + { + forbid: [ + { + propNamePattern: '**-**', + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbidden', + data: { prop: 'kebab-case-prop' }, + line: 3, + column: 16, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const MyComponent = () => ( + + ); + `, + options: [ + { + forbid: [ + { + propNamePattern: '**-**', + message: 'Avoid using kebab-case', + }, + ], + }, + ], + errors: [ + { + message: 'Avoid using kebab-case', + line: 3, + column: 16, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const MyComponent = () => ( +
+
+ +
+ ); + `, + options: [ + { + forbid: [ + { + propNamePattern: '**-**', + allowedFor: ['div'], + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbidden', + data: { prop: 'kebab-case-prop' }, + line: 5, + column: 18, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const MyComponent = () => ( +
+
+

+ +

+ ); + `, + options: [ + { + forbid: [ + { + propNamePattern: '**-**', + disallowedFor: ['Foo'], + }, + ], + }, + ], + errors: [ + { + messageId: 'propIsForbidden', + data: { prop: 'kebab-case-prop' }, + line: 6, + column: 18, + type: 'JSXAttribute', + }, + ], + }, ]), });