From 4ecf034e0d2a813a5d1b9f1500ea52bda0fbc845 Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Mon, 23 Sep 2024 13:20:03 -0700 Subject: [PATCH 1/2] [Dev Deps] update `eslint-plugin-import`, `gfm-footnotes` --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index cf2a85b153..2e541d99e1 100644 --- a/package.json +++ b/package.json @@ -60,12 +60,12 @@ "eslint-config-airbnb-base": "^15.0.0", "eslint-doc-generator": "^1.7.1", "eslint-plugin-eslint-plugin": "^2.3.0 || ^3.5.3 || ^4.0.1 || ^5.0.5", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.30.0", "eslint-remote-tester": "^3.0.1", "eslint-remote-tester-repositories": "^1.0.1", "eslint-scope": "^3.7.3", "espree": "^3.5.4", - "gfm-footnotes": "^1.0.0", + "gfm-footnotes": "^1.0.1", "glob": "=10.3.7", "istanbul": "^0.4.5", "jackspeak": "=2.1.1", From 3e01e105c2ec900d1754503b94fea22dfb2f441c Mon Sep 17 00:00:00 2001 From: Bohdan Yefimenko Date: Wed, 21 Aug 2024 22:42:22 +0300 Subject: [PATCH 2/2] [New] `forbid-component-props`: add `allowedForPatterns`/`disallowedForPatterns` options --- docs/rules/forbid-component-props.md | 26 ++- lib/rules/forbid-component-props.js | 75 ++++++++- tests/lib/rules/forbid-component-props.js | 193 ++++++++++++++++++++++ 3 files changed, 278 insertions(+), 16 deletions(-) diff --git a/docs/rules/forbid-component-props.md b/docs/rules/forbid-component-props.md index 3d796c648d..61bc4e59ed 100644 --- a/docs/rules/forbid-component-props.md +++ b/docs/rules/forbid-component-props.md @@ -55,7 +55,17 @@ custom message, and a component allowlist: } ``` -For glob string patterns: +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" +} +``` + +For `propNamePattern` glob string patterns: ```js { @@ -65,23 +75,23 @@ For glob string patterns: } ``` -Use `disallowedFor` as an exclusion list to warn on props for specific components. `disallowedFor` must have at least one item. +Use `allowedForPatterns` for glob string patterns: ```js { "propName": "someProp", - "disallowedFor": ["SomeComponent", "AnotherComponent"], - "message": "Avoid using someProp for SomeComponent and AnotherComponent" + "allowedForPatterns": ["*Component"], + "message": "Avoid using `someProp` except components that match the `*Component` pattern" } ``` -For glob string patterns: +Use `disallowedForPatterns` for glob string patterns: ```js { - "propNamePattern": "**-**", - "disallowedFor": ["MyComponent"], - "message": "Avoid using kebab-case for MyComponent" + "propName": "someProp", + "disallowedForPatterns": ["*Component"], + "message": "Avoid using `someProp` for components that match the `*Component` pattern" } ``` diff --git a/lib/rules/forbid-component-props.js b/lib/rules/forbid-component-props.js index 20b11d9218..2dd4412b87 100644 --- a/lib/rules/forbid-component-props.js +++ b/lib/rules/forbid-component-props.js @@ -52,6 +52,11 @@ module.exports = { uniqueItems: true, items: { type: 'string' }, }, + allowedForPatterns: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + }, message: { type: 'string' }, }, additionalProperties: false, @@ -66,12 +71,20 @@ module.exports = { minItems: 1, items: { type: 'string' }, }, + disallowedForPatterns: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { type: 'string' }, + }, message: { type: 'string' }, }, - required: ['disallowedFor'], + anyOf: [ + { required: ['disallowedFor'] }, + { required: ['disallowedForPatterns'] }, + ], additionalProperties: false, }, - { type: 'object', properties: { @@ -81,6 +94,11 @@ module.exports = { uniqueItems: true, items: { type: 'string' }, }, + allowedForPatterns: { + type: 'array', + uniqueItems: true, + items: { type: 'string' }, + }, message: { type: 'string' }, }, additionalProperties: false, @@ -95,9 +113,18 @@ module.exports = { minItems: 1, items: { type: 'string' }, }, + disallowedForPatterns: { + type: 'array', + uniqueItems: true, + minItems: 1, + items: { type: 'string' }, + }, message: { type: 'string' }, }, - required: ['disallowedFor'], + anyOf: [ + { required: ['disallowedFor'] }, + { required: ['disallowedForPatterns'] }, + ], additionalProperties: false, }, ], @@ -114,8 +141,10 @@ module.exports = { const propPattern = value.propNamePattern; const prop = propName || propPattern; const options = { - allowList: typeof value === 'string' ? [] : (value.allowedFor || []), - disallowList: typeof value === 'string' ? [] : (value.disallowedFor || []), + allowList: [].concat(value.allowedFor || []), + allowPatternList: [].concat(value.allowedForPatterns || []), + disallowList: [].concat(value.disallowedFor || []), + disallowPatternList: [].concat(value.disallowedForPatterns || []), message: typeof value === 'string' ? null : value.message, isPattern: !!value.propNamePattern, }; @@ -140,10 +169,40 @@ module.exports = { return false; } + function checkIsTagForbiddenByAllowOptions() { + if (options.allowList.indexOf(tagName) !== -1) { + return false; + } + + if (options.allowPatternList.length === 0) { + return true; + } + + return options.allowPatternList.every( + (pattern) => !minimatch(tagName, pattern) + ); + } + + function checkIsTagForbiddenByDisallowOptions() { + if (options.disallowList.indexOf(tagName) !== -1) { + return true; + } + + if (options.disallowPatternList.length === 0) { + return false; + } + + return options.disallowPatternList.some( + (pattern) => minimatch(tagName, pattern) + ); + } + + const hasDisallowOptions = options.disallowList.length > 0 || options.disallowPatternList.length > 0; + // 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; + const isTagForbidden = hasDisallowOptions + ? checkIsTagForbiddenByDisallowOptions() + : checkIsTagForbiddenByAllowOptions(); // if the tagName is undefined (``), we assume it's a forbidden element return typeof tagName === 'undefined' || isTagForbidden; diff --git a/tests/lib/rules/forbid-component-props.js b/tests/lib/rules/forbid-component-props.js index d97299e1bd..566860d139 100644 --- a/tests/lib/rules/forbid-component-props.js +++ b/tests/lib/rules/forbid-component-props.js @@ -250,6 +250,78 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + allowedForPatterns: ['*Icon', '*Svg', 'UI*'], + }, + ], + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + allowedFor: ['ButtonLegacy'], + allowedForPatterns: ['*Icon', '*Svg', 'UI*'], + }, + ], + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + disallowedFor: ['Modal'], + disallowedForPatterns: ['*Legacy', 'Shared*'], + }, + ], + }, + ], + }, ]), invalid: parsers.all([ @@ -679,5 +751,126 @@ ruleTester.run('forbid-component-props', rule, { }, ], }, + { + code: ` + const rootElement = () => ( + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + message: 'className available only for icons', + allowedForPatterns: ['*Icon'], + }, + ], + }, + ], + errors: [ + { + message: 'className available only for icons', + line: 5, + column: 22, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const rootElement = () => ( + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + message: 'className available only for icons', + allowedForPatterns: ['*Icon'], + }, + { + propName: 'style', + message: 'style available only for SVGs', + allowedForPatterns: ['*Svg'], + }, + ], + }, + ], + errors: [ + { + message: 'style available only for SVGs', + line: 4, + column: 21, + type: 'JSXAttribute', + }, + { + message: 'className available only for icons', + line: 6, + column: 22, + type: 'JSXAttribute', + }, + ], + }, + { + code: ` + const rootElement = ( + + + + + + + + ); + `, + options: [ + { + forbid: [ + { + propName: 'className', + disallowedFor: ['SomeSvg'], + disallowedForPatterns: ['UI*', '*Icon'], + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + }, + ], + }, + ], + errors: [ + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 4, + column: 23, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 5, + column: 26, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 6, + column: 22, + type: 'JSXAttribute', + }, + { + message: 'Avoid using className for SomeSvg and components that match the `UI*` and `*Icon` patterns', + line: 7, + column: 21, + type: 'JSXAttribute', + }, + ], + }, ]), });